From db334b22a6a4cbddd0b271e83aab98e2855a9d56 Mon Sep 17 00:00:00 2001 From: isidor Date: Fri, 13 Jan 2017 18:38:42 +0100 Subject: [PATCH] debug: inline values use map, set. Polish --- .../debugEditorContribution.ts | 19 +- .../electron-browser/debugInlineDecorators.ts | 166 ------------------ .../electron-browser/debugInlineValues.ts | 140 +++++++++++++++ ...tors.test.ts => debugInlineValues.test.ts} | 78 +++----- 4 files changed, 176 insertions(+), 227 deletions(-) delete mode 100644 src/vs/workbench/parts/debug/electron-browser/debugInlineDecorators.ts create mode 100644 src/vs/workbench/parts/debug/electron-browser/debugInlineValues.ts rename src/vs/workbench/parts/debug/test/electron-browser/{debugInlineDecorators.test.ts => debugInlineValues.test.ts} (68%) diff --git a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts index 1fa41b06b92ee..917641ad7f3cd 100644 --- a/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts +++ b/src/vs/workbench/parts/debug/electron-browser/debugEditorContribution.ts @@ -14,10 +14,9 @@ import { visit } from 'vs/base/common/json'; import { IAction, Action } from 'vs/base/common/actions'; import { KeyCode } from 'vs/base/common/keyCodes'; import { IKeyboardEvent } from 'vs/base/browser/keyboardEvent'; -import { IStringDictionary } from 'vs/base/common/collections'; import { ICodeEditor, IEditorMouseEvent } from 'vs/editor/browser/editorBrowser'; import { editorContribution } from 'vs/editor/browser/editorBrowserExtensions'; -import { IRange, IModelDecorationOptions, MouseTargetType, IModelDeltaDecoration, TrackedRangeStickiness, IPosition } from 'vs/editor/common/editorCommon'; +import { IModelDecorationOptions, MouseTargetType, IModelDeltaDecoration, TrackedRangeStickiness, IPosition } from 'vs/editor/common/editorCommon'; import { ICodeEditorService } from 'vs/editor/common/services/codeEditorService'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; @@ -32,7 +31,7 @@ import { RemoveBreakpointAction, EditConditionalBreakpointAction, EnableBreakpoi import { IDebugEditorContribution, IDebugService, State, IBreakpoint, EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, IStackFrame, IDebugConfiguration } from 'vs/workbench/parts/debug/common/debug'; import { BreakpointWidget } from 'vs/workbench/parts/debug/browser/breakpointWidget'; import { FloatingClickWidget } from 'vs/workbench/parts/preferences/browser/preferencesWidgets'; -import { getNameValueMapFromScopeChildren, getDecorators, getEditorWordRangeMap } from 'vs/workbench/parts/debug/electron-browser/debugInlineDecorators'; +import { toNameValueMap, getDecorations, getWordToLineNumbersMap } from 'vs/workbench/parts/debug/electron-browser/debugInlineValues'; const HOVER_DELAY = 300; const LAUNCH_JSON_REGEX = /launch\.json$/; @@ -52,7 +51,7 @@ export class DebugEditorContribution implements IDebugEditorContribution { private breakpointWidget: BreakpointWidget; private breakpointWidgetVisible: IContextKey; private removeDecorationsTimeoutId = 0; - private editorModelWordRangeMap: IStringDictionary; + private wordToLineNumbersMap: Map; private configurationWidget: FloatingClickWidget; @@ -226,14 +225,14 @@ export class DebugEditorContribution implements IDebugEditorContribution { if (!stackFrame) { this.removeDecorationsTimeoutId = setTimeout(() => { this.editor.removeDecorations(INLINE_DECORATOR_KEY); - this.editorModelWordRangeMap = null; + this.wordToLineNumbersMap = null; }, REMOVE_DECORATORS_DEBOUNCE_INTERVAL); return; } // URI has changed, invalidate the editorWordRangeMap so its re-computed for the current model if (stackFrame.source.uri.toString() !== this.editor.getModel().uri.toString()) { - this.editorModelWordRangeMap = null; + this.wordToLineNumbersMap = null; } stackFrame.getScopes() @@ -243,13 +242,13 @@ export class DebugEditorContribution implements IDebugEditorContribution { const editorModel = this.editor.getModel(); // Compute name-value map for all variables in scope chain const expressions = [].concat.apply([], children); - const nameValueMap = getNameValueMapFromScopeChildren(expressions); + const nameValueMap = toNameValueMap(expressions); // Build wordRangeMap if not already computed for the editor model - if (!this.editorModelWordRangeMap) { - this.editorModelWordRangeMap = getEditorWordRangeMap(editorModel); + if (!this.wordToLineNumbersMap) { + this.wordToLineNumbersMap = getWordToLineNumbersMap(editorModel); } // Compute decorators from nameValueMap and wordRangeMap and apply to editor - const decorators = getDecorators(nameValueMap, this.editorModelWordRangeMap, editorModel.getLinesContent()); + const decorators = getDecorations(nameValueMap, this.wordToLineNumbersMap); this.editor.setDecorations(INLINE_DECORATOR_KEY, decorators); }); } diff --git a/src/vs/workbench/parts/debug/electron-browser/debugInlineDecorators.ts b/src/vs/workbench/parts/debug/electron-browser/debugInlineDecorators.ts deleted file mode 100644 index d78ad27ad19d8..0000000000000 --- a/src/vs/workbench/parts/debug/electron-browser/debugInlineDecorators.ts +++ /dev/null @@ -1,166 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import { IStringDictionary } from 'vs/base/common/collections'; -import { IDecorationOptions, IRange, IModel } from 'vs/editor/common/editorCommon'; -import { StandardTokenType } from 'vs/editor/common/modes'; -import { IExpression } from 'vs/workbench/parts/debug/common/debug'; - -export const MAX_INLINE_VALUE_LENGTH = 50; // Max string length of each inline 'x = y' string. If exceeded ... is added -export const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added -export const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We want to limit ourselves for perf reasons -export const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped -export const ELLIPSES = '…'; -// LanguageConfigurationRegistry.getWordDefinition() return regexes that allow spaces and punctuation characters for languages like python -// Using that approach is not viable so we are using a simple regex to look for word tokens. -export const WORD_REGEXP = /[\$\_A-Za-z][\$\_A-Za-z0-9]*/g; - -export function getNameValueMapFromScopeChildren(expressions: IExpression[]): IStringDictionary { - const nameValueMap: IStringDictionary = Object.create(null); - let valueCount = 0; - - for (let expr of expressions) { - // Put ellipses in value if its too long. Preserve last char e.g "longstr…" or {a:true, b:true, …} - let value = expr.value; - if (value && value.length > MAX_INLINE_VALUE_LENGTH) { - value = value.substr(0, MAX_INLINE_VALUE_LENGTH - ELLIPSES.length) + ELLIPSES + value[value.length - 1]; - } - - nameValueMap[expr.name] = value; - - // Limit the size of map. Too large can have a perf impact - if (++valueCount >= MAX_NUM_INLINE_VALUES) { - break; - } - } - - return nameValueMap; -} - -export function getDecorators(nameValueMap: IStringDictionary, wordRangeMap: IStringDictionary, linesContent: string[]): IDecorationOptions[] { - const linesNames: IStringDictionary> = Object.create(null); - const names = Object.keys(nameValueMap); - const decorators: IDecorationOptions[] = []; - - // Compute unique set of names on each line - for (let name of names) { - const ranges = wordRangeMap[name]; - if (ranges) { - for (let range of ranges) { - const lineNum = range.startLineNumber; - if (!linesNames[lineNum]) { - linesNames[lineNum] = Object.create(null); - } - linesNames[lineNum][name] = true; - } - } - } - - // Compute decorators for each line - const lineNums = Object.keys(linesNames); - for (let lineNum of lineNums) { - const uniqueNames = Object.keys(linesNames[lineNum]); - const decorator = getDecoratorFromNames(parseInt(lineNum), uniqueNames, nameValueMap, linesContent); - decorators.push(decorator); - } - - return decorators; -} - -export function getDecoratorFromNames(lineNumber: number, names: string[], nameValueMap: IStringDictionary, linesContent: string[]): IDecorationOptions { - const margin = '10px'; - const backgroundColor = 'rgba(255,200,0,0.2)'; - const lightForegroundColor = 'rgba(0,0,0,0.5)'; - const darkForegroundColor = 'rgba(255,255,255,0.5)'; - const lineLength = linesContent[lineNumber - 1].length; - - // Wrap with 1em unicode space for readability - let contentText = '\u2003' + names.map(n => `${n} = ${nameValueMap[n]}`).join(', ') + '\u2003'; - - // If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line - if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) { - contentText = contentText.substr(0, MAX_INLINE_DECORATOR_LENGTH - ELLIPSES.length) + ELLIPSES; - } - - const decorator: IDecorationOptions = { - range: { - startLineNumber: lineNumber, - endLineNumber: lineNumber, - startColumn: lineLength, - endColumn: lineLength + 1 - }, - renderOptions: { - dark: { - after: { - contentText, - backgroundColor, - color: darkForegroundColor, - margin - } - }, - light: { - after: { - contentText, - backgroundColor, - color: lightForegroundColor, - margin - } - } - } - }; - - return decorator; -} - -export function getEditorWordRangeMap(editorModel: IModel): IStringDictionary { - const wordRangeMap: IStringDictionary = Object.create(null); - const linesContent = editorModel.getLinesContent(); - - // For every word in every line, map its ranges for fast lookup - for (let i = 0, len = linesContent.length; i < len; ++i) { - const lineContent = linesContent[i]; - - // If line is too long then skip the line - if (lineContent.length > MAX_TOKENIZATION_LINE_LEN) { - continue; - } - - const lineTokens = editorModel.getLineTokens(i + 1); // lineNumbers are 1 based - - for (let j = 0, len = lineTokens.getTokenCount(); j < len; ++j) { - let startOffset = lineTokens.getTokenStartOffset(j); - let endOffset = lineTokens.getTokenEndOffset(j); - const tokenStr = lineContent.substring(startOffset, endOffset); - - // Token is a word and not a comment - if (lineTokens.getStandardTokenType(j) === StandardTokenType.Other) { - WORD_REGEXP.lastIndex = 0; // We assume tokens will usually map 1:1 to words if they match - const wordMatch = WORD_REGEXP.exec(tokenStr); - - if (wordMatch) { - const word = wordMatch[0]; - startOffset += wordMatch.index; - endOffset = startOffset + word.length; - - const range: IRange = { - startColumn: startOffset + 1, // Line and columns are 1 based - endColumn: endOffset + 1, - startLineNumber: i + 1, - endLineNumber: i + 1 - }; - - if (!wordRangeMap[word]) { - wordRangeMap[word] = []; - } - - wordRangeMap[word].push(range); - } - } - } - } - - return wordRangeMap; -} diff --git a/src/vs/workbench/parts/debug/electron-browser/debugInlineValues.ts b/src/vs/workbench/parts/debug/electron-browser/debugInlineValues.ts new file mode 100644 index 0000000000000..ddb2864ee93a9 --- /dev/null +++ b/src/vs/workbench/parts/debug/electron-browser/debugInlineValues.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDecorationOptions, IModel } from 'vs/editor/common/editorCommon'; +import { StandardTokenType } from 'vs/editor/common/modes'; +import { IExpression } from 'vs/workbench/parts/debug/common/debug'; + +export const MAX_INLINE_VALUE_LENGTH = 50; // Max string length of each inline 'x = y' string. If exceeded ... is added +export const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added +export const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We want to limit ourselves for perf reasons +export const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped +// LanguageConfigurationRegistry.getWordDefinition() return regexes that allow spaces and punctuation characters for languages like python +// Using that approach is not viable so we are using a simple regex to look for word tokens. +export const WORD_REGEXP = /[\$\_A-Za-z][\$\_A-Za-z0-9]*/g; + +export function toNameValueMap(expressions: IExpression[]): Map { + const result = new Map(); + let valueCount = 0; + + for (let expr of expressions) { + // Put ellipses in value if its too long. Preserve last char e.g "longstr…" or {a:true, b:true, …} + let value = expr.value; + if (value && value.length > MAX_INLINE_VALUE_LENGTH) { + value = value.substr(0, MAX_INLINE_VALUE_LENGTH) + '…' + value[value.length - 1]; + } + + result.set(expr.name, value); + + // Limit the size of map. Too large can have a perf impact + if (++valueCount >= MAX_NUM_INLINE_VALUES) { + break; + } + } + + return result; +} + +export function getDecorations(nameValueMap: Map, wordToLineNumbersMap: Map): IDecorationOptions[] { + const lineToNamesMap: Map = new Map(); + const decorations: IDecorationOptions[] = []; + + // Compute unique set of names on each line + nameValueMap.forEach((value, name) => { + if (wordToLineNumbersMap.has(name)) { + for (let lineNumber of wordToLineNumbersMap.get(name)) { + if (!lineToNamesMap.has(lineNumber)) { + lineToNamesMap.set(lineNumber, []); + } + + lineToNamesMap.get(lineNumber).push(name); + } + } + }); + + // Compute decorators for each line + lineToNamesMap.forEach((names, line) => { + // Wrap with 1em unicode space for readability + const contentText = '\u2003' + names.map(name => `${name} = ${nameValueMap.get(name)}`).join(', ') + '\u2003'; + decorations.push(createDecoration(line, contentText)); + }); + + return decorations; +} + +function createDecoration(lineNumber: number, contentText: string): IDecorationOptions { + const margin = '10px'; + const backgroundColor = 'rgba(255, 200, 0, 0.2)'; + const lightForegroundColor = 'rgba(0, 0, 0, 0.5)'; + const darkForegroundColor = 'rgba(255, 255, 255, 0.5)'; + + // If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line + if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) { + contentText = contentText.substr(0, MAX_INLINE_DECORATOR_LENGTH) + '...'; + } + + return { + range: { + startLineNumber: lineNumber, + endLineNumber: lineNumber, + startColumn: Number.MAX_VALUE, + endColumn: Number.MAX_VALUE + }, + renderOptions: { + dark: { + after: { + contentText, + backgroundColor, + color: darkForegroundColor, + margin + } + }, + light: { + after: { + contentText, + backgroundColor, + color: lightForegroundColor, + margin + } + } + } + }; +} + +export function getWordToLineNumbersMap(model: IModel): Map { + const result = new Map(); + + // For every word in every line, map its ranges for fast lookup + for (let lineNumber = 1, len = model.getLineCount(); lineNumber <= len; ++lineNumber) { + const lineContent = model.getLineContent(lineNumber); + + // If line is too long then skip the line + if (lineContent.length > MAX_TOKENIZATION_LINE_LEN) { + continue; + } + + const lineTokens = model.getLineTokens(lineNumber); + for (let token = lineTokens.firstToken(); !!token; token = token.next()) { + const tokenStr = lineContent.substring(token.startOffset, token.endOffset); + + // Token is a word and not a comment + if (token.tokenType === StandardTokenType.Other) { + WORD_REGEXP.lastIndex = 0; // We assume tokens will usually map 1:1 to words if they match + const wordMatch = WORD_REGEXP.exec(tokenStr); + + if (wordMatch) { + const word = wordMatch[0]; + if (!result.has(word)) { + result.set(word, []); + } + + result.get(word).push(lineNumber); + } + } + } + } + + return result; +} diff --git a/src/vs/workbench/parts/debug/test/electron-browser/debugInlineDecorators.test.ts b/src/vs/workbench/parts/debug/test/electron-browser/debugInlineValues.test.ts similarity index 68% rename from src/vs/workbench/parts/debug/test/electron-browser/debugInlineDecorators.test.ts rename to src/vs/workbench/parts/debug/test/electron-browser/debugInlineValues.test.ts index a84b8f675d659..4b3df0ffb1483 100644 --- a/src/vs/workbench/parts/debug/test/electron-browser/debugInlineDecorators.test.ts +++ b/src/vs/workbench/parts/debug/test/electron-browser/debugInlineValues.test.ts @@ -6,23 +6,24 @@ import * as assert from 'assert'; import { IStringDictionary } from 'vs/base/common/collections'; import { Model as EditorModel } from 'vs/editor/common/model/model'; -import { IRange, IModel } from 'vs/editor/common/editorCommon'; +import { IModel } from 'vs/editor/common/editorCommon'; import { StandardTokenType } from 'vs/editor/common/modes'; import { LineTokens } from 'vs/editor/common/core/lineTokens'; import { IExpression } from 'vs/workbench/parts/debug/common/debug'; -import * as inlineDecorators from 'vs/workbench/parts/debug/electron-browser/debugInlineDecorators'; +import * as inlineValues from 'vs/workbench/parts/debug/electron-browser/debugInlineValues'; // Test data const testLine = 'function doit(everything, is, awesome, awesome, when, youre, part, of, a, team){}'; - -const testNameValueMap = { - everything: '{emmet: true, batman: true, legoUniverse: true}', - is: '15', - awesome: '"aweeeeeeeeeeeeeeeeeeeeeeeeeeeeeeesome…"', - when: 'true', - youre: '"Yes I mean you"', - part: '"𝄞 ♪ ♫"' -}; +const testNameValueMap = new Map(); + +setup(() => { + testNameValueMap.set('everything', '{emmet: true, batman: true, legoUniverse: true}'); + testNameValueMap.set('is', '15'); + testNameValueMap.set('awesome', '"aweeeeeeeeeeeeeeeeeeeeeeeeeeeeeeesome…"'); + testNameValueMap.set('when', 'true'); + testNameValueMap.set('youre', '"Yes I mean you"'); + testNameValueMap.set('part', '"𝄞 ♪ ♫"'); +}); suite('Debug - Inline Value Decorators', () => { test('getNameValueMapFromScopeChildren trims long values', () => { @@ -31,7 +32,7 @@ suite('Debug - Inline Value Decorators', () => { createExpression('blah', createLongString()) ]; - const nameValueMap = inlineDecorators.getNameValueMapFromScopeChildren(expressions); + const nameValueMap = inlineValues.toNameValueMap(expressions); // Ensure blah is capped and ellipses added assert.deepEqual(nameValueMap, { @@ -54,7 +55,7 @@ suite('Debug - Inline Value Decorators', () => { const val = `val${i}.${j}`; expressions[j] = createExpression(name, val); - if ((i * expressions.length + j) < inlineDecorators.MAX_NUM_INLINE_VALUES) { + if ((i * expressions.length + j) < inlineValues.MAX_NUM_INLINE_VALUES) { expectedNameValueMap[name] = val; } } @@ -63,32 +64,16 @@ suite('Debug - Inline Value Decorators', () => { } const expressions = [].concat.apply([], scopeChildren); - const nameValueMap = inlineDecorators.getNameValueMapFromScopeChildren(expressions); + const nameValueMap = inlineValues.toNameValueMap(expressions); assert.deepEqual(nameValueMap, expectedNameValueMap); }); - test('getDecoratorFromNames caps long decorator afterText', () => { - const names = Object.keys(testNameValueMap); - const lineNumber = 1; - const decorator = inlineDecorators.getDecoratorFromNames(lineNumber, names, testNameValueMap, [testLine]); - - const expectedDecoratorText = ' everything = {emmet: true, batman: true, legoUniverse: true}, is = 15, awesome = "aweeeeeeeeeeeeeeeeeeeeeeeeeeeeeeesome…", when = true, youre = "Yes…'; - assert.equal(decorator.renderOptions.dark.after.contentText, decorator.renderOptions.light.after.contentText); - assert.equal(decorator.renderOptions.dark.after.contentText, expectedDecoratorText); - assert.deepEqual(decorator.range, { - startLineNumber: lineNumber, - endLineNumber: lineNumber, - startColumn: testLine.length, - endColumn: testLine.length + 1 - }); - }); - test('getDecorators returns correct decorator afterText', () => { const lineContent = 'console.log(everything, part, part);'; // part shouldn't be duplicated const lineNumber = 1; - const wordRangeMap = updateWordRangeMap(Object.create(null), lineNumber, lineContent); - const decorators = inlineDecorators.getDecorators(testNameValueMap, wordRangeMap, [lineContent]); + const wordToLinesMap = getWordToLineMap(lineNumber, lineContent); + const decorators = inlineValues.getDecorations(testNameValueMap, wordToLinesMap); const expectedDecoratorText = ' everything = {emmet: true, batman: true, legoUniverse: true}, part = "𝄞 ♪ ♫" '; assert.equal(decorators[0].renderOptions.dark.after.contentText, expectedDecoratorText); }); @@ -98,7 +83,7 @@ suite('Debug - Inline Value Decorators', () => { const editorModel = EditorModel.createFromString(`/** Copyright comment */\n \n${testLine}\n// Test comment\n${createLongString()}\n`); mockEditorModelLineTokens(editorModel); - const wordRangeMap = inlineDecorators.getEditorWordRangeMap(editorModel); + const wordRangeMap = inlineValues.getWordToLineNumbersMap(editorModel); const words = Object.keys(wordRangeMap); assert.deepEqual(words, expectedWords); }); @@ -125,8 +110,9 @@ function createLongString(): string { } // Simple word range creator that maches wordRegex throughout string -function updateWordRangeMap(wordRangeMap: IStringDictionary, lineNumber: number, lineContent: string): IStringDictionary { - const wordRegexp = inlineDecorators.WORD_REGEXP; +function getWordToLineMap(lineNumber: number, lineContent: string): Map { + const result = new Map(); + const wordRegexp = inlineValues.WORD_REGEXP; wordRegexp.lastIndex = 0; // Reset matching while (true) { @@ -134,26 +120,16 @@ function updateWordRangeMap(wordRangeMap: IStringDictionary, lineNumbe if (!wordMatch) { break; } - const word = wordMatch[0]; - const startOffset = wordMatch.index; - const endOffset = startOffset + word.length; - - const range: IRange = { - startColumn: startOffset + 1, - endColumn: endOffset + 1, - startLineNumber: lineNumber, - endLineNumber: lineNumber - }; - - if (!wordRangeMap[word]) { - wordRangeMap[word] = []; + + if (!result.has(word)) { + result.set(word, []); } - wordRangeMap[word].push(range); + result.get(word).push(lineNumber); } - return wordRangeMap; + return result; } interface MockToken { @@ -182,7 +158,7 @@ function mockLineTokens(lineContent: string): LineTokens { }); } else { - const wordRegexp = inlineDecorators.WORD_REGEXP; + const wordRegexp = inlineValues.WORD_REGEXP; wordRegexp.lastIndex = 0; while (true) {