mirror of
https://github.com/SpaceVim/SpaceVim.git
synced 2025-01-24 02:20:03 +08:00
548 lines
17 KiB
Python
548 lines
17 KiB
Python
# 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 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.
|
|
|
|
Ensures that:
|
|
* The target buffer exists, is loaded and is present in the tab.
|
|
* That neovim is not in terminal mode.
|
|
"""
|
|
global nodesData
|
|
|
|
if not nodesData:
|
|
nodesData = Nodes()
|
|
|
|
# Check that the target buffer exists, is loaded and is present in the tab
|
|
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
|
|
|
|
# Check if we are in terminal mode.
|
|
mode = vim.eval('mode()')
|
|
|
|
if mode == 't':
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
INLINE_HELP = '''\
|
|
" Mundo (%d) - Press ? for Help:
|
|
" j/k - 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):# {{{
|
|
""" Renders the undo graph if necessary, updating it to reflect changes in
|
|
the target buffer's undo tree.
|
|
|
|
Arguments
|
|
---------
|
|
force : bool
|
|
If True, the graph will always be rendered. If False, then the
|
|
graph may not be rendered - when is already current for example.
|
|
"""
|
|
if not _check_sanity():
|
|
return
|
|
|
|
util._goto_window_for_buffer('__Mundo__')
|
|
|
|
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'))
|
|
|
|
header = []
|
|
if int(vim.eval('g:mundo_header')):
|
|
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
|
|
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():# {{{
|
|
""" Opens the preview window if necessary and renders a preview diff. """
|
|
if not _check_sanity():
|
|
return
|
|
|
|
target_state = MundoGetTargetState()
|
|
target_n = int(vim.eval('g:mundo_target_n'))
|
|
|
|
# If there's no target state or the buffer has changed, update the cached
|
|
# undo tree data, redraw the graph and abort preview rendering
|
|
if target_state is None or nodesData.target_n != target_n:
|
|
nodesData.make_nodes()
|
|
MundoRenderGraph(True)
|
|
return
|
|
|
|
util._goto_window_for_buffer(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))
|
|
|
|
# Mark the preview as up-to-date
|
|
vim.command('call mundo#MundoPreviewOutdated(0)')
|
|
# }}}
|
|
|
|
def MundoGetTargetState():# {{{
|
|
""" Get the current undo number that mundo is at. """
|
|
util._goto_window_for_buffer('__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('.')"):# {{{
|
|
""" Recursively finds the line number resulting from undo graph traversal
|
|
according to the given parameters.
|
|
"""
|
|
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 = 0
|
|
if int(vim.eval('g:mundo_header')):
|
|
help_lines = 3
|
|
elif 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)
|
|
if len(idxs)==0:
|
|
minidx=0
|
|
else:
|
|
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)
|
|
|
|
# Mark the preview as outdated
|
|
vim.command('call mundo#MundoPreviewOutdated(1)')
|
|
# }}}
|
|
|
|
def MundoSearch():# {{{
|
|
try:
|
|
search = vim.eval("input('/')")
|
|
except:
|
|
return
|
|
|
|
if len(search) == 0:
|
|
return
|
|
|
|
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(int(vim.eval('g:mundo_target_n')))
|
|
|
|
nodes, nmap = nodesData.make_nodes()
|
|
total = len(nodes) - 1
|
|
|
|
util._goto_window_for_buffer('__Mundo__')
|
|
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('__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('__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('__Mundo_Preview__')
|
|
if vim.current.buffer[:] == ['']:
|
|
# restore the cursor position before exiting.
|
|
util._goto_window_for_buffer('__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('__Mundo__')
|
|
vim.command('quit')
|
|
|
|
# save the __Mundo_Preview__ buffer to a temp file.
|
|
util._goto_window_for_buffer('__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('__Mundo__')
|
|
return False
|
|
else:
|
|
target_state = int(target_state)
|
|
|
|
util._goto_window_for_buffer(int(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 a diff of the target buffer and the selected undo
|
|
tree node. Returns True on success, False otherwise.
|
|
"""
|
|
if not _check_sanity():
|
|
return
|
|
|
|
vim.command('call s:MundoOpenPreview()')
|
|
util._output_preview_text(MundoGetChangesForLine())
|
|
util._goto_window_for_buffer('__Mundo__')
|
|
|
|
# Mark the preview as up-to-date
|
|
vim.command('call mundo#MundoPreviewOutdated(0)')
|
|
|
|
return True
|
|
# }}}
|
|
|
|
def MundoRenderToggleInlineDiff():# {{{
|
|
""" Toggles g:mundo_inline_undo and redraws the graph window. """
|
|
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():# {{{
|
|
""" Toggles g:mundo_help and redraws the graph window. """
|
|
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():# {{{
|
|
""" Reverts the target buffer to the state associated with a selected node
|
|
in the undo graph.
|
|
"""
|
|
if not _check_sanity():
|
|
return
|
|
|
|
target_n = MundoGetTargetState()
|
|
back = int(vim.eval('g:mundo_target_n'))
|
|
|
|
util._goto_window_for_buffer(back)
|
|
util._undo_to(target_n)
|
|
|
|
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():# {{{
|
|
""" Replays changes between the current state and a selected state in
|
|
real-time.
|
|
"""
|
|
if not _check_sanity():
|
|
return
|
|
|
|
target_n = MundoGetTargetState()
|
|
back = int(vim.eval('g:mundo_target_n'))
|
|
delay = int(vim.eval('g:mundo_playback_delay'))
|
|
|
|
util._goto_window_for_buffer(back)
|
|
util.normal('zn')
|
|
|
|
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:
|
|
nodes.append(current)
|
|
current = current.parent
|
|
|
|
if current.n != final.n:
|
|
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!"')
|
|
util.normal('zN')
|
|
return
|
|
|
|
for node in branch:
|
|
util._undo_to(node.n)
|
|
MundoRenderGraph()
|
|
util.normal('zz')
|
|
util._goto_window_for_buffer(back)
|
|
vim.command('redraw | sleep %dm' % delay)
|
|
|
|
util.normal('zN')
|
|
# }}}
|
|
|
|
# vim: set ts=4 sw=4 tw=79 fdm=marker et :
|