| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- # vim: set fdm=marker ts=4 sw=4 et:
- # ============================================================================
- # File: mundo.py
- # Description: vim global plugin to visualize your undo tree
- # Maintainer: Hyeon Kim <simnalamburt@gmail.com>
- # License: GPLv2+ -- look it up.
- # Notes: Much of this code was thieved from Mercurial, and the rest was
- # heavily inspired by scratch.vim and histwin.vim.
- #
- # ============================================================================
- import re
- import sys
- import tempfile
- import vim
- from mundo.node import Nodes
- import mundo.util as util
- import mundo.graphlog as graphlog
- # Python Vim utility functions -----------------------------------------------------#{{{
- MISSING_BUFFER = "Cannot find Mundo's target buffer (%s)"
- MISSING_WINDOW = "Cannot find window (%s) for Mundo's target buffer (%s)"
- def _check_sanity():
- '''Check to make sure we're not crazy.
- Does the following things:
- * Make sure the target buffer still exists.
- '''
- global nodesData
- if not nodesData:
- nodesData = Nodes()
- b = int(vim.eval('g:mundo_target_n'))
- if not vim.eval('bufloaded(%d)' % int(b)):
- vim.command('echo "%s"' % (MISSING_BUFFER % b))
- return False
- w = int(vim.eval('bufwinnr(%d)' % int(b)))
- if w == -1:
- vim.command('echo "%s"' % (MISSING_WINDOW % (w, b)))
- return False
- return True
- INLINE_HELP = '''\
- " Mundo (%d) - Press ? for Help:
- " %s/%s - Next/Prev undo state.
- " J/K - Next/Prev write state.
- " i - Toggle 'inline diff' mode.
- " / - Find changes that match string.
- " n/N - Next/Prev undo that matches search.
- " P - Play current state to selected undo.
- " d - Vert diff of undo with current state.
- " p - Diff of selected undo and current state.
- " r - Diff of selected undo and prior undo.
- " q - Quit!
- " <cr> - Revert to selected state.
- '''
- #}}}
- nodesData = Nodes()
- # from profilehooks import profile
- # @profile(immediate=True)
- def MundoRenderGraph(force=False):
- if not _check_sanity():
- return
- first_visible_line = int(vim.eval("line('w0')"))
- last_visible_line = int(vim.eval("line('w$')"))
- verbose = vim.eval('g:mundo_verbose_graph') == "1"
- target = (int(vim.eval('g:mundo_target_n')),
- vim.eval('g:mundo_map_move_older'),
- vim.eval('g:mundo_map_move_newer'))
- if int(vim.eval('g:mundo_help')):
- header = (INLINE_HELP % target).splitlines()
- else:
- header = [(INLINE_HELP % target).splitlines()[0], '\n']
- show_inline_undo = int(vim.eval("g:mundo_inline_undo")) == 1
- mundo_last_visible_line = int(vim.eval("g:mundo_last_visible_line"))
- mundo_first_visible_line = int(vim.eval("g:mundo_first_visible_line"))
- if not force and not nodesData.is_outdated() and (
- not show_inline_undo or
- (
- mundo_first_visible_line == first_visible_line and
- mundo_last_visible_line == last_visible_line
- )
- ):
- return
- result = graphlog.generate(
- verbose,
- len(header)+1,
- first_visible_line,
- last_visible_line,
- show_inline_undo,
- nodesData
- )
- vim.command("let g:mundo_last_visible_line=%s"%last_visible_line)
- vim.command("let g:mundo_first_visible_line=%s"%first_visible_line)
- output = []
- # right align the dag and flip over the y axis:
- flip_dag = int(vim.eval("g:mundo_mirror_graph")) == 1
- dag_width = 1
- maxwidth = int(vim.eval("g:mundo_width"))
- for line in result:
- if len(line[0]) > dag_width:
- dag_width = len(line[0])
- for line in result:
- if flip_dag:
- dag_line = (line[0][::-1]).replace("/","\\")
- output.append("%*s %s"% (dag_width,dag_line,line[1]))
- else:
- output.append("%-*s %s"% (dag_width,line[0],line[1]))
- vim.command('call s:MundoOpenGraph()')
- vim.command('setlocal modifiable')
- lines = (header + output)
- lines = [line.rstrip('\n') for line in lines]
- vim.current.buffer[:] = lines
- vim.command('setlocal nomodifiable')
- i = 1
- for line in output:
- try:
- line.split('[')[0].index('@')
- i += 1
- break
- except ValueError:
- pass
- i += 1
- vim.command('%d' % (i+len(header)-1))
- def MundoRenderPreview():
- if not _check_sanity():
- return
- target_state = MundoGetTargetState()
- # Check that there's an undo state. There may not be if we're talking about
- # a buffer with no changes yet.
- if target_state == None:
- util._goto_window_for_buffer_name('__Mundo__')
- return
- else:
- target_state = int(target_state)
- util._goto_window_for_buffer(vim.eval('g:mundo_target_n'))
- nodes, nmap = nodesData.make_nodes()
- node_after = nmap[target_state]
- node_before = node_after.parent
- vim.command('call s:MundoOpenPreview()')
- util._output_preview_text(nodesData.preview_diff(node_before, node_after))
- util._goto_window_for_buffer_name('__Mundo__')
- def MundoGetTargetState():
- """ Get the current undo number that mundo is at. """
- util._goto_window_for_buffer_name('__Mundo__')
- target_line = vim.eval("getline('.')")
- matches = re.match('^.* \[([0-9]+)\] .*$',target_line)
- if matches:
- return int(matches.group(1))
- return 0
- def GetNextLine(direction,move_count,write,start="line('.')"):
- start_line_no = int(vim.eval(start))
- start_line = vim.eval("getline(%d)" % start_line_no)
- mundo_verbose_graph = vim.eval('g:mundo_verbose_graph')
- if mundo_verbose_graph != "0":
- distance = 2
- # If we're in between two nodes we move by one less to get back on track.
- if start_line.find('[') == -1:
- distance = distance - 1
- else:
- distance = 1
- nextline = vim.eval("getline(%d)" % (start_line_no+direction))
- idx1 = nextline.find('@')
- idx2 = nextline.find('o')
- idx3 = nextline.find('w')
- # if the next line is not a revision - then go down one more.
- if (idx1+idx2+idx3) == -3:
- distance = distance + 1
- next_line = start_line_no + distance*direction
- if move_count > 1:
- return GetNextLine(direction,move_count-1,write,str(next_line))
- elif write:
- newline = vim.eval("getline(%d)" % (next_line))
- if newline.find('w ') == -1:
- # make sure that if we can't go up/down anymore that we quit out.
- if direction < 0 and next_line == 1:
- return next_line
- if direction > 0 and next_line >= len(vim.current.window.buffer):
- return next_line
- return GetNextLine(direction,1,write,str(next_line))
- return next_line
- def MundoMove(direction,move_count=1,relative=True,write=False):
- """
- Move within the undo graph in the direction specified (or to the specific
- undo node specified).
- Parameters:
- direction - -1/1 (up/down). when 'relative' if False, the undo node to
- move to.
- move_count - how many times to perform the operation (irrelevent for
- relative == False).
- relative - whether to move up/down, or to jump to a specific undo node.
- write - If True, move to the next written undo.
- """
- if relative:
- target_n = GetNextLine(direction,move_count,write)
- else:
- updown = 1
- if MundoGetTargetState() < direction:
- updown = -1
- target_n = GetNextLine(updown,abs(MundoGetTargetState()-direction),write)
- # Bound the movement to the graph.
- help_lines = 3
- if int(vim.eval('g:mundo_help')):
- help_lines = len(INLINE_HELP.split('\n'))
- if target_n <= help_lines:
- vim.command("call cursor(%d, 0)" % help_lines)
- else:
- vim.command("call cursor(%d, 0)" % target_n)
- line = vim.eval("getline('.')")
- # Move to the node, whether it's an @, o, or w
- idx1 = line.find('@ ')
- idx2 = line.find('o ')
- idx3 = line.find('w ')
- idxs = []
- if idx1 != -1:
- idxs.append(idx1)
- if idx2 != -1:
- idxs.append(idx2)
- if idx3 != -1:
- idxs.append(idx3)
- minidx = min(idxs)
- if idx1 == minidx:
- vim.command("call cursor(0, %d + 1)" % idx1)
- elif idx2 == minidx:
- vim.command("call cursor(0, %d + 1)" % idx2)
- else:
- vim.command("call cursor(0, %d + 1)" % idx3)
- if vim.eval('g:mundo_auto_preview') == '1':
- MundoRenderPreview()
- def MundoSearch():
- search = vim.eval("input('/')");
- vim.command('let @/="%s"'% search.replace("\\","\\\\").replace('"','\\"'))
- MundoNextMatch()
- def MundoPrevMatch():
- MundoMatch(-1)
- def MundoNextMatch():
- MundoMatch(1)
- def MundoMatch(down):
- """ Jump to the next node that matches the current pattern. If there is a
- next node, search from the next node to the end of the list of changes. Stop
- on a match. """
- if not _check_sanity():
- return
- # save the current window number (should be the navigation window)
- # then generate the undo nodes, and then go back to the current window.
- util._goto_window_for_buffer(vim.eval('g:mundo_target_n'))
- nodes, nmap = nodesData.make_nodes()
- total = len(nodes) - 1
- util._goto_window_for_buffer_name('__Mundo__')
- curline = int(vim.eval("line('.')"))
- mundo_node = MundoGetTargetState()
- found_version = -1
- if total > 0:
- therange = range(mundo_node-1,-1,-1)
- if down < 0:
- therange = range(mundo_node+1,total+1)
- for version in therange:
- util._goto_window_for_buffer_name('__Mundo__')
- undochanges = nodesData.preview_diff(nmap[version].parent, nmap[version])
- # Look thru all of the changes, ignore the first two b/c those are the
- # diff timestamp fields (not relevent):
- for change in undochanges[3:]:
- match_index = vim.eval('match("%s",@/)'% change.replace("\\","\\\\").replace('"','\\"'))
- # only consider the matches that are actual additions or
- # subtractions
- if int(match_index) >= 0 and (change.startswith('-') or change.startswith('+')):
- found_version = version
- break
- # found something, lets get out of here:
- if found_version != -1:
- break
- util._goto_window_for_buffer_name('__Mundo__')
- if found_version >= 0:
- MundoMove(found_version,1,False)
- def MundoRenderPatchdiff():
- """ Call MundoRenderChangePreview and display a vert diffpatch with the
- current file. """
- if MundoRenderChangePreview():
- # if there are no lines, do nothing (show a warning).
- util._goto_window_for_buffer_name('__Mundo_Preview__')
- if vim.current.buffer[:] == ['']:
- # restore the cursor position before exiting.
- util._goto_window_for_buffer_name('__Mundo__')
- vim.command('unsilent echo "No difference between current file and undo number!"')
- return False
- # quit out of mundo main screen
- util._goto_window_for_buffer_name('__Mundo__')
- vim.command('quit')
- # save the __Mundo_Preview__ buffer to a temp file.
- util._goto_window_for_buffer_name('__Mundo_Preview__')
- (handle,filename) = tempfile.mkstemp()
- vim.command('silent! w %s' % (filename))
- # exit the __Mundo_Preview__ window
- vim.command('bdelete')
- # diff the temp file
- vim.command('silent! keepalt vert diffpatch %s' % (filename))
- vim.command('set buftype=nofile bufhidden=delete')
- return True
- return False
- def MundoGetChangesForLine():
- if not _check_sanity():
- return False
- target_state = MundoGetTargetState()
- # Check that there's an undo state. There may not be if we're talking about
- # a buffer with no changes yet.
- if target_state == None:
- util._goto_window_for_buffer_name('__Mundo__')
- return False
- else:
- target_state = int(target_state)
- util._goto_window_for_buffer(vim.eval('g:mundo_target_n'))
- nodes, nmap = nodesData.make_nodes()
- node_after = nmap[target_state]
- node_before = nmap[nodesData.current()]
- return nodesData.change_preview_diff(node_before, node_after)
- def MundoRenderChangePreview():
- """ Render the selected undo level with the current file.
- Return True on success, False on failure. """
- if not _check_sanity():
- return
- vim.command('call s:MundoOpenPreview()')
- util._output_preview_text(MundoGetChangesForLine())
- util._goto_window_for_buffer_name('__Mundo__')
- return True
- def MundoRenderToggleInlineDiff():
- show_inline = int(vim.eval('g:mundo_inline_undo'))
- if show_inline == 0:
- vim.command("let g:mundo_inline_undo=1")
- else:
- vim.command("let g:mundo_inline_undo=0")
- line = int(vim.eval("line('.')"))
- nodesData.clear_oneline_diffs()
- MundoRenderGraph(True)
- vim.command("call cursor(%d,0)" % line)
- def MundoToggleHelp():
- show_help = int(vim.eval('g:mundo_help'))
- if show_help == 0:
- vim.command("let g:mundo_help=1")
- else:
- vim.command("let g:mundo_help=0")
- line = int(vim.eval("line('.')"))
- column = int(vim.eval("col('.')"))
- old_line_count = int(vim.eval("line('$')"))
- MundoRenderGraph(True)
- new_line_count = int(vim.eval("line('$')"))
- vim.command("call cursor(%d, %d)" % (line + new_line_count - old_line_count, column))
- # Mundo undo/redo
- def MundoRevert():
- if not _check_sanity():
- return
- target_n = MundoGetTargetState()
- back = vim.eval('g:mundo_target_n')
- util._goto_window_for_buffer(back)
- util._undo_to(target_n)
- vim.command('MundoRenderGraph')
- if int(vim.eval('g:mundo_return_on_revert')):
- util._goto_window_for_buffer(back)
- if int(vim.eval('g:mundo_close_on_revert')):
- vim.command('MundoToggle')
- def MundoPlayTo():
- if not _check_sanity():
- return
- target_n = MundoGetTargetState()
- back = int(vim.eval('g:mundo_target_n'))
- delay = int(vim.eval('g:mundo_playback_delay'))
- vim.command('echo "%s"' % back)
- util._goto_window_for_buffer(back)
- util.normal('zR')
- nodes, nmap = nodesData.make_nodes()
- start = nmap[nodesData.current()]
- end = nmap[target_n]
- def _walk_branch(origin, dest):
- rev = origin.n < dest.n
- nodes = []
- if origin.n > dest.n:
- current, final = origin, dest
- else:
- current, final = dest, origin
- while current.n >= final.n:
- if current.n == final.n:
- break
- nodes.append(current)
- current = current.parent
- else:
- return None
- nodes.append(current)
- if rev:
- return reversed(nodes)
- else:
- return nodes
- branch = _walk_branch(start, end)
- if not branch:
- vim.command('unsilent echo "No path to that node from here!"')
- return
- for node in branch:
- util._undo_to(node.n)
- vim.command('MundoRenderGraph')
- util.normal('zz')
- util._goto_window_for_buffer(back)
- vim.command('redraw')
- vim.command('sleep %dm' % delay)
- def initPythonModule():
- if sys.version_info[:2] < (2, 4):
- vim.command('let s:has_supported_python = 0')
|