OLD | NEW |
| (Empty) |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 /** | |
5 * @unrestricted | |
6 */ | |
7 Sources.SourceCodeDiff = class { | |
8 /** | |
9 * @param {!Promise<?string>} diffBaseline | |
10 * @param {!TextEditor.CodeMirrorTextEditor} textEditor | |
11 */ | |
12 constructor(diffBaseline, textEditor) { | |
13 this._textEditor = textEditor; | |
14 this._decorations = []; | |
15 this._textEditor.installGutter(Sources.SourceCodeDiff.DiffGutterType, true); | |
16 this._diffBaseline = diffBaseline; | |
17 /** @type {!Array<!TextEditor.TextEditorPositionHandle>}*/ | |
18 this._animatedLines = []; | |
19 } | |
20 | |
21 updateDiffMarkersWhenPossible() { | |
22 if (this._updateTimeout) | |
23 clearTimeout(this._updateTimeout); | |
24 this._updateTimeout = | |
25 setTimeout(this.updateDiffMarkersImmediately.bind(this), Sources.SourceC
odeDiff.UpdateTimeout); | |
26 } | |
27 | |
28 updateDiffMarkersImmediately() { | |
29 if (this._updateTimeout) | |
30 clearTimeout(this._updateTimeout); | |
31 this._updateTimeout = null; | |
32 this._diffBaseline.then(this._innerUpdate.bind(this)); | |
33 } | |
34 | |
35 /** | |
36 * @param {?string} oldContent | |
37 * @param {?string} newContent | |
38 */ | |
39 highlightModifiedLines(oldContent, newContent) { | |
40 if (typeof oldContent !== 'string' || typeof newContent !== 'string') | |
41 return; | |
42 | |
43 var diff = this._computeDiff(oldContent, newContent); | |
44 var changedLines = []; | |
45 for (var i = 0; i < diff.length; ++i) { | |
46 var diffEntry = diff[i]; | |
47 if (diffEntry.type === Sources.SourceCodeDiff.GutterDecorationType.Delete) | |
48 continue; | |
49 for (var lineNumber = diffEntry.from; lineNumber < diffEntry.to; ++lineNum
ber) { | |
50 var position = this._textEditor.textEditorPositionHandle(lineNumber, 0); | |
51 if (position) | |
52 changedLines.push(position); | |
53 } | |
54 } | |
55 this._updateHighlightedLines(changedLines); | |
56 this._animationTimeout = setTimeout( | |
57 this._updateHighlightedLines.bind(this, []), 400); // // Keep this time
out in sync with sourcesView.css. | |
58 } | |
59 | |
60 /** | |
61 * @param {!Array<!TextEditor.TextEditorPositionHandle>} newLines | |
62 */ | |
63 _updateHighlightedLines(newLines) { | |
64 if (this._animationTimeout) | |
65 clearTimeout(this._animationTimeout); | |
66 this._animationTimeout = null; | |
67 this._textEditor.operation(operation.bind(this)); | |
68 | |
69 /** | |
70 * @this {Sources.SourceCodeDiff} | |
71 */ | |
72 function operation() { | |
73 toggleLines.call(this, false); | |
74 this._animatedLines = newLines; | |
75 toggleLines.call(this, true); | |
76 } | |
77 | |
78 /** | |
79 * @param {boolean} value | |
80 * @this {Sources.SourceCodeDiff} | |
81 */ | |
82 function toggleLines(value) { | |
83 for (var i = 0; i < this._animatedLines.length; ++i) { | |
84 var location = this._animatedLines[i].resolve(); | |
85 if (location) | |
86 this._textEditor.toggleLineClass(location.lineNumber, 'highlight-line-
modification', value); | |
87 } | |
88 } | |
89 } | |
90 | |
91 /** | |
92 * @param {!Array<!Sources.SourceCodeDiff.GutterDecoration>} removed | |
93 * @param {!Array<!Sources.SourceCodeDiff.GutterDecoration>} added | |
94 */ | |
95 _updateDecorations(removed, added) { | |
96 this._textEditor.operation(operation); | |
97 | |
98 function operation() { | |
99 for (var decoration of removed) | |
100 decoration.remove(); | |
101 for (var decoration of added) | |
102 decoration.install(); | |
103 } | |
104 } | |
105 | |
106 /** | |
107 * @param {string} baseline | |
108 * @param {string} current | |
109 * @return {!Array<!{type: !Sources.SourceCodeDiff.GutterDecorationType, from:
number, to: number}>} | |
110 */ | |
111 _computeDiff(baseline, current) { | |
112 var diff = Diff.Diff.lineDiff(baseline.split('\n'), current.split('\n')); | |
113 var result = []; | |
114 var hasAdded = false; | |
115 var hasRemoved = false; | |
116 var blockStartLineNumber = 0; | |
117 var currentLineNumber = 0; | |
118 var isInsideBlock = false; | |
119 for (var i = 0; i < diff.length; ++i) { | |
120 var token = diff[i]; | |
121 if (token[0] === Diff.Diff.Operation.Equal) { | |
122 if (isInsideBlock) | |
123 flush(); | |
124 currentLineNumber += token[1].length; | |
125 continue; | |
126 } | |
127 | |
128 if (!isInsideBlock) { | |
129 isInsideBlock = true; | |
130 blockStartLineNumber = currentLineNumber; | |
131 } | |
132 | |
133 if (token[0] === Diff.Diff.Operation.Delete) { | |
134 hasRemoved = true; | |
135 } else { | |
136 currentLineNumber += token[1].length; | |
137 hasAdded = true; | |
138 } | |
139 } | |
140 if (isInsideBlock) | |
141 flush(); | |
142 if (result.length > 1 && result[0].from === 0 && result[1].from === 0) { | |
143 var merged = {type: Sources.SourceCodeDiff.GutterDecorationType.Modify, fr
om: 0, to: result[1].to}; | |
144 result.splice(0, 2, merged); | |
145 } | |
146 return result; | |
147 | |
148 function flush() { | |
149 var type = Sources.SourceCodeDiff.GutterDecorationType.Insert; | |
150 var from = blockStartLineNumber; | |
151 var to = currentLineNumber; | |
152 if (hasAdded && hasRemoved) { | |
153 type = Sources.SourceCodeDiff.GutterDecorationType.Modify; | |
154 } else if (!hasAdded && hasRemoved && from === 0 && to === 0) { | |
155 type = Sources.SourceCodeDiff.GutterDecorationType.Modify; | |
156 to = 1; | |
157 } else if (!hasAdded && hasRemoved) { | |
158 type = Sources.SourceCodeDiff.GutterDecorationType.Delete; | |
159 from -= 1; | |
160 } | |
161 result.push({type: type, from: from, to: to}); | |
162 isInsideBlock = false; | |
163 hasAdded = false; | |
164 hasRemoved = false; | |
165 } | |
166 } | |
167 | |
168 /** | |
169 * @param {?string} baseline | |
170 */ | |
171 _innerUpdate(baseline) { | |
172 var current = this._textEditor.text(); | |
173 if (typeof baseline !== 'string') { | |
174 this._updateDecorations(this._decorations, [] /* added */); | |
175 this._decorations = []; | |
176 return; | |
177 } | |
178 | |
179 var diff = this._computeDiff(baseline, current); | |
180 | |
181 /** @type {!Map<number, !Sources.SourceCodeDiff.GutterDecoration>} */ | |
182 var oldDecorations = new Map(); | |
183 for (var i = 0; i < this._decorations.length; ++i) { | |
184 var decoration = this._decorations[i]; | |
185 var lineNumber = decoration.lineNumber(); | |
186 if (lineNumber === -1) | |
187 continue; | |
188 oldDecorations.set(lineNumber, decoration); | |
189 } | |
190 | |
191 /** @type {!Map<number, !{lineNumber: number, type: !Sources.SourceCodeDiff.
GutterDecorationType}>} */ | |
192 var newDecorations = new Map(); | |
193 for (var i = 0; i < diff.length; ++i) { | |
194 var diffEntry = diff[i]; | |
195 for (var lineNumber = diffEntry.from; lineNumber < diffEntry.to; ++lineNum
ber) | |
196 newDecorations.set(lineNumber, {lineNumber: lineNumber, type: diffEntry.
type}); | |
197 } | |
198 | |
199 var decorationDiff = oldDecorations.diff(newDecorations, (e1, e2) => e1.type
=== e2.type); | |
200 var addedDecorations = decorationDiff.added.map( | |
201 entry => new Sources.SourceCodeDiff.GutterDecoration(this._textEditor, e
ntry.lineNumber, entry.type)); | |
202 | |
203 this._decorations = decorationDiff.equal.concat(addedDecorations); | |
204 this._updateDecorations(decorationDiff.removed, addedDecorations); | |
205 this._decorationsSetForTest(newDecorations); | |
206 } | |
207 | |
208 /** | |
209 * @param {!Map<number, !{lineNumber: number, type: !Sources.SourceCodeDiff.Gu
tterDecorationType}>} decorations | |
210 */ | |
211 _decorationsSetForTest(decorations) { | |
212 } | |
213 }; | |
214 | |
215 /** @type {number} */ | |
216 Sources.SourceCodeDiff.UpdateTimeout = 200; | |
217 | |
218 /** @type {string} */ | |
219 Sources.SourceCodeDiff.DiffGutterType = 'CodeMirror-gutter-diff'; | |
220 | |
221 /** @enum {symbol} */ | |
222 Sources.SourceCodeDiff.GutterDecorationType = { | |
223 Insert: Symbol('Insert'), | |
224 Delete: Symbol('Delete'), | |
225 Modify: Symbol('Modify'), | |
226 }; | |
227 | |
228 /** | |
229 * @unrestricted | |
230 */ | |
231 Sources.SourceCodeDiff.GutterDecoration = class { | |
232 /** | |
233 * @param {!TextEditor.CodeMirrorTextEditor} textEditor | |
234 * @param {number} lineNumber | |
235 * @param {!Sources.SourceCodeDiff.GutterDecorationType} type | |
236 */ | |
237 constructor(textEditor, lineNumber, type) { | |
238 this._textEditor = textEditor; | |
239 this._position = this._textEditor.textEditorPositionHandle(lineNumber, 0); | |
240 this._className = ''; | |
241 if (type === Sources.SourceCodeDiff.GutterDecorationType.Insert) | |
242 this._className = 'diff-entry-insert'; | |
243 else if (type === Sources.SourceCodeDiff.GutterDecorationType.Delete) | |
244 this._className = 'diff-entry-delete'; | |
245 else if (type === Sources.SourceCodeDiff.GutterDecorationType.Modify) | |
246 this._className = 'diff-entry-modify'; | |
247 this.type = type; | |
248 } | |
249 | |
250 /** | |
251 * @return {number} | |
252 */ | |
253 lineNumber() { | |
254 var location = this._position.resolve(); | |
255 if (!location) | |
256 return -1; | |
257 return location.lineNumber; | |
258 } | |
259 | |
260 install() { | |
261 var location = this._position.resolve(); | |
262 if (!location) | |
263 return; | |
264 var element = createElementWithClass('div', 'diff-marker'); | |
265 element.textContent = '\u00A0'; | |
266 this._textEditor.setGutterDecoration(location.lineNumber, Sources.SourceCode
Diff.DiffGutterType, element); | |
267 this._textEditor.toggleLineClass(location.lineNumber, this._className, true)
; | |
268 } | |
269 | |
270 remove() { | |
271 var location = this._position.resolve(); | |
272 if (!location) | |
273 return; | |
274 this._textEditor.setGutterDecoration(location.lineNumber, Sources.SourceCode
Diff.DiffGutterType, null); | |
275 this._textEditor.toggleLineClass(location.lineNumber, this._className, false
); | |
276 } | |
277 }; | |
OLD | NEW |