forked from CJTozer/SublimeDiffView
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathDiffView.py
439 lines (366 loc) · 16.4 KB
/
DiffView.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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
import sublime
import sublime_plugin
import os
import codecs
import threading
import time
import tempfile
from .util.view_finder import ViewFinder
from .util.constants import Constants
from .util.vcs import NoVCSError
from .parser.diff_parser import DiffParser
class DiffView(sublime_plugin.WindowCommand):
"""Main Sublime command for running a diff.
Asks for input for what to diff against; a Git SHA/branch/tag.
"""
diff_args = ''
def _prepare(self):
"""Some preparation common to all subclasses."""
self.window.last_diff = self
self.last_hunk_index = 0
self.settings = sublime.load_settings('DiffView.sublime-settings')
self.debug = self.settings.get("debug", False)
if self.debug:
print("DiffView running in debug mode")
self.view_style = self.settings.get("view_style", "quick_panel")
self.collapse_diff_list = (
self.settings.get("collapse_diff_list", False) and self.view_style == "persistent_list")
self.styles = {
"ADD": self.settings.get("add_highlight_style", "support.class"),
"MOD": self.settings.get("mod_highlight_style", "string"),
"DEL": self.settings.get("del_highlight_style", "invalid"),
"LIST_SEL": self.settings.get("list_sel_highlight_style", "comment")}
# Set up the groups
self.list_group = 0
if self.view_style == "quick_panel":
self.diff_layout = {
"cols": [0.0, 0.5, 1.0],
"rows": [0.0, 1.0],
"cells": [
[0, 0, 1, 1],
[1, 0, 2, 1]]}
self.lhs_group = 0
self.rhs_group = 1
elif self.view_style == "persistent_list":
self.diff_layout = {
"cols": [0.0, 0.5, 1.0],
"rows": [0.0, 0.25, 1.0],
"cells": [
[0, 0, 2, 1],
[0, 1, 1, 2],
[1, 1, 2, 2]]}
self.lhs_group = 1
self.rhs_group = 2
else:
sublime.error_message("Invalid value '{}'' for 'view_style'".format(self.view_style))
raise ValueError("Invalid 'view_style': '{}'".format(self.view_style))
def run(self, diff_args=None, cwd=None):
"""Runs the diff.
Starts by conditionanlly asking for diff arguments in an input panel (see arguments).
Args:
diff_args: [optional] the arguments to the diff. If not present, an input panel will ask for them instead
cwd: [optional] the [c]urrent [w]orking [d]irectory to open the diff in. If not present, this will default to the cwd of the currently open file
"""
self._prepare()
# Use show_input_panel as show_quick_panel doesn't allow arbitrary data
if diff_args:
# if args passed directly (e.g., from another plugin), don't ask for input panel
self.do_diff(diff_args, cwd)
else:
self.window.show_input_panel(
"Diff arguments?",
self.diff_args,
self.do_diff,
None,
None)
def do_diff(self, diff_args, cwd=None):
"""Run a diff and display the changes.
Args:
diff_args: the arguments to the diff.
cwd: [optional] the [c]urrent [w]orking [d]irectory to open the diff in. If not present, this will default to the cwd of the currently open file
"""
self.diff_args = diff_args
try:
# Create the diff parser
if not cwd:
cwd = os.path.dirname(self.window.active_view().file_name())
self.parser = DiffParser(
self.diff_args,
cwd,
debug=self.debug,
get_diff_headers=self.collapse_diff_list)
except NoVCSError:
# No changes; say so
sublime.message_dialog("This file does not appear to be under version control (Git, SVN or Bazaar).")
return
if not self.parser.changed_hunks:
# No changes; say so
sublime.message_dialog("No changes to report...")
else:
# Show the list of changed hunks
self.list_changed_hunks()
def list_changed_hunks(self):
"""Show a list of changed hunks in a quick panel."""
# Record the starting view and position.
self.orig_view = self.window.active_view()
self.orig_pos = self.orig_view.sel()[0]
self.orig_viewport = self.orig_view.viewport_position()
# Store old layout, then set layout to 2 columns.
self.orig_layout = self.window.layout()
self.window.set_layout(self.diff_layout)
if self.view_style == "quick_panel":
# Start listening for the quick panel creation, then create it.
ViewFinder.instance().start_listen(self.quick_panel_found)
self.window.show_quick_panel(
[h.description for h in self.parser.changed_hunks],
self.show_hunk_diff,
sublime.MONOSPACE_FONT | sublime.KEEP_OPEN_ON_FOCUS_LOST,
self.last_hunk_index,
self.preview_hunk)
else:
# Put the hunks list in the top panel
self.changes_list_file = tempfile.mkstemp()[1]
with codecs.open(self.changes_list_file, 'w', 'utf-8') as f:
def get_prefix(hunk):
# Prefix with indent if not a header and using headers
if self.collapse_diff_list and not hasattr(hunk, 'n_changes'):
return " "
return ""
changes_list = " \n".join(
[get_prefix(h) + h.oneline_description for h in self.parser.changed_hunks])
f.write(changes_list + " ")
self.changes_list_view = self.window.open_file(
self.changes_list_file,
flags=sublime.TRANSIENT |
sublime.FORCE_GROUP,
group=self.list_group)
self.changes_list_view.set_read_only(True)
self.changes_list_view.set_scratch(True)
# Move cursor to the last selected diff when the file is ready
def select_latest_diff_when_ready(view):
while view.is_loading():
time.sleep(0.1)
# Force a change in position - so the selection change event always triggers.
view.sel().clear()
view.sel().add(sublime.Region(1, 1))
view.run_command(
"show_diff_list",
args={'last_selected': self.last_hunk_index,
'style': self.styles['LIST_SEL']})
# Add folding regions if configured
if self.collapse_diff_list:
# Arrange the changes into per-file collapsing regions
n_hunks = len(self.parser.changed_hunks)
line = 0
regions = []
while line < n_hunks:
header = self.parser.changed_hunks[line]
file_hunks = header.n_changes
fold_region = sublime.Region(
self.changes_list_view.text_point(line + 1, 0) - 1,
self.changes_list_view.text_point(line + file_hunks + 1, 0) - 1)
regions.append(fold_region)
# For simplicity, give each hunk in the file a reference to the region.
next_header = line + file_hunks + 1
while line < next_header:
self.parser.changed_hunks[line].fold_region = fold_region
line += 1
# Add the regions to the view and fold them by default
self.changes_list_view.add_regions(
Constants.ADD_REGION_KEY,
regions,
"string",
flags=sublime.HIDDEN)
self.changes_list_view.fold(regions)
# Listen for changes to this view's selection.
DiffViewEventListner.instance().start_listen(
self.preview_hunk,
self.changes_list_view,
self)
# Choose the last selected change, when the view's ready
threading.Thread(target=select_latest_diff_when_ready, args=(self.changes_list_view,)).start()
def show_hunk_diff(self, hunk_index):
"""Open the location of the selected hunk.
Removes any diff highlighting shown in the previews.
Args:
hunk_index: the selected index in the changed hunks list.
"""
# Remove diff highlighting from all views.
for view in self.window.views():
view.erase_regions(Constants.ADD_REGION_KEY)
view.erase_regions(Constants.MOD_REGION_KEY)
view.erase_regions(Constants.DEL_REGION_KEY)
if hunk_index == -1:
self.reset_window()
return
# Reset the layout.
self.window.set_layout(self.orig_layout)
if self.view_style == "persistent_list":
self.changes_list_view.close()
self.last_hunk_index = hunk_index
hunk = self.parser.changed_hunks[hunk_index]
(_, new_filespec) = hunk.filespecs()
self.window.open_file(new_filespec, sublime.ENCODED_POSITION)
def preview_hunk(self, hunk_index):
"""Show a preview of the selected hunk.
Args:
hunk_index: the selected index in the changed hunks list.
"""
hunk = self.parser.changed_hunks[hunk_index]
(old_filespec, new_filespec) = hunk.filespecs()
def highlight_when_ready(view, highlight_fn):
while view.is_loading():
time.sleep(0.1)
highlight_fn(view, self.styles)
def open_preview(filespec, group, highlight_fn):
view = self.window.open_file(
filespec,
flags=sublime.TRANSIENT |
sublime.ENCODED_POSITION |
sublime.FORCE_GROUP,
group=group)
threading.Thread(target=highlight_when_ready, args=(view, highlight_fn)).start()
return view
self.right_view = open_preview(new_filespec, self.rhs_group, hunk.file_diff.add_new_regions)
self.left_view = open_preview(old_filespec, self.lhs_group, hunk.file_diff.add_old_regions)
self.window.focus_group(0)
if self.view_style == "quick_panel":
# Keep the focus in the quick panel
self.window.focus_view(self.qpanel)
def list_toggle_fold(self, hunk_index):
hunk = self.parser.changed_hunks[hunk_index]
fold_region = hunk.fold_region
# fold() returns False if already folded
if not self.changes_list_view.fold(fold_region):
# Unfold
self.changes_list_view.unfold(fold_region)
def reset_window(self):
"""Reset the window to its original state."""
if self.view_style == "persistent_list":
self.changes_list_view.close()
self.window.set_layout(self.orig_layout)
self.window.focus_view(self.orig_view)
self.orig_view.sel().clear()
self.orig_view.sel().add(self.orig_pos)
self.orig_view.set_viewport_position(self.orig_viewport, animate=False)
# Stop listening for events
ViewFinder.instance().stop()
DiffViewEventListner.instance().stop()
self.qpanel = None
def quick_panel_found(self, view):
"""Callback to store the quick panel when found.
Args:
view: The quick panel view.
"""
self.qpanel = view
class DiffHunksList(sublime_plugin.WindowCommand):
def run(self):
"""Resume the previous diff.
Displays the list of changed hunks starting from the last hunk viewed.
"""
if hasattr(self.window, 'last_diff'):
self.window.last_diff.list_changed_hunks()
class DiffCancel(sublime_plugin.WindowCommand):
def run(self):
"""Cancel the current diff."""
if hasattr(self.window, 'last_diff'):
self.window.last_diff.reset_window()
class DiffShowSelected(sublime_plugin.WindowCommand):
def run(self):
"""Show the change that's curently selected by this view."""
if hasattr(self.window, 'last_diff'):
self.window.last_diff.show_hunk_diff(DiffViewEventListner.instance().current_row)
class DiffListToggleFoldCommand(sublime_plugin.WindowCommand):
def run(self):
"""Toggle the folding of the current file's hunk list."""
if hasattr(self.window, 'last_diff') and self.window.last_diff.collapse_diff_list:
self.window.last_diff.list_toggle_fold(
DiffViewEventListner.instance().current_row)
class DiffViewUncommitted(DiffView):
"""Command to display a simple diff of uncommitted changes."""
def run(self):
self._prepare()
self.do_diff('')
class ShowDiffListCommand(sublime_plugin.TextCommand):
"""Command to show the diff list."""
def run(self, edit, last_selected, style):
"""Entry point for running the command.
Args:
edit: The edit for this `TextCommand`.
last_selected: The last selected change (zero indexed).
style: The style to use for the selected line.
"""
# Move cursor to the last selected diff
self.view.sel().clear()
pos = self.view.text_point(last_selected, 0)
self.view.sel().add(sublime.Region(pos, pos))
# Highlight the selected line
end_pos = self.view.text_point(last_selected + 1, 0)
self.view.add_regions(
Constants.SELECTED_CHANGE_KEY,
[sublime.Region(pos, end_pos)],
style,
flags=Constants.SELECTED_CHANGE_FLAGS)
class DiffViewEventListner(sublime_plugin.EventListener):
"""Helper class for catching events during a diff."""
_instance = None
def __init__(self):
"""Constructor."""
self.__class__._instance = self
self._listening = False
self.current_row = -1
def on_selection_modified_async(self, view):
"""Called when a selection has been modified.
Only interested if this is the change list view, and we're now on a different line.
Args:
view: The view that's changed selection.
"""
if self._listening and view == self.view:
current_selection = view.sel()[0]
(new_row, _) = view.rowcol(current_selection.a)
if new_row != self.current_row:
self.current_row = new_row
# rowcol is zero indexed, so line 1 gives index zero - perfect
self.diff.preview_hunk(self.current_row)
# Highlight the selected line
view.erase_regions(Constants.SELECTED_CHANGE_KEY)
selected_line_region = sublime.Region(
self.view.text_point(self.current_row, 0),
self.view.text_point(self.current_row + 1, 0))
view.add_regions(
Constants.SELECTED_CHANGE_KEY,
[selected_line_region],
self.diff.styles["LIST_SEL"],
flags=Constants.SELECTED_CHANGE_FLAGS)
def on_query_context(self, view, key, operator, operand, match_all):
"""Context queries mean someone is trying to work out whether to override key bindings.
The bindings that are overridden are as follows:
- escape -> "cancel_diff" when a diff is running
- enter -> "show_hunk" when in the changes list view
"""
if key == "diff_running":
return self._listening
elif key == "diff_changes_list":
return self._listening and view == self.view
return None
@classmethod
def instance(cls):
if cls._instance:
return cls._instance
else:
return cls()
def start_listen(self, cb, view, diff):
"""Start listening for the changes list.
Args:
cb: The callback to call when a widget is created.
view: The view to listen for.
diff: The diff currently being run.
"""
self.cb = cb
self.view = view
self.diff = diff
self._listening = True
self.current_row = -1
def stop(self):
"""Stop listening."""
self._listening = False