1
0

mundo.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484
  1. # vim: set fdm=marker ts=4 sw=4 et:
  2. # ============================================================================
  3. # File: mundo.py
  4. # Description: vim global plugin to visualize your undo tree
  5. # Maintainer: Hyeon Kim <simnalamburt@gmail.com>
  6. # License: GPLv2+ -- look it up.
  7. # Notes: Much of this code was thieved from Mercurial, and the rest was
  8. # heavily inspired by scratch.vim and histwin.vim.
  9. #
  10. # ============================================================================
  11. import re
  12. import sys
  13. import tempfile
  14. import vim
  15. from mundo.node import Nodes
  16. import mundo.util as util
  17. import mundo.graphlog as graphlog
  18. # Python Vim utility functions -----------------------------------------------------#{{{
  19. MISSING_BUFFER = "Cannot find Mundo's target buffer (%s)"
  20. MISSING_WINDOW = "Cannot find window (%s) for Mundo's target buffer (%s)"
  21. def _check_sanity():
  22. '''Check to make sure we're not crazy.
  23. Does the following things:
  24. * Make sure the target buffer still exists.
  25. '''
  26. global nodesData
  27. if not nodesData:
  28. nodesData = Nodes()
  29. b = int(vim.eval('g:mundo_target_n'))
  30. if not vim.eval('bufloaded(%d)' % int(b)):
  31. vim.command('echo "%s"' % (MISSING_BUFFER % b))
  32. return False
  33. w = int(vim.eval('bufwinnr(%d)' % int(b)))
  34. if w == -1:
  35. vim.command('echo "%s"' % (MISSING_WINDOW % (w, b)))
  36. return False
  37. return True
  38. INLINE_HELP = '''\
  39. " Mundo (%d) - Press ? for Help:
  40. " %s/%s - Next/Prev undo state.
  41. " J/K - Next/Prev write state.
  42. " i - Toggle 'inline diff' mode.
  43. " / - Find changes that match string.
  44. " n/N - Next/Prev undo that matches search.
  45. " P - Play current state to selected undo.
  46. " d - Vert diff of undo with current state.
  47. " p - Diff of selected undo and current state.
  48. " r - Diff of selected undo and prior undo.
  49. " q - Quit!
  50. " <cr> - Revert to selected state.
  51. '''
  52. #}}}
  53. nodesData = Nodes()
  54. # from profilehooks import profile
  55. # @profile(immediate=True)
  56. def MundoRenderGraph(force=False):
  57. if not _check_sanity():
  58. return
  59. first_visible_line = int(vim.eval("line('w0')"))
  60. last_visible_line = int(vim.eval("line('w$')"))
  61. verbose = vim.eval('g:mundo_verbose_graph') == "1"
  62. target = (int(vim.eval('g:mundo_target_n')),
  63. vim.eval('g:mundo_map_move_older'),
  64. vim.eval('g:mundo_map_move_newer'))
  65. if int(vim.eval('g:mundo_help')):
  66. header = (INLINE_HELP % target).splitlines()
  67. else:
  68. header = [(INLINE_HELP % target).splitlines()[0], '\n']
  69. show_inline_undo = int(vim.eval("g:mundo_inline_undo")) == 1
  70. mundo_last_visible_line = int(vim.eval("g:mundo_last_visible_line"))
  71. mundo_first_visible_line = int(vim.eval("g:mundo_first_visible_line"))
  72. if not force and not nodesData.is_outdated() and (
  73. not show_inline_undo or
  74. (
  75. mundo_first_visible_line == first_visible_line and
  76. mundo_last_visible_line == last_visible_line
  77. )
  78. ):
  79. return
  80. result = graphlog.generate(
  81. verbose,
  82. len(header)+1,
  83. first_visible_line,
  84. last_visible_line,
  85. show_inline_undo,
  86. nodesData
  87. )
  88. vim.command("let g:mundo_last_visible_line=%s"%last_visible_line)
  89. vim.command("let g:mundo_first_visible_line=%s"%first_visible_line)
  90. output = []
  91. # right align the dag and flip over the y axis:
  92. flip_dag = int(vim.eval("g:mundo_mirror_graph")) == 1
  93. dag_width = 1
  94. maxwidth = int(vim.eval("g:mundo_width"))
  95. for line in result:
  96. if len(line[0]) > dag_width:
  97. dag_width = len(line[0])
  98. for line in result:
  99. if flip_dag:
  100. dag_line = (line[0][::-1]).replace("/","\\")
  101. output.append("%*s %s"% (dag_width,dag_line,line[1]))
  102. else:
  103. output.append("%-*s %s"% (dag_width,line[0],line[1]))
  104. vim.command('call s:MundoOpenGraph()')
  105. vim.command('setlocal modifiable')
  106. lines = (header + output)
  107. lines = [line.rstrip('\n') for line in lines]
  108. vim.current.buffer[:] = lines
  109. vim.command('setlocal nomodifiable')
  110. i = 1
  111. for line in output:
  112. try:
  113. line.split('[')[0].index('@')
  114. i += 1
  115. break
  116. except ValueError:
  117. pass
  118. i += 1
  119. vim.command('%d' % (i+len(header)-1))
  120. def MundoRenderPreview():
  121. if not _check_sanity():
  122. return
  123. target_state = MundoGetTargetState()
  124. # Check that there's an undo state. There may not be if we're talking about
  125. # a buffer with no changes yet.
  126. if target_state == None:
  127. util._goto_window_for_buffer_name('__Mundo__')
  128. return
  129. else:
  130. target_state = int(target_state)
  131. util._goto_window_for_buffer(vim.eval('g:mundo_target_n'))
  132. nodes, nmap = nodesData.make_nodes()
  133. node_after = nmap[target_state]
  134. node_before = node_after.parent
  135. vim.command('call s:MundoOpenPreview()')
  136. util._output_preview_text(nodesData.preview_diff(node_before, node_after))
  137. util._goto_window_for_buffer_name('__Mundo__')
  138. def MundoGetTargetState():
  139. """ Get the current undo number that mundo is at. """
  140. util._goto_window_for_buffer_name('__Mundo__')
  141. target_line = vim.eval("getline('.')")
  142. matches = re.match('^.* \[([0-9]+)\] .*$',target_line)
  143. if matches:
  144. return int(matches.group(1))
  145. return 0
  146. def GetNextLine(direction,move_count,write,start="line('.')"):
  147. start_line_no = int(vim.eval(start))
  148. start_line = vim.eval("getline(%d)" % start_line_no)
  149. mundo_verbose_graph = vim.eval('g:mundo_verbose_graph')
  150. if mundo_verbose_graph != "0":
  151. distance = 2
  152. # If we're in between two nodes we move by one less to get back on track.
  153. if start_line.find('[') == -1:
  154. distance = distance - 1
  155. else:
  156. distance = 1
  157. nextline = vim.eval("getline(%d)" % (start_line_no+direction))
  158. idx1 = nextline.find('@')
  159. idx2 = nextline.find('o')
  160. idx3 = nextline.find('w')
  161. # if the next line is not a revision - then go down one more.
  162. if (idx1+idx2+idx3) == -3:
  163. distance = distance + 1
  164. next_line = start_line_no + distance*direction
  165. if move_count > 1:
  166. return GetNextLine(direction,move_count-1,write,str(next_line))
  167. elif write:
  168. newline = vim.eval("getline(%d)" % (next_line))
  169. if newline.find('w ') == -1:
  170. # make sure that if we can't go up/down anymore that we quit out.
  171. if direction < 0 and next_line == 1:
  172. return next_line
  173. if direction > 0 and next_line >= len(vim.current.window.buffer):
  174. return next_line
  175. return GetNextLine(direction,1,write,str(next_line))
  176. return next_line
  177. def MundoMove(direction,move_count=1,relative=True,write=False):
  178. """
  179. Move within the undo graph in the direction specified (or to the specific
  180. undo node specified).
  181. Parameters:
  182. direction - -1/1 (up/down). when 'relative' if False, the undo node to
  183. move to.
  184. move_count - how many times to perform the operation (irrelevent for
  185. relative == False).
  186. relative - whether to move up/down, or to jump to a specific undo node.
  187. write - If True, move to the next written undo.
  188. """
  189. if relative:
  190. target_n = GetNextLine(direction,move_count,write)
  191. else:
  192. updown = 1
  193. if MundoGetTargetState() < direction:
  194. updown = -1
  195. target_n = GetNextLine(updown,abs(MundoGetTargetState()-direction),write)
  196. # Bound the movement to the graph.
  197. help_lines = 3
  198. if int(vim.eval('g:mundo_help')):
  199. help_lines = len(INLINE_HELP.split('\n'))
  200. if target_n <= help_lines:
  201. vim.command("call cursor(%d, 0)" % help_lines)
  202. else:
  203. vim.command("call cursor(%d, 0)" % target_n)
  204. line = vim.eval("getline('.')")
  205. # Move to the node, whether it's an @, o, or w
  206. idx1 = line.find('@ ')
  207. idx2 = line.find('o ')
  208. idx3 = line.find('w ')
  209. idxs = []
  210. if idx1 != -1:
  211. idxs.append(idx1)
  212. if idx2 != -1:
  213. idxs.append(idx2)
  214. if idx3 != -1:
  215. idxs.append(idx3)
  216. minidx = min(idxs)
  217. if idx1 == minidx:
  218. vim.command("call cursor(0, %d + 1)" % idx1)
  219. elif idx2 == minidx:
  220. vim.command("call cursor(0, %d + 1)" % idx2)
  221. else:
  222. vim.command("call cursor(0, %d + 1)" % idx3)
  223. if vim.eval('g:mundo_auto_preview') == '1':
  224. MundoRenderPreview()
  225. def MundoSearch():
  226. search = vim.eval("input('/')");
  227. vim.command('let @/="%s"'% search.replace("\\","\\\\").replace('"','\\"'))
  228. MundoNextMatch()
  229. def MundoPrevMatch():
  230. MundoMatch(-1)
  231. def MundoNextMatch():
  232. MundoMatch(1)
  233. def MundoMatch(down):
  234. """ Jump to the next node that matches the current pattern. If there is a
  235. next node, search from the next node to the end of the list of changes. Stop
  236. on a match. """
  237. if not _check_sanity():
  238. return
  239. # save the current window number (should be the navigation window)
  240. # then generate the undo nodes, and then go back to the current window.
  241. util._goto_window_for_buffer(vim.eval('g:mundo_target_n'))
  242. nodes, nmap = nodesData.make_nodes()
  243. total = len(nodes) - 1
  244. util._goto_window_for_buffer_name('__Mundo__')
  245. curline = int(vim.eval("line('.')"))
  246. mundo_node = MundoGetTargetState()
  247. found_version = -1
  248. if total > 0:
  249. therange = range(mundo_node-1,-1,-1)
  250. if down < 0:
  251. therange = range(mundo_node+1,total+1)
  252. for version in therange:
  253. util._goto_window_for_buffer_name('__Mundo__')
  254. undochanges = nodesData.preview_diff(nmap[version].parent, nmap[version])
  255. # Look thru all of the changes, ignore the first two b/c those are the
  256. # diff timestamp fields (not relevent):
  257. for change in undochanges[3:]:
  258. match_index = vim.eval('match("%s",@/)'% change.replace("\\","\\\\").replace('"','\\"'))
  259. # only consider the matches that are actual additions or
  260. # subtractions
  261. if int(match_index) >= 0 and (change.startswith('-') or change.startswith('+')):
  262. found_version = version
  263. break
  264. # found something, lets get out of here:
  265. if found_version != -1:
  266. break
  267. util._goto_window_for_buffer_name('__Mundo__')
  268. if found_version >= 0:
  269. MundoMove(found_version,1,False)
  270. def MundoRenderPatchdiff():
  271. """ Call MundoRenderChangePreview and display a vert diffpatch with the
  272. current file. """
  273. if MundoRenderChangePreview():
  274. # if there are no lines, do nothing (show a warning).
  275. util._goto_window_for_buffer_name('__Mundo_Preview__')
  276. if vim.current.buffer[:] == ['']:
  277. # restore the cursor position before exiting.
  278. util._goto_window_for_buffer_name('__Mundo__')
  279. vim.command('unsilent echo "No difference between current file and undo number!"')
  280. return False
  281. # quit out of mundo main screen
  282. util._goto_window_for_buffer_name('__Mundo__')
  283. vim.command('quit')
  284. # save the __Mundo_Preview__ buffer to a temp file.
  285. util._goto_window_for_buffer_name('__Mundo_Preview__')
  286. (handle,filename) = tempfile.mkstemp()
  287. vim.command('silent! w %s' % (filename))
  288. # exit the __Mundo_Preview__ window
  289. vim.command('bdelete')
  290. # diff the temp file
  291. vim.command('silent! keepalt vert diffpatch %s' % (filename))
  292. vim.command('set buftype=nofile bufhidden=delete')
  293. return True
  294. return False
  295. def MundoGetChangesForLine():
  296. if not _check_sanity():
  297. return False
  298. target_state = MundoGetTargetState()
  299. # Check that there's an undo state. There may not be if we're talking about
  300. # a buffer with no changes yet.
  301. if target_state == None:
  302. util._goto_window_for_buffer_name('__Mundo__')
  303. return False
  304. else:
  305. target_state = int(target_state)
  306. util._goto_window_for_buffer(vim.eval('g:mundo_target_n'))
  307. nodes, nmap = nodesData.make_nodes()
  308. node_after = nmap[target_state]
  309. node_before = nmap[nodesData.current()]
  310. return nodesData.change_preview_diff(node_before, node_after)
  311. def MundoRenderChangePreview():
  312. """ Render the selected undo level with the current file.
  313. Return True on success, False on failure. """
  314. if not _check_sanity():
  315. return
  316. vim.command('call s:MundoOpenPreview()')
  317. util._output_preview_text(MundoGetChangesForLine())
  318. util._goto_window_for_buffer_name('__Mundo__')
  319. return True
  320. def MundoRenderToggleInlineDiff():
  321. show_inline = int(vim.eval('g:mundo_inline_undo'))
  322. if show_inline == 0:
  323. vim.command("let g:mundo_inline_undo=1")
  324. else:
  325. vim.command("let g:mundo_inline_undo=0")
  326. line = int(vim.eval("line('.')"))
  327. nodesData.clear_oneline_diffs()
  328. MundoRenderGraph(True)
  329. vim.command("call cursor(%d,0)" % line)
  330. def MundoToggleHelp():
  331. show_help = int(vim.eval('g:mundo_help'))
  332. if show_help == 0:
  333. vim.command("let g:mundo_help=1")
  334. else:
  335. vim.command("let g:mundo_help=0")
  336. line = int(vim.eval("line('.')"))
  337. column = int(vim.eval("col('.')"))
  338. old_line_count = int(vim.eval("line('$')"))
  339. MundoRenderGraph(True)
  340. new_line_count = int(vim.eval("line('$')"))
  341. vim.command("call cursor(%d, %d)" % (line + new_line_count - old_line_count, column))
  342. # Mundo undo/redo
  343. def MundoRevert():
  344. if not _check_sanity():
  345. return
  346. target_n = MundoGetTargetState()
  347. back = vim.eval('g:mundo_target_n')
  348. util._goto_window_for_buffer(back)
  349. util._undo_to(target_n)
  350. vim.command('MundoRenderGraph')
  351. if int(vim.eval('g:mundo_return_on_revert')):
  352. util._goto_window_for_buffer(back)
  353. if int(vim.eval('g:mundo_close_on_revert')):
  354. vim.command('MundoToggle')
  355. def MundoPlayTo():
  356. if not _check_sanity():
  357. return
  358. target_n = MundoGetTargetState()
  359. back = int(vim.eval('g:mundo_target_n'))
  360. delay = int(vim.eval('g:mundo_playback_delay'))
  361. vim.command('echo "%s"' % back)
  362. util._goto_window_for_buffer(back)
  363. util.normal('zR')
  364. nodes, nmap = nodesData.make_nodes()
  365. start = nmap[nodesData.current()]
  366. end = nmap[target_n]
  367. def _walk_branch(origin, dest):
  368. rev = origin.n < dest.n
  369. nodes = []
  370. if origin.n > dest.n:
  371. current, final = origin, dest
  372. else:
  373. current, final = dest, origin
  374. while current.n >= final.n:
  375. if current.n == final.n:
  376. break
  377. nodes.append(current)
  378. current = current.parent
  379. else:
  380. return None
  381. nodes.append(current)
  382. if rev:
  383. return reversed(nodes)
  384. else:
  385. return nodes
  386. branch = _walk_branch(start, end)
  387. if not branch:
  388. vim.command('unsilent echo "No path to that node from here!"')
  389. return
  390. for node in branch:
  391. util._undo_to(node.n)
  392. vim.command('MundoRenderGraph')
  393. util.normal('zz')
  394. util._goto_window_for_buffer(back)
  395. vim.command('redraw')
  396. vim.command('sleep %dm' % delay)
  397. def initPythonModule():
  398. if sys.version_info[:2] < (2, 4):
  399. vim.command('let s:has_supported_python = 0')