|
6 | 6 | import sublime
|
7 | 7 | from . import GitTextCommand, GitWindowCommand, git_root
|
8 | 8 | from .status import GitStatusCommand
|
| 9 | +from collections import namedtuple |
| 10 | +from .diff import get_GitDiffRootInView |
9 | 11 |
|
10 | 12 |
|
11 | 13 | class GitAddChoiceCommand(GitStatusCommand):
|
@@ -45,62 +47,105 @@ def rerun(self, result):
|
45 | 47 |
|
46 | 48 |
|
47 | 49 | class GitAddSelectedHunkCommand(GitTextCommand):
|
| 50 | + def is_gitDiffView(self, view): |
| 51 | + return view.name() == "Git Diff" and get_GitDiffRootInView(view) is not None |
| 52 | + |
| 53 | + def is_enabled(self): |
| 54 | + |
| 55 | + view = self.active_view() |
| 56 | + if self.is_gitDiffView(view): |
| 57 | + return True |
| 58 | + |
| 59 | + # First, is this actually a file on the file system? |
| 60 | + return super().is_enabled() |
| 61 | + |
48 | 62 | def run(self, edit, edit_patch=False):
|
49 |
| - self.run_command(['git', 'diff', '--no-color', '-U1', self.get_file_name()], lambda result: self.cull_diff(result, edit_patch)) |
| 63 | + if self.is_gitDiffView(self.view): |
| 64 | + kwargs = {} |
| 65 | + kwargs['working_dir'] = get_GitDiffRootInView(self.view) |
| 66 | + full_diff = self.view.substr(sublime.Region(0,self.view.size())) |
| 67 | + self.cull_diff(full_diff, edit_patch=edit_patch, direct_select=True, **kwargs) |
| 68 | + else: |
| 69 | + self.run_command(['git', 'diff', '--no-color', '-U1', self.get_file_name()], lambda result: self.cull_diff(result, edit_patch)) |
50 | 70 |
|
51 |
| - def cull_diff(self, result, edit_patch=False): |
| 71 | + def cull_diff(self, result, edit_patch=False, direct_select=False, **kwargs): |
52 | 72 | selection = []
|
53 | 73 | for sel in self.view.sel():
|
54 | 74 | selection.append({
|
55 | 75 | "start": self.view.rowcol(sel.begin())[0] + 1,
|
56 | 76 | "end": self.view.rowcol(sel.end())[0] + 1,
|
57 | 77 | })
|
58 | 78 |
|
59 |
| - hunks = [{"diff": ""}] |
60 |
| - i = 0 |
| 79 | + # We devide the diff output into hunk groups. A file header starts a new group. |
| 80 | + # Each group can contain zero or more hunks. |
| 81 | + HunkGroup = namedtuple("HunkGroup", ["fileHeader","hunkList"]) |
| 82 | + section = [] |
| 83 | + hunks = [HunkGroup(section,[])] # Initial lines before hunks |
61 | 84 | matcher = re.compile('^@@ -([0-9]*)(?:,([0-9]*))? \+([0-9]*)(?:,([0-9]*))? @@')
|
62 |
| - for line in result.splitlines(keepends=True): # if different line endings, patch will not apply |
63 |
| - if line.startswith('@@'): |
64 |
| - i += 1 |
| 85 | + for line_num, line in enumerate(result.splitlines(keepends=True)): # if different line endings, patch will not apply |
| 86 | + if line.startswith('diff'): # new file |
| 87 | + section = [] |
| 88 | + hunks.append(HunkGroup(section,[])) |
| 89 | + elif line.startswith('@@'): # new hunk |
65 | 90 | match = matcher.match(line)
|
66 | 91 | start = int(match.group(3))
|
67 | 92 | end = match.group(4)
|
68 | 93 | if end:
|
69 | 94 | end = start + int(end)
|
70 | 95 | else:
|
71 | 96 | end = start
|
72 |
| - hunks.append({"diff": "", "start": start, "end": end}) |
73 |
| - hunks[i]["diff"] += line |
| 97 | + section = [] |
| 98 | + hunks[-1].hunkList.append({"diff": section, "start": start, "end": end, "diff_start": line_num+1, "diff_end": line_num+1}) |
| 99 | + elif hunks[-1].hunkList: # new line for file header or hunk |
| 100 | + hunks[-1].hunkList[-1]["diff_end"] = line_num+1 # update hunk end |
| 101 | + |
| 102 | + section.append(line) |
74 | 103 |
|
75 |
| - diffs = hunks[0]["diff"] |
| 104 | + diffs = "".join(hunks[0][0]) |
76 | 105 | hunks.pop(0)
|
77 | 106 | selection_is_hunky = False
|
78 |
| - for hunk in hunks: |
79 |
| - for sel in selection: |
80 |
| - if sel["end"] < hunk["start"]: |
81 |
| - continue |
82 |
| - if sel["start"] > hunk["end"]: |
83 |
| - continue |
84 |
| - diffs += hunk["diff"] # + "\n\nEND OF HUNK\n\n" |
85 |
| - selection_is_hunky = True |
| 107 | + for file_header, hunkL in hunks: |
| 108 | + file_header = "".join(file_header) |
| 109 | + file_header_added = False |
| 110 | + for hunk in hunkL: |
| 111 | + for sel in selection: |
| 112 | + # In direct mode the selected view lines correspond directly to the lines of the diff file |
| 113 | + # In indirect mode the selected view lines correspond to the lines in the "@@" hunk header |
| 114 | + if direct_select: |
| 115 | + hunk_start = hunk["diff_start"] |
| 116 | + hunk_end = hunk["diff_end"] |
| 117 | + else: |
| 118 | + hunk_start = hunk["start"] |
| 119 | + hunk_end = hunk["end"] |
| 120 | + if sel["end"] < hunk_start: |
| 121 | + continue |
| 122 | + if sel["start"] > hunk_end: |
| 123 | + continue |
| 124 | + # Only print the file header once |
| 125 | + if not file_header_added: |
| 126 | + file_header_added = True |
| 127 | + diffs += file_header |
| 128 | + hunk_str = "".join(hunk["diff"]) |
| 129 | + diffs += hunk_str # + "\n\nEND OF HUNK\n\n" |
| 130 | + selection_is_hunky = True |
86 | 131 |
|
87 | 132 | if selection_is_hunky:
|
88 | 133 | if edit_patch: # open an input panel to modify the patch
|
89 | 134 | patch_view = self.get_window().show_input_panel(
|
90 | 135 | "Message", diffs,
|
91 |
| - lambda edited_patch: self.on_input(edited_patch), None, None |
| 136 | + lambda edited_patch: self.on_input(edited_patch,**kwargs), None, None |
92 | 137 | )
|
93 | 138 | s = sublime.load_settings("Git.sublime-settings")
|
94 | 139 | syntax = s.get("diff_syntax", "Packages/Diff/Diff.tmLanguage")
|
95 | 140 | patch_view.set_syntax_file(syntax)
|
96 | 141 | patch_view.settings().set('word_wrap', False)
|
97 | 142 | else:
|
98 |
| - self.on_input(diffs) |
| 143 | + self.on_input(diffs,**kwargs) |
99 | 144 | else:
|
100 | 145 | sublime.status_message("No selected hunk")
|
101 | 146 |
|
102 |
| - def on_input(self, patch): |
103 |
| - self.run_command(['git', 'apply', '--cached'], stdin=patch) |
| 147 | + def on_input(self, patch, **kwargs): |
| 148 | + self.run_command(['git', 'apply', '--cached'], stdin=patch, **kwargs) |
104 | 149 |
|
105 | 150 | # Also, sometimes we want to undo adds
|
106 | 151 |
|
|
0 commit comments