UNPKG

@dcloudio/uni-debugger

Version:

uni-app debugger

305 lines (286 loc) 11.5 kB
// Copyright 2017 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. /** * @typedef {!{ * id: string, * contentProvider: !Common.ContentProvider, * line: number, * column: number * }} */ Coverage.RawLocation; Coverage.CoverageDecorationManager = class { /** * @param {!Coverage.CoverageModel} coverageModel */ constructor(coverageModel) { this._coverageModel = coverageModel; /** @type {!Map<!Common.ContentProvider, ?TextUtils.Text>} */ this._textByProvider = new Map(); /** @type {!Multimap<!Common.ContentProvider, !Workspace.UISourceCode>} */ this._uiSourceCodeByContentProvider = new Multimap(); // TODO(crbug.com/720162): get rid of this, use bindings. /** @type {!WeakMap<!Workspace.UISourceCode, !Array<!SDK.CSSStyleSheetHeader>>} */ this._documentUISouceCodeToStylesheets = new WeakMap(); for (const uiSourceCode of Workspace.workspace.uiSourceCodes()) uiSourceCode.addLineDecoration(0, Coverage.CoverageDecorationManager._decoratorType, this); Workspace.workspace.addEventListener(Workspace.Workspace.Events.UISourceCodeAdded, this._onUISourceCodeAdded, this); } reset() { for (const uiSourceCode of Workspace.workspace.uiSourceCodes()) uiSourceCode.removeDecorationsForType(Coverage.CoverageDecorationManager._decoratorType); } dispose() { this.reset(); Workspace.workspace.removeEventListener( Workspace.Workspace.Events.UISourceCodeAdded, this._onUISourceCodeAdded, this); } /** * @param {!Array<!Coverage.CoverageInfo>} updatedEntries */ update(updatedEntries) { for (const entry of updatedEntries) { for (const uiSourceCode of this._uiSourceCodeByContentProvider.get(entry.contentProvider())) { uiSourceCode.removeDecorationsForType(Coverage.CoverageDecorationManager._decoratorType); uiSourceCode.addLineDecoration(0, Coverage.CoverageDecorationManager._decoratorType, this); } } } /** * @param {!Workspace.UISourceCode} uiSourceCode * @return {!Promise<!Array<boolean>>} */ async usageByLine(uiSourceCode) { const result = []; const sourceText = new TextUtils.Text(uiSourceCode.content() || ''); await this._updateTexts(uiSourceCode, sourceText); const lineEndings = sourceText.lineEndings(); for (let line = 0; line < sourceText.lineCount(); ++line) { const lineLength = lineEndings[line] - (line ? lineEndings[line - 1] : 0) - 1; if (!lineLength) { result.push(undefined); continue; } const startLocations = this._rawLocationsForSourceLocation(uiSourceCode, line, 0); const endLocations = this._rawLocationsForSourceLocation(uiSourceCode, line, lineLength); let used = undefined; for (let startIndex = 0, endIndex = 0; startIndex < startLocations.length; ++startIndex) { const start = startLocations[startIndex]; while (endIndex < endLocations.length && Coverage.CoverageDecorationManager._compareLocations(start, endLocations[endIndex]) >= 0) ++endIndex; if (endIndex >= endLocations.length || endLocations[endIndex].id !== start.id) continue; const end = endLocations[endIndex++]; const text = this._textByProvider.get(end.contentProvider); if (!text) continue; const textValue = text.value(); let startOffset = Math.min(text.offsetFromPosition(start.line, start.column), textValue.length - 1); let endOffset = Math.min(text.offsetFromPosition(end.line, end.column), textValue.length - 1); while (startOffset <= endOffset && /\s/.test(textValue[startOffset])) ++startOffset; while (startOffset <= endOffset && /\s/.test(textValue[endOffset])) --endOffset; if (startOffset <= endOffset) used = this._coverageModel.usageForRange(end.contentProvider, startOffset, endOffset); if (used) break; } result.push(used); } return result; } /** * @param {!Workspace.UISourceCode} uiSourceCode * @param {!TextUtils.Text} text * @return {!Promise} */ _updateTexts(uiSourceCode, text) { const promises = []; for (let line = 0; line < text.lineCount(); ++line) { for (const entry of this._rawLocationsForSourceLocation(uiSourceCode, line, 0)) { if (this._textByProvider.has(entry.contentProvider)) continue; this._textByProvider.set(entry.contentProvider, null); this._uiSourceCodeByContentProvider.set(entry.contentProvider, uiSourceCode); promises.push(this._updateTextForProvider(entry.contentProvider)); } } return Promise.all(promises); } /** * @param {!Common.ContentProvider} contentProvider * @return {!Promise} */ async _updateTextForProvider(contentProvider) { const content = await contentProvider.requestContent(); this._textByProvider.set(contentProvider, new TextUtils.Text(content)); } /** * @param {!Workspace.UISourceCode} uiSourceCode * @param {number} line * @param {number} column * @return {!Array<!Coverage.RawLocation>} */ _rawLocationsForSourceLocation(uiSourceCode, line, column) { const result = []; const contentType = uiSourceCode.contentType(); if (contentType.hasScripts()) { let location = Bindings.debuggerWorkspaceBinding.uiLocationToRawLocation(uiSourceCode, line, column); if (location && location.script()) { const script = location.script(); if (script.isInlineScript() && contentType.isDocument()) { if (comparePositions(script.lineOffset, script.columnOffset, location.lineNumber, location.columnNumber) > 0 || comparePositions(script.endLine, script.endColumn, location.lineNumber, location.columnNumber) <= 0) { location = null; } else { location.lineNumber -= script.lineOffset; if (!location.lineNumber) location.columnNumber -= script.columnOffset; } } if (location) { result.push({ id: `js:${location.scriptId}`, contentProvider: location.script(), line: location.lineNumber, column: location.columnNumber }); } } } if (contentType.isStyleSheet() || contentType.isDocument()) { const rawStyleLocations = contentType.isDocument() ? this._documentUILocationToCSSRawLocations(uiSourceCode, line, column) : Bindings.cssWorkspaceBinding.uiLocationToRawLocations(new Workspace.UILocation(uiSourceCode, line, column)); for (const location of rawStyleLocations) { const header = location.header(); if (!header) continue; if (header.isInline && contentType.isDocument()) { location.lineNumber -= header.startLine; if (!location.lineNumber) location.columnNumber -= header.startColumn; } result.push({ id: `css:${location.styleSheetId}`, contentProvider: location.header(), line: location.lineNumber, column: location.columnNumber }); } } result.sort(Coverage.CoverageDecorationManager._compareLocations); /** * @param {number} aLine * @param {number} aColumn * @param {number} bLine * @param {number} bColumn * @return {number} */ function comparePositions(aLine, aColumn, bLine, bColumn) { return aLine - bLine || aColumn - bColumn; } return result; } /** * TODO(crbug.com/720162): get rid of this, use bindings. * * @param {!Workspace.UISourceCode} uiSourceCode * @param {number} line * @param {number} column * @return {!Array<!SDK.CSSLocation>} */ _documentUILocationToCSSRawLocations(uiSourceCode, line, column) { let stylesheets = this._documentUISouceCodeToStylesheets.get(uiSourceCode); if (!stylesheets) { stylesheets = []; const cssModel = this._coverageModel.target().model(SDK.CSSModel); if (!cssModel) return []; for (const headerId of cssModel.styleSheetIdsForURL(uiSourceCode.url())) { const header = cssModel.styleSheetHeaderForId(headerId); if (header) stylesheets.push(header); } stylesheets.sort(stylesheetComparator); this._documentUISouceCodeToStylesheets.set(uiSourceCode, stylesheets); } const endIndex = stylesheets.upperBound(undefined, (unused, header) => line - header.startLine || column - header.startColumn); if (!endIndex) return []; const locations = []; const last = stylesheets[endIndex - 1]; for (let index = endIndex - 1; index >= 0 && stylesheets[index].startLine === last.startLine && stylesheets[index].startColumn === last.startColumn; --index) locations.push(new SDK.CSSLocation(stylesheets[index], line, column)); return locations; /** * @param {!SDK.CSSStyleSheetHeader} a * @param {!SDK.CSSStyleSheetHeader} b * @return {number} */ function stylesheetComparator(a, b) { return a.startLine - b.startLine || a.startColumn - b.startColumn || a.id.localeCompare(b.id); } } /** * @param {!Coverage.RawLocation} a * @param {!Coverage.RawLocation} b */ static _compareLocations(a, b) { return a.id.localeCompare(b.id) || a.line - b.line || a.column - b.column; } /** * @param {!Common.Event} event */ _onUISourceCodeAdded(event) { const uiSourceCode = /** @type !Workspace.UISourceCode */ (event.data); uiSourceCode.addLineDecoration(0, Coverage.CoverageDecorationManager._decoratorType, this); } }; Coverage.CoverageDecorationManager._decoratorType = 'coverage'; /** * @implements {SourceFrame.LineDecorator} */ Coverage.CoverageView.LineDecorator = class { /** * @override * @param {!Workspace.UISourceCode} uiSourceCode * @param {!TextEditor.CodeMirrorTextEditor} textEditor */ decorate(uiSourceCode, textEditor) { const decorations = uiSourceCode.decorationsForType(Coverage.CoverageDecorationManager._decoratorType); if (!decorations || !decorations.size) { textEditor.uninstallGutter(Coverage.CoverageView.LineDecorator._gutterType); return; } const decorationManager = /** @type {!Coverage.CoverageDecorationManager} */ (decorations.values().next().value.data()); decorationManager.usageByLine(uiSourceCode).then(lineUsage => { textEditor.operation(() => this._innerDecorate(textEditor, lineUsage)); }); } /** * @param {!TextEditor.CodeMirrorTextEditor} textEditor * @param {!Array<boolean>} lineUsage */ _innerDecorate(textEditor, lineUsage) { const gutterType = Coverage.CoverageView.LineDecorator._gutterType; textEditor.uninstallGutter(gutterType); textEditor.installGutter(gutterType, false); for (let line = 0; line < lineUsage.length; ++line) { // Do not decorate the line if we don't have data. if (typeof lineUsage[line] !== 'boolean') continue; const className = lineUsage[line] ? 'text-editor-coverage-used-marker' : 'text-editor-coverage-unused-marker'; textEditor.setGutterDecoration(line, gutterType, createElementWithClass('div', className)); } } }; Coverage.CoverageView.LineDecorator._gutterType = 'CodeMirror-gutter-coverage';