forked from kemayo/sublime-text-git
-
Notifications
You must be signed in to change notification settings - Fork 0
/
annotate.py
130 lines (114 loc) · 5.55 KB
/
annotate.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
import tempfile
import re
import os
import sublime
import sublime_plugin
from git import git_root, GitTextCommand
class GitClearAnnotationCommand(GitTextCommand):
def run(self, view):
self.active_view().settings().set('live_git_annotations', False)
self.view.erase_regions('git.changes.x')
self.view.erase_regions('git.changes.+')
self.view.erase_regions('git.changes.-')
class GitToggleAnnotationsCommand(GitTextCommand):
def run(self, view):
if self.active_view().settings().get('live_git_annotations'):
self.view.run_command('git_clear_annotation')
else:
self.view.run_command('git_annotate')
class GitAnnotationListener(sublime_plugin.EventListener):
def on_modified(self, view):
if not view.settings().get('live_git_annotations'):
return
view.run_command('git_annotate')
def on_load(self, view):
s = sublime.load_settings("Git.sublime-settings")
if s.get('annotations'):
view.run_command('git_annotate')
class GitAnnotateCommand(GitTextCommand):
# Unfortunately, git diff does not support text from stdin, making a *live*
# annotation difficult. Therefore I had to resort to the system diff
# command.
# This works as follows:
# 1. When the command is run for the first time for this file, a temporary
# file with the current state of the HEAD is being pulled from git.
# 2. All consecutive runs will pass the current buffer into diffs stdin.
# The resulting output is then parsed and regions are set accordingly.
def run(self, view):
# If the annotations are already running, we dont have to create a new
# tmpfile
if hasattr(self, "tmp"):
self.compare_tmp(None)
return
self.tmp = tempfile.NamedTemporaryFile()
self.active_view().settings().set('live_git_annotations', True)
root = git_root(self.get_working_dir())
repo_file = os.path.relpath(self.view.file_name(), root)
self.run_command(['git', 'show', 'HEAD:{0}'.format(repo_file)], show_status=False, no_save=True, callback=self.compare_tmp, stdout=self.tmp)
def compare_tmp(self, result, stdout=None):
all_text = self.view.substr(sublime.Region(0, self.view.size())).encode("utf-8")
self.run_command(['diff', '-u', self.tmp.name, '-'], stdin=all_text, no_save=True, show_status=False, callback=self.parse_diff)
# This is where the magic happens. At the moment, only one chunk format is supported. While
# the unified diff format theoritaclly supports more, I don't think git diff creates them.
def parse_diff(self, result, stdin=None):
lines = result.splitlines()
matcher = re.compile('^@@ -([0-9]*),([0-9]*) \+([0-9]*),([0-9]*) @@')
diff = []
for line_index in range(0, len(lines)):
line = lines[line_index]
if not line.startswith('@'):
continue
match = matcher.match(line)
if not match:
continue
line_before, len_before, line_after, len_after = [int(match.group(x)) for x in [1, 2, 3, 4]]
chunk_index = line_index + 1
tracked_line_index = line_after - 1
deletion = False
insertion = False
while True:
line = lines[chunk_index]
if line.startswith('@'):
break
elif line.startswith('-'):
if not line.strip() == '-':
deletion = True
tracked_line_index -= 1
elif line.startswith('+'):
if deletion and not line.strip() == '+':
diff.append(['x', tracked_line_index])
insertion = True
elif not deletion:
insertion = True
diff.append(['+', tracked_line_index])
else:
if not insertion and deletion:
diff.append(['-', tracked_line_index])
insertion = deletion = False
tracked_line_index += 1
chunk_index += 1
if chunk_index >= len(lines):
break
self.annotate(diff)
# Once we got all lines with their specific change types (either x, +, or - for
# modified, added, or removed) we can create our regions and do the actual annotation.
def annotate(self, diff):
self.view.erase_regions('git.changes.x')
self.view.erase_regions('git.changes.+')
self.view.erase_regions('git.changes.-')
typed_diff = {'x': [], '+': [], '-': []}
for change_type, line in diff:
if change_type == '-':
full_region = self.view.full_line(self.view.text_point(line - 1, 0))
position = full_region.begin()
for i in xrange(full_region.size()):
typed_diff[change_type].append(sublime.Region(position + i))
else:
point = self.view.text_point(line, 0)
region = self.view.full_line(point)
if change_type == '-':
region = sublime.Region(point, point + 5)
typed_diff[change_type].append(region)
for change in ['x', '+']:
self.view.add_regions("git.changes.{0}".format(change), typed_diff[change], 'git.changes.{0}'.format(change), 'dot', sublime.HIDDEN)
self.view.add_regions("git.changes.-", typed_diff['-'], 'git.changes.-', 'dot', sublime.DRAW_EMPTY_AS_OVERWRITE)