Configuration options for gitv plugin.
[profile.git] / .vim / autoload / gundo.py
1 # ============================================================================
2 # File:        gundo.py
3 # Description: vim global plugin to visualize your undo tree
4 # Maintainer:  Steve Losh <steve@stevelosh.com>
5 # License:     GPLv2+ -- look it up.
6 # Notes:       Much of this code was thiefed from Mercurial, and the rest was
7 #              heavily inspired by scratch.vim and histwin.vim.
8 #
9 # ============================================================================
10
11 import difflib
12 import itertools
13 import sys
14 import time
15 import vim
16
17
18 # Mercurial's graphlog code --------------------------------------------------------
19 def asciiedges(seen, rev, parents):
20     """adds edge info to changelog DAG walk suitable for ascii()"""
21     if rev not in seen:
22         seen.append(rev)
23     nodeidx = seen.index(rev)
24
25     knownparents = []
26     newparents = []
27     for parent in parents:
28         if parent in seen:
29             knownparents.append(parent)
30         else:
31             newparents.append(parent)
32
33     ncols = len(seen)
34     seen[nodeidx:nodeidx + 1] = newparents
35     edges = [(nodeidx, seen.index(p)) for p in knownparents]
36
37     if len(newparents) > 0:
38         edges.append((nodeidx, nodeidx))
39     if len(newparents) > 1:
40         edges.append((nodeidx, nodeidx + 1))
41
42     nmorecols = len(seen) - ncols
43     return nodeidx, edges, ncols, nmorecols
44
45 def get_nodeline_edges_tail(
46         node_index, p_node_index, n_columns, n_columns_diff, p_diff, fix_tail):
47     if fix_tail and n_columns_diff == p_diff and n_columns_diff != 0:
48         # Still going in the same non-vertical direction.
49         if n_columns_diff == -1:
50             start = max(node_index + 1, p_node_index)
51             tail = ["|", " "] * (start - node_index - 1)
52             tail.extend(["/", " "] * (n_columns - start))
53             return tail
54         else:
55             return ["\\", " "] * (n_columns - node_index - 1)
56     else:
57         return ["|", " "] * (n_columns - node_index - 1)
58
59 def draw_edges(edges, nodeline, interline):
60     for (start, end) in edges:
61         if start == end + 1:
62             interline[2 * end + 1] = "/"
63         elif start == end - 1:
64             interline[2 * start + 1] = "\\"
65         elif start == end:
66             interline[2 * start] = "|"
67         else:
68             nodeline[2 * end] = "+"
69             if start > end:
70                 (start, end) = (end, start)
71             for i in range(2 * start + 1, 2 * end):
72                 if nodeline[i] != "+":
73                     nodeline[i] = "-"
74
75 def fix_long_right_edges(edges):
76     for (i, (start, end)) in enumerate(edges):
77         if end > start:
78             edges[i] = (start, end + 1)
79
80 def ascii(buf, state, type, char, text, coldata):
81     """prints an ASCII graph of the DAG
82
83     takes the following arguments (one call per node in the graph):
84
85       - Somewhere to keep the needed state in (init to asciistate())
86       - Column of the current node in the set of ongoing edges.
87       - Type indicator of node data == ASCIIDATA.
88       - Payload: (char, lines):
89         - Character to use as node's symbol.
90         - List of lines to display as the node's text.
91       - Edges; a list of (col, next_col) indicating the edges between
92         the current node and its parents.
93       - Number of columns (ongoing edges) in the current revision.
94       - The difference between the number of columns (ongoing edges)
95         in the next revision and the number of columns (ongoing edges)
96         in the current revision. That is: -1 means one column removed;
97         0 means no columns added or removed; 1 means one column added.
98     """
99
100     idx, edges, ncols, coldiff = coldata
101     assert -2 < coldiff < 2
102     if coldiff == -1:
103         # Transform
104         #
105         #     | | |        | | |
106         #     o | |  into  o---+
107         #     |X /         |/ /
108         #     | |          | |
109         fix_long_right_edges(edges)
110
111     # add_padding_line says whether to rewrite
112     #
113     #     | | | |        | | | |
114     #     | o---+  into  | o---+
115     #     |  / /         |   | |  # <--- padding line
116     #     o | |          |  / /
117     #                    o | |
118     add_padding_line = (len(text) > 2 and coldiff == -1 and
119                         [x for (x, y) in edges if x + 1 < y])
120
121     # fix_nodeline_tail says whether to rewrite
122     #
123     #     | | o | |        | | o | |
124     #     | | |/ /         | | |/ /
125     #     | o | |    into  | o / /   # <--- fixed nodeline tail
126     #     | |/ /           | |/ /
127     #     o | |            o | |
128     fix_nodeline_tail = len(text) <= 2 and not add_padding_line
129
130     # nodeline is the line containing the node character (typically o)
131     nodeline = ["|", " "] * idx
132     nodeline.extend([char, " "])
133
134     nodeline.extend(
135         get_nodeline_edges_tail(idx, state[1], ncols, coldiff,
136                                 state[0], fix_nodeline_tail))
137
138     # shift_interline is the line containing the non-vertical
139     # edges between this entry and the next
140     shift_interline = ["|", " "] * idx
141     if coldiff == -1:
142         n_spaces = 1
143         edge_ch = "/"
144     elif coldiff == 0:
145         n_spaces = 2
146         edge_ch = "|"
147     else:
148         n_spaces = 3
149         edge_ch = "\\"
150     shift_interline.extend(n_spaces * [" "])
151     shift_interline.extend([edge_ch, " "] * (ncols - idx - 1))
152
153     # draw edges from the current node to its parents
154     draw_edges(edges, nodeline, shift_interline)
155
156     # lines is the list of all graph lines to print
157     lines = [nodeline]
158     if add_padding_line:
159         lines.append(get_padding_line(idx, ncols, edges))
160     lines.append(shift_interline)
161
162     # make sure that there are as many graph lines as there are
163     # log strings
164     while len(text) < len(lines):
165         text.append("")
166     if len(lines) < len(text):
167         extra_interline = ["|", " "] * (ncols + coldiff)
168         while len(lines) < len(text):
169             lines.append(extra_interline)
170
171     # print lines
172     indentation_level = max(ncols, ncols + coldiff)
173     for (line, logstr) in zip(lines, text):
174         ln = "%-*s %s" % (2 * indentation_level, "".join(line), logstr)
175         buf.write(ln.rstrip() + '\n')
176
177     # ... and start over
178     state[0] = coldiff
179     state[1] = idx
180
181 def generate(dag, edgefn, current):
182     seen, state = [], [0, 0]
183     buf = Buffer()
184     for node, parents in list(dag):
185         if node.time:
186             age_label = age(int(node.time))
187         else:
188             age_label = 'Original'
189         line = '[%s] %s' % (node.n, age_label)
190         if node.n == current:
191             char = '@'
192         else:
193             char = 'o'
194         ascii(buf, state, 'C', char, [line], edgefn(seen, node, parents))
195     return buf.b
196
197
198 # Mercurial age function -----------------------------------------------------------
199 agescales = [("year", 3600 * 24 * 365),
200              ("month", 3600 * 24 * 30),
201              ("week", 3600 * 24 * 7),
202              ("day", 3600 * 24),
203              ("hour", 3600),
204              ("minute", 60),
205              ("second", 1)]
206
207 def age(ts):
208     '''turn a timestamp into an age string.'''
209
210     def plural(t, c):
211         if c == 1:
212             return t
213         return t + "s"
214     def fmt(t, c):
215         return "%d %s" % (c, plural(t, c))
216
217     now = time.time()
218     then = ts
219     if then > now:
220         return 'in the future'
221
222     delta = max(1, int(now - then))
223     if delta > agescales[0][1] * 2:
224         return time.strftime('%Y-%m-%d', time.gmtime(float(ts)))
225
226     for t, s in agescales:
227         n = delta // s
228         if n >= 2 or s == 1:
229             return '%s ago' % fmt(t, n)
230
231
232 # Python Vim utility functions -----------------------------------------------------
233 normal = lambda s: vim.command('normal %s' % s)
234
235 MISSING_BUFFER = "Cannot find Gundo's target buffer (%s)"
236 MISSING_WINDOW = "Cannot find window (%s) for Gundo's target buffer (%s)"
237
238 def _check_sanity():
239     '''Check to make sure we're not crazy.
240
241     Does the following things:
242
243         * Make sure the target buffer still exists.
244     '''
245     b = int(vim.eval('g:gundo_target_n'))
246
247     if not vim.eval('bufloaded(%d)' % b):
248         vim.command('echo "%s"' % (MISSING_BUFFER % b))
249         return False
250
251     w = int(vim.eval('bufwinnr(%d)' % b))
252     if w == -1:
253         vim.command('echo "%s"' % (MISSING_WINDOW % (w, b)))
254         return False
255
256     return True
257
258 def _goto_window_for_buffer(b):
259     w = int(vim.eval('bufwinnr(%d)' % int(b)))
260     vim.command('%dwincmd w' % w)
261
262 def _goto_window_for_buffer_name(bn):
263     b = vim.eval('bufnr("%s")' % bn)
264     return _goto_window_for_buffer(b)
265
266 def _undo_to(n):
267     n = int(n)
268     if n == 0:
269         vim.command('silent earlier %s' % (int(vim.eval('&undolevels')) + 1))
270     else:
271         vim.command('silent undo %d' % n)
272
273
274 INLINE_HELP = '''\
275 " Gundo for %s (%d)
276 " j/k  - move between undo states
277 " p    - preview diff of selected and current states
278 " <cr> - revert to selected state
279
280 '''
281
282
283 # Python undo tree data structures and functions -----------------------------------
284 class Buffer(object):
285     def __init__(self):
286         self.b = ''
287
288     def write(self, s):
289         self.b += s
290
291 class Node(object):
292     def __init__(self, n, parent, time, curhead):
293         self.n = int(n)
294         self.parent = parent
295         self.children = []
296         self.curhead = curhead
297         self.time = time
298
299 def _make_nodes(alts, nodes, parent=None):
300     p = parent
301
302     for alt in alts:
303         curhead = 'curhead' in alt
304         node = Node(n=alt['seq'], parent=p, time=alt['time'], curhead=curhead)
305         nodes.append(node)
306         if alt.get('alt'):
307             _make_nodes(alt['alt'], nodes, p)
308         p = node
309
310 def make_nodes():
311     ut = vim.eval('undotree()')
312     entries = ut['entries']
313
314     root = Node(0, None, False, 0)
315     nodes = []
316     _make_nodes(entries, nodes, root)
317     nodes.append(root)
318     nmap = dict((node.n, node) for node in nodes)
319     return nodes, nmap
320
321 def changenr(nodes):
322     _curhead_l = list(itertools.dropwhile(lambda n: not n.curhead, nodes))
323     if _curhead_l:
324         current = _curhead_l[0].parent.n
325     else:
326         current = int(vim.eval('changenr()'))
327     return current
328
329
330 # Gundo rendering ------------------------------------------------------------------
331
332 # Rendering utility functions
333 def _fmt_time(t):
334     return time.strftime('%Y-%m-%d %I:%M:%S %p', time.localtime(float(t)))
335
336 def _output_preview_text(lines):
337     _goto_window_for_buffer_name('__Gundo_Preview__')
338     vim.command('setlocal modifiable')
339     vim.current.buffer[:] = lines
340     vim.command('setlocal nomodifiable')
341
342 def _generate_preview_diff(current, node_before, node_after):
343     _goto_window_for_buffer(vim.eval('g:gundo_target_n'))
344
345     if not node_after.n:    # we're at the original file
346         before_lines = []
347
348         _undo_to(0)
349         after_lines = vim.current.buffer[:]
350
351         before_name = 'n/a'
352         before_time = ''
353         after_name = 'Original'
354         after_time = ''
355     elif not node_before.n: # we're at a pseudo-root state
356         _undo_to(0)
357         before_lines = vim.current.buffer[:]
358
359         _undo_to(node_after.n)
360         after_lines = vim.current.buffer[:]
361
362         before_name = 'Original'
363         before_time = ''
364         after_name = node_after.n
365         after_time = _fmt_time(node_after.time)
366     else:
367         _undo_to(node_before.n)
368         before_lines = vim.current.buffer[:]
369
370         _undo_to(node_after.n)
371         after_lines = vim.current.buffer[:]
372
373         before_name = node_before.n
374         before_time = _fmt_time(node_before.time)
375         after_name = node_after.n
376         after_time = _fmt_time(node_after.time)
377
378     _undo_to(current)
379
380     return list(difflib.unified_diff(before_lines, after_lines,
381                                      before_name, after_name,
382                                      before_time, after_time))
383
384 def _generate_change_preview_diff(current, node_before, node_after):
385     _goto_window_for_buffer(vim.eval('g:gundo_target_n'))
386
387     _undo_to(node_before.n)
388     before_lines = vim.current.buffer[:]
389
390     _undo_to(node_after.n)
391     after_lines = vim.current.buffer[:]
392
393     before_name = node_before.n or 'Original'
394     before_time = node_before.time and _fmt_time(node_before.time) or ''
395     after_name = node_after.n or 'Original'
396     after_time = node_after.time and _fmt_time(node_after.time) or ''
397
398     _undo_to(current)
399
400     return list(difflib.unified_diff(before_lines, after_lines,
401                                      before_name, after_name,
402                                      before_time, after_time))
403
404 def GundoRenderGraph():
405     if not _check_sanity():
406         return
407
408     nodes, nmap = make_nodes()
409
410     for node in nodes:
411         node.children = [n for n in nodes if n.parent == node]
412
413     def walk_nodes(nodes):
414         for node in nodes:
415             if node.parent:
416                 yield (node, [node.parent])
417             else:
418                 yield (node, [])
419
420     dag = sorted(nodes, key=lambda n: int(n.n), reverse=True)
421     current = changenr(nodes)
422
423     result = generate(walk_nodes(dag), asciiedges, current).rstrip().splitlines()
424     result = [' ' + l for l in result]
425
426     target = (vim.eval('g:gundo_target_f'), int(vim.eval('g:gundo_target_n')))
427
428     if int(vim.eval('g:gundo_help')):
429         header = (INLINE_HELP % target).splitlines()
430     else:
431         header = []
432
433     vim.command('call s:GundoOpenGraph()')
434     vim.command('setlocal modifiable')
435     vim.current.buffer[:] = (header + result)
436     vim.command('setlocal nomodifiable')
437
438     i = 1
439     for line in result:
440         try:
441             line.split('[')[0].index('@')
442             i += 1
443             break
444         except ValueError:
445             pass
446         i += 1
447     vim.command('%d' % (i+len(header)-1))
448
449 def GundoRenderPreview():
450     if not _check_sanity():
451         return
452
453     target_state = vim.eval('s:GundoGetTargetState()')
454
455     # Check that there's an undo state. There may not be if we're talking about
456     # a buffer with no changes yet.
457     if target_state == None:
458         _goto_window_for_buffer_name('__Gundo__')
459         return
460     else:
461         target_state = int(target_state)
462
463     _goto_window_for_buffer(vim.eval('g:gundo_target_n'))
464
465     nodes, nmap = make_nodes()
466     current = changenr(nodes)
467
468     node_after = nmap[target_state]
469     node_before = node_after.parent
470
471     vim.command('call s:GundoOpenPreview()')
472     _output_preview_text(_generate_preview_diff(current, node_before, node_after))
473
474     _goto_window_for_buffer_name('__Gundo__')
475
476 def GundoRenderChangePreview():
477     if not _check_sanity():
478         return
479
480     target_state = vim.eval('s:GundoGetTargetState()')
481
482     # Check that there's an undo state. There may not be if we're talking about
483     # a buffer with no changes yet.
484     if target_state == None:
485         _goto_window_for_buffer_name('__Gundo__')
486         return
487     else:
488         target_state = int(target_state)
489
490     _goto_window_for_buffer(vim.eval('g:gundo_target_n'))
491
492     nodes, nmap = make_nodes()
493     current = changenr(nodes)
494
495     node_after = nmap[target_state]
496     node_before = nmap[current]
497
498     vim.command('call s:GundoOpenPreview()')
499     _output_preview_text(_generate_change_preview_diff(current, node_before, node_after))
500
501     _goto_window_for_buffer_name('__Gundo__')
502
503
504 # Gundo undo/redo
505 def GundoRevert():
506     if not _check_sanity():
507         return
508
509     target_n = int(vim.eval('s:GundoGetTargetState()'))
510     back = vim.eval('g:gundo_target_n')
511
512     _goto_window_for_buffer(back)
513     _undo_to(target_n)
514
515     vim.command('GundoRenderGraph')
516     _goto_window_for_buffer(back)
517
518     if int(vim.eval('g:gundo_close_on_revert')):
519         vim.command('GundoToggle')
520
521 def GundoPlayTo():
522     if not _check_sanity():
523         return
524
525     target_n = int(vim.eval('s:GundoGetTargetState()'))
526     back = int(vim.eval('g:gundo_target_n'))
527
528     vim.command('echo "%s"' % back)
529
530     _goto_window_for_buffer(back)
531     normal('zR')
532
533     nodes, nmap = make_nodes()
534
535     start = nmap[changenr(nodes)]
536     end = nmap[target_n]
537
538     def _walk_branch(origin, dest):
539         rev = origin.n < dest.n
540
541         nodes = []
542         if origin.n > dest.n:
543             current, final = origin, dest
544         else:
545             current, final = dest, origin
546
547         while current.n >= final.n:
548             if current.n == final.n:
549                 break
550             nodes.append(current)
551             current = current.parent
552         else:
553             return None
554         nodes.append(current)
555
556         if rev:
557             return reversed(nodes)
558         else:
559             return nodes
560
561     branch = _walk_branch(start, end)
562
563     if not branch:
564         vim.command('unsilent echo "No path to that node from here!"')
565         return
566
567     for node in branch:
568         _undo_to(node.n)
569         vim.command('GundoRenderGraph')
570         normal('zz')
571         _goto_window_for_buffer(back)
572         vim.command('redraw')
573         vim.command('sleep 60m')
574
575 def initPythonModule():
576     if sys.version_info[:2] < (2, 4):
577         vim.command('let s:has_supported_python = 0')