131 lines
5.6 KiB
Python
131 lines
5.6 KiB
Python
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)
|