blob: 3a5fdad5a61ec8eaf5708e11615411449af77f68 [file] [log] [blame]
// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
export class SourceCodeDiff {
/**
* @param {!TextEditor.CodeMirrorTextEditor} textEditor
*/
constructor(textEditor) {
this._textEditor = textEditor;
/** @type {!Array<!TextEditor.TextEditorPositionHandle>}*/
this._animatedLines = [];
/** @type {?number} */
this._animationTimeout = null;
}
/**
* @param {?string} oldContent
* @param {?string} newContent
*/
highlightModifiedLines(oldContent, newContent) {
if (typeof oldContent !== 'string' || typeof newContent !== 'string') {
return;
}
const diff = SourceCodeDiff.computeDiff(Diff.Diff.lineDiff(oldContent.split('\n'), newContent.split('\n')));
const changedLines = [];
for (let i = 0; i < diff.length; ++i) {
const diffEntry = diff[i];
if (diffEntry.type === EditType.Delete) {
continue;
}
for (let lineNumber = diffEntry.from; lineNumber < diffEntry.to; ++lineNumber) {
const position = this._textEditor.textEditorPositionHandle(lineNumber, 0);
if (position) {
changedLines.push(position);
}
}
}
this._updateHighlightedLines(changedLines);
this._animationTimeout = setTimeout(
this._updateHighlightedLines.bind(this, []), 400); // // Keep this timeout in sync with sourcesView.css.
}
/**
* @param {!Array<!TextEditor.TextEditorPositionHandle>} newLines
*/
_updateHighlightedLines(newLines) {
if (this._animationTimeout) {
clearTimeout(this._animationTimeout);
}
this._animationTimeout = null;
this._textEditor.operation(operation.bind(this));
/**
* @this {SourceCodeDiff}
*/
function operation() {
toggleLines.call(this, false);
this._animatedLines = newLines;
toggleLines.call(this, true);
}
/**
* @param {boolean} value
* @this {SourceCodeDiff}
*/
function toggleLines(value) {
for (let i = 0; i < this._animatedLines.length; ++i) {
const location = this._animatedLines[i].resolve();
if (location) {
this._textEditor.toggleLineClass(location.lineNumber, 'highlight-line-modification', value);
}
}
}
}
/**
* @param {!Diff.Diff.DiffArray} diff
* @return {!Array<!{type: !EditType, from: number, to: number}>}
*/
static computeDiff(diff) {
const result = [];
let hasAdded = false;
let hasRemoved = false;
let blockStartLineNumber = 0;
let currentLineNumber = 0;
let isInsideBlock = false;
for (let i = 0; i < diff.length; ++i) {
const token = diff[i];
if (token[0] === Diff.Diff.Operation.Equal) {
if (isInsideBlock) {
flush();
}
currentLineNumber += token[1].length;
continue;
}
if (!isInsideBlock) {
isInsideBlock = true;
blockStartLineNumber = currentLineNumber;
}
if (token[0] === Diff.Diff.Operation.Delete) {
hasRemoved = true;
} else {
currentLineNumber += token[1].length;
hasAdded = true;
}
}
if (isInsideBlock) {
flush();
}
if (result.length > 1 && result[0].from === 0 && result[1].from === 0) {
const merged = {type: EditType.Modify, from: 0, to: result[1].to};
result.splice(0, 2, merged);
}
return result;
function flush() {
let type = EditType.Insert;
let from = blockStartLineNumber;
let to = currentLineNumber;
if (hasAdded && hasRemoved) {
type = EditType.Modify;
} else if (!hasAdded && hasRemoved && from === 0 && to === 0) {
type = EditType.Modify;
to = 1;
} else if (!hasAdded && hasRemoved) {
type = EditType.Delete;
from -= 1;
}
result.push({type: type, from: from, to: to});
isInsideBlock = false;
hasAdded = false;
hasRemoved = false;
}
}
}
/** @enum {symbol} */
export const EditType = {
Insert: Symbol('Insert'),
Delete: Symbol('Delete'),
Modify: Symbol('Modify'),
};
/* Legacy exported object */
self.SourceFrame = self.SourceFrame || {};
/* Legacy exported object */
SourceFrame = SourceFrame || {};
/** @constructor */
SourceFrame.SourceCodeDiff = SourceCodeDiff;
/** @enum {symbol} */
SourceFrame.SourceCodeDiff.EditType = EditType;