" record.vim - Don Yang (uguu.org) " " To use, source this script and run the following command to start " recording: " :RecordStart " " Where is the name of the output file. If it already " exists, new entries will be appended. " " Run this command to stop recording: " :RecordStop " " Always stop recording before closing any buffer, otherwise event log " will be lost. " " " Output lines come in one of two formats: " Record cursor position and edit time: " YXFT " Snapshot a single line: " LE= " " Format notes: " refers to 1-based line number (first line is 1) " " refers to 1-based column number (first column is 1). For " UTF-8 characters, each byte increments this character by 1. When " VIM displays column numbers with something like "4-3", the first " number is what should be stored. This is also the column number " returned by getpos(). " " and is always non-negative. First frame is 0. " " Multiple for the same is accepted (and is used to " differentiate subsecond events). Multiple for the same " is illegal. " " is always in UTF-8 encoding. For this to work, 'encoding' " for the current buffer must be set to UTF-8. " " This script should capture enough to show how a file changes over time, " but since most VIM handlers trigger before a change happens rather than " after, it's not a faithful representation of the editing session. " " 05/15/11 let s:cpo = &cpoptions set cpoptions-=C "...................................................................... " Snapshot functions " Record current cursor position function! s:SnapshotCursor(state) let a:state.cursor = getpos('.') let a:state.records = add(a:state.records, \'Y' . a:state.cursor[1] . \'X' . a:state.cursor[2] . \'F' . a:state.frame . \'T' . a:state.uptime) endfunction " Take a snapshot some range of lines and append to output buffer function! s:SnapshotLines(state, start_line, end_line) let l:i = a:start_line while l:i <= a:end_line let a:state.records = add(a:state.records, \'L' . l:i . \'E' . a:state.last_size . \'=' . getline(l:i)) let l:i += 1 endwhile endfunction " Snapshot from one line to end of file function! s:SnapshotToEndOfFile(state, start_line) let a:state.last_size = line('$') call s:SnapshotLines(a:state, a:start_line, a:state.last_size) endfunction " Snapshot the entire file function! s:SnapshotFileAndCursor(state) call s:SnapshotCursor(a:state) call s:SnapshotToEndOfFile(a:state, 1) let a:state.last_cursor = a:state.cursor endfunction " Append buffered records to file function! s:FlushRecords(state) exec "redir >>" . a:state.output_file for l:i in a:state.records silent! echon l:i "\n" endfor redir END let a:state.records = [] endfunction "...................................................................... " Event handlers " Update timestamps in preparation for snapshot operations. This is " the only function that calls localtime(), this is to ensure consistency " between a:state.frame and a:state.uptime. function! s:UpdateTimestamps(state) let a:state.frame += 1 let a:state.uptime = localtime() - a:state.start_time endfunction " Snapshot file and write buffered records when idle function! s:OnCursorHold(state) " Record cursor position and current file snapshot call s:UpdateTimestamps(a:state) call s:SnapshotFileAndCursor(a:state) " Flush pending records to disk call s:FlushRecords(a:state) endfunction " Record changes to recently touched line function! s:OnCursorMoved(state) let a:state.last_cursor = a:state.cursor let a:state.cursor = getpos('.') " Snapshot on change. The snapshot goes with the last frame. if a:state.last_change != changenr() " If file size has changed, record the entire file to be sure, " otherwise just snapshot the recently touched lines if a:state.last_size != line('$') call s:SnapshotToEndOfFile(a:state, 1) else call s:SnapshotLines(a:state, \min([a:state.last_cursor[1], \a:state.cursor[1]]), \max([a:state.last_cursor[1], \a:state.cursor[1]])) endif let a:state.last_change = changenr() endif " Record cursor position call s:UpdateTimestamps(a:state) call s:SnapshotCursor(a:state) endfunction " Record changes to recently touched line function! s:OnCursorMovedI(state) let a:state.last_cursor = a:state.cursor let a:state.cursor = getpos('.') " On beginning of a changed group, record the start of the changed " line range. if a:state.last_change != changenr() let a:state.insert_start_line = min([a:state.cursor[1], \a:state.last_cursor[1]]) let a:state.last_change = changenr() endif " If file size has changed, record from first changed line to end of file if a:state.last_size != line('$') call s:SnapshotToEndOfFile(a:state, \min([a:state.insert_start_line, \a:state.cursor[1]])) else " File size is unchanged, if cursor moved to a different line, " snapshot the current and previous lines. Otherwise, snapshot " just the current line. if a:state.last_cursor[1] != a:state.cursor[1] call s:SnapshotLines(a:state, \min([a:state.last_cursor[1], \a:state.cursor[1]]), \max([a:state.last_cursor[1], \a:state.cursor[1]])) else call s:SnapshotLines(a:state, a:state.cursor[1], a:state.cursor[1]) endif endif " Record cursor position call s:UpdateTimestamps(a:state) call s:SnapshotCursor(a:state) endfunction " Record first inserted line function! s:OnInsertEnter(state) call s:UpdateTimestamps(a:state) call s:SnapshotFileAndCursor(a:state) let a:state.insert_start_line = a:state.cursor[1] let a:state.last_change = changenr() endfunction " Snapshot the entire file function! s:OnInsertLeave(state) call s:UpdateTimestamps(a:state) call s:SnapshotFileAndCursor(a:state) endfunction "...................................................................... " Setup and teardown " Stop recording function! s:RecordStopInternal() " Remove event handlers autocmd! InsertEnter,InsertLeave autocmd! CursorMoved,CursorMovedI autocmd! CursorHold,CursorHoldI " Restore command to start recording delcommand RecordStop com! -nargs=1 -buffer RecordStart :call RecordStartInternal() " Snapshot file unconditionally before flush let l:state = s:state[bufnr('')] call s:SnapshotFileAndCursor(l:state) " Flush buffered records call s:FlushRecords(l:state) " Cleanup state let &updatetime = l:state.old_updatetime unlet! l:state.output_file unlet! l:state.frame unlet! l:state.start_time unlet! l:state.uptime unlet! l:state.cursor unlet! l:state.last_cursor unlet! l:state.last_change unlet! l:state.last_size unlet! l:state.insert_start_line unlet! l:state.records endfunction " Start recording function! s:RecordStartInternal(output) " Add command to stop recording delcommand RecordStart command! -buffer RecordStop :call RecordStopInternal() " Initialize state let l:state = s:state[bufnr('')] let l:state.output_file = a:output let l:state.frame = 0 let l:state.start_time = localtime() let l:state.uptime = 0 let l:state.last_change = changenr() let l:state.old_updatetime = &updatetime if l:state.old_updatetime > 1000 set updatetime=1000 endif " Get initial snapshot let l:state.records = [] call s:SnapshotFileAndCursor(l:state) let l:state.insert_start_line = l:state.cursor[1] " Install event handlers au! InsertEnter :call OnInsertEnter(s:state[bufnr('')]) au! InsertLeave :call SnapshotFileAndCursor(s:state[bufnr('')]) au! CursorMoved :call OnCursorMoved(s:state[bufnr('')]) au! CursorMovedI :call OnCursorMovedI(s:state[bufnr('')]) au! CursorHold :call OnCursorHold(s:state[bufnr('')]) au! CursorHoldI :call OnCursorHold(s:state[bufnr('')]) endfunction " When we enter a buffer for the first time, register command to start " recording for the current buffer. This is triggered on BufEnter " rather than on other events to ensure that it's executed on every " new buffer, with bufnr('') referencing the current buffer. function! s:InitHandlers() if exists("s:state[bufnr('')]") return endif let s:state[bufnr('')] = {} com! -nargs=1 -buffer RecordStart :call RecordStartInternal() endfunction " Flush pending events for current buffer. This is a last ditch " effort to save the event log before it's lost. In practice it " doesn't work so well because the per-buffer variables might not be " accessible anymore, and most of these events don't trigger when user " is trying to exit with ":q", so the only sure way to make sure event " logs are not lost is to call RecordStop. function! s:FlushBuffer() if v:dying return endif if exists("s:state[bufnr('')].output_file") call s:SnapshotFileAndCursor(s:state[bufnr('')]) endif endfunction " Initialize script state (indexed by current buffered number) let s:state = {} " Use autocommands to setup commands. These are normally triggered if " users put "runtime record.vim" in their .vimrc or .gvimrc. autocmd BufEnter * :call InitHandlers() autocmd BufLeave,WinLeave * :call FlushBuffer() autocmd VimLeave * :bufdo :call FlushBuffer() " Setup script command manually. This is so that the commands are " setup when the script is not part of VIM's startup process. call s:InitHandlers() let &cpoptions = s:cpo