UNPKG

chrome-devtools-frontend

Version:
321 lines (290 loc) • 10.1 kB
// Copyright 2018 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. import * as Common from '../common/common.js'; import * as Diff from '../diff/diff.js'; // eslint-disable-line no-unused-vars import * as i18n from '../i18n/i18n.js'; import * as Persistence from '../persistence/persistence.js'; import * as SourceFrame from '../source_frame/source_frame.js'; import * as TextEditor from '../text_editor/text_editor.js'; // eslint-disable-line no-unused-vars import * as UI from '../ui/ui.js'; // eslint-disable-line no-unused-vars import * as Workspace from '../workspace/workspace.js'; import * as WorkspaceDiff from '../workspace_diff/workspace_diff.js'; import {Plugin} from './Plugin.js'; export const UIStrings = { /** *@description A context menu item in the Gutter Diff Plugin of the Sources panel */ localModifications: 'Local Modifications...', }; const str_ = i18n.i18n.registerUIStrings('sources/GutterDiffPlugin.js', UIStrings); const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_); export class GutterDiffPlugin extends Plugin { /** * @param {!SourceFrame.SourcesTextEditor.SourcesTextEditor} textEditor * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode */ constructor(textEditor, uiSourceCode) { super(); this._textEditor = textEditor; this._uiSourceCode = uiSourceCode; /** @type {!Array<!GutterDecoration>} */ this._decorations = []; this._textEditor.installGutter(DiffGutterType, true); this._workspaceDiff = WorkspaceDiff.WorkspaceDiff.workspaceDiff(); this._workspaceDiff.subscribeToDiffChange(this._uiSourceCode, this._update, this); this._update(); } /** * @override * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode * @return {boolean} */ static accepts(uiSourceCode) { return uiSourceCode.project().type() === Workspace.Workspace.projectTypes.Network; } /** * @param {!Array<!GutterDecoration>} removed * @param {!Array<!GutterDecoration>} added */ _updateDecorations(removed, added) { this._textEditor.operation(operation); function operation() { for (const decoration of removed) { decoration.remove(); } for (const decoration of added) { decoration.install(); } } } _update() { if (this._uiSourceCode) { this._workspaceDiff.requestDiff(this._uiSourceCode).then(this._innerUpdate.bind(this)); } else { this._innerUpdate(null); } } /** * @param {?Diff.Diff.DiffArray} lineDiff */ _innerUpdate(lineDiff) { if (!lineDiff) { this._updateDecorations(this._decorations, []); this._decorations = []; return; } const diff = SourceFrame.SourceCodeDiff.SourceCodeDiff.computeDiff(lineDiff); /** @type {!Map<number, !{lineNumber: number, type: !SourceFrame.SourceCodeDiff.EditType}>} */ const newDecorations = new Map(); for (let i = 0; i < diff.length; ++i) { const diffEntry = diff[i]; for (let lineNumber = diffEntry.from; lineNumber < diffEntry.to; ++lineNumber) { newDecorations.set(lineNumber, {lineNumber: lineNumber, type: diffEntry.type}); } } const decorationDiff = this._calculateDecorationsDiff(newDecorations); const addedDecorations = decorationDiff.added.map(entry => new GutterDecoration(this._textEditor, entry.lineNumber, entry.type)); this._decorations = decorationDiff.equal.concat(addedDecorations); this._updateDecorations(decorationDiff.removed, addedDecorations); this._decorationsSetForTest(newDecorations); } /** * @return {!Map<number, !GutterDecoration>} */ _decorationsByLine() { const decorations = new Map(); for (const decoration of this._decorations) { const lineNumber = decoration.lineNumber(); if (lineNumber !== -1) { decorations.set(lineNumber, decoration); } } return decorations; } /** * @param {!Map<number, !{lineNumber: number, type: !SourceFrame.SourceCodeDiff.EditType}>} decorations */ _calculateDecorationsDiff(decorations) { const oldDecorations = this._decorationsByLine(); const leftKeys = [...oldDecorations.keys()]; const rightKeys = [...decorations.keys()]; leftKeys.sort((a, b) => a - b); rightKeys.sort((a, b) => a - b); const removed = []; const added = []; const equal = []; let leftIndex = 0; let rightIndex = 0; while (leftIndex < leftKeys.length && rightIndex < rightKeys.length) { const leftKey = leftKeys[leftIndex]; const rightKey = rightKeys[rightIndex]; const left = oldDecorations.get(leftKey); const right = decorations.get(rightKey); if (!left) { throw new Error(`No decoration with key ${leftKey}`); } if (!right) { throw new Error(`No decoration with key ${rightKey}`); } if (leftKey === rightKey && left.type === right.type) { equal.push(left); ++leftIndex; ++rightIndex; } else if (leftKey <= rightKey) { removed.push(left); ++leftIndex; } else { added.push(right); ++rightIndex; } } while (leftIndex < leftKeys.length) { const leftKey = leftKeys[leftIndex++]; const left = oldDecorations.get(leftKey); if (!left) { throw new Error(`No decoration with key ${leftKey}`); } removed.push(left); } while (rightIndex < rightKeys.length) { const rightKey = rightKeys[rightIndex++]; const right = decorations.get(rightKey); if (!right) { throw new Error(`No decoration with key ${rightKey}`); } added.push(right); } return {added: added, removed: removed, equal: equal}; } /** * @param {!Map<number, !{lineNumber: number, type: !SourceFrame.SourceCodeDiff.EditType}>} decorations */ _decorationsSetForTest(decorations) { } /** * @override * @param {!UI.ContextMenu.ContextMenu} contextMenu * @param {number} lineNumber * @return {!Promise<void>} */ async populateLineGutterContextMenu(contextMenu, lineNumber) { GutterDiffPlugin._appendRevealDiffContextMenu(contextMenu, this._uiSourceCode); } /** * @override * @param {!UI.ContextMenu.ContextMenu} contextMenu * @param {number} lineNumber * @param {number} columnNumber * @return {!Promise<void>} */ async populateTextAreaContextMenu(contextMenu, lineNumber, columnNumber) { GutterDiffPlugin._appendRevealDiffContextMenu(contextMenu, this._uiSourceCode); } /** * @param {!UI.ContextMenu.ContextMenu} contextMenu * @param {!Workspace.UISourceCode.UISourceCode} uiSourceCode */ static _appendRevealDiffContextMenu(contextMenu, uiSourceCode) { if (!WorkspaceDiff.WorkspaceDiff.workspaceDiff().isUISourceCodeModified(uiSourceCode)) { return; } contextMenu.revealSection().appendItem(i18nString(UIStrings.localModifications), () => { Common.Revealer.reveal(new WorkspaceDiff.WorkspaceDiff.DiffUILocation(uiSourceCode)); }); } /** * @override */ dispose() { for (const decoration of this._decorations) { decoration.remove(); } WorkspaceDiff.WorkspaceDiff.workspaceDiff().unsubscribeFromDiffChange(this._uiSourceCode, this._update, this); } } export class GutterDecoration { /** * @param {!SourceFrame.SourcesTextEditor.SourcesTextEditor} textEditor * @param {number} lineNumber * @param {!SourceFrame.SourceCodeDiff.EditType} type */ constructor(textEditor, lineNumber, type) { this._textEditor = textEditor; this._position = this._textEditor.textEditorPositionHandle(lineNumber, 0); this._className = ''; if (type === SourceFrame.SourceCodeDiff.EditType.Insert) { this._className = 'diff-entry-insert'; } else if (type === SourceFrame.SourceCodeDiff.EditType.Delete) { this._className = 'diff-entry-delete'; } else if (type === SourceFrame.SourceCodeDiff.EditType.Modify) { this._className = 'diff-entry-modify'; } this.type = type; } /** * @return {number} */ lineNumber() { const location = this._position.resolve(); if (!location) { return -1; } return location.lineNumber; } install() { const location = this._position.resolve(); if (!location) { return; } const element = document.createElement('div'); element.classList.add('diff-marker'); element.textContent = '\xA0'; this._textEditor.setGutterDecoration(location.lineNumber, DiffGutterType, element); this._textEditor.toggleLineClass(location.lineNumber, this._className, true); } remove() { const location = this._position.resolve(); if (!location) { return; } this._textEditor.setGutterDecoration(location.lineNumber, DiffGutterType, null); this._textEditor.toggleLineClass(location.lineNumber, this._className, false); } } /** @type {string} */ export const DiffGutterType = 'CodeMirror-gutter-diff'; /** * @type {ContextMenuProvider} */ let contextMenuProviderInstance; /** * @implements {UI.ContextMenu.Provider} */ export class ContextMenuProvider { /** * @param {{forceNew: ?boolean}} opts */ static instance(opts = {forceNew: null}) { const {forceNew} = opts; if (!contextMenuProviderInstance || forceNew) { contextMenuProviderInstance = new ContextMenuProvider(); } return contextMenuProviderInstance; } /** * @override * @param {!Event} event * @param {!UI.ContextMenu.ContextMenu} contextMenu * @param {!Object} target */ appendApplicableItems(event, contextMenu, target) { let uiSourceCode = /** @type {!Workspace.UISourceCode.UISourceCode} */ (target); const binding = Persistence.Persistence.PersistenceImpl.instance().binding(uiSourceCode); if (binding) { uiSourceCode = binding.network; } GutterDiffPlugin._appendRevealDiffContextMenu(contextMenu, uiSourceCode); } }