1
0
mirror of https://github.com/SpaceVim/SpaceVim.git synced 2025-01-24 05:30:07 +08:00
SpaceVim/bundle/vim-mundo/autoload/mundo.py

548 lines
17 KiB
Python
Raw Normal View History

2020-10-31 15:52:34 +08:00
# 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 :