Files
ChocolateyPackages/EthanBrown.SublimeText2.GitPackages/tools/PackageCache/Git/annotate.py
2013-04-04 08:55:33 -04:00

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)