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