提交 db334b22 编写于 作者: I isidor

debug: inline values use map, set. Polish

上级 07b83624
......@@ -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<boolean>;
private removeDecorationsTimeoutId = 0;
private editorModelWordRangeMap: IStringDictionary<IRange[]>;
private wordToLineNumbersMap: Map<string, number[]>;
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);
});
}
......
......@@ -2,10 +2,8 @@
* 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 { IDecorationOptions, IModel } from 'vs/editor/common/editorCommon';
import { StandardTokenType } from 'vs/editor/common/modes';
import { IExpression } from 'vs/workbench/parts/debug/common/debug';
......@@ -13,23 +11,22 @@ export const MAX_INLINE_VALUE_LENGTH = 50; // Max string length of each inline '
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<string> {
const nameValueMap: IStringDictionary<string> = Object.create(null);
export function toNameValueMap(expressions: IExpression[]): Map<string, string> {
const result = new Map<string, string>();
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];
value = value.substr(0, MAX_INLINE_VALUE_LENGTH) + '' + value[value.length - 1];
}
nameValueMap[expr.name] = value;
result.set(expr.name, value);
// Limit the size of map. Too large can have a perf impact
if (++valueCount >= MAX_NUM_INLINE_VALUES) {
......@@ -37,60 +34,53 @@ export function getNameValueMapFromScopeChildren(expressions: IExpression[]): IS
}
}
return nameValueMap;
return result;
}
export function getDecorators(nameValueMap: IStringDictionary<string>, wordRangeMap: IStringDictionary<IRange[]>, linesContent: string[]): IDecorationOptions[] {
const linesNames: IStringDictionary<IStringDictionary<boolean>> = Object.create(null);
const names = Object.keys(nameValueMap);
const decorators: IDecorationOptions[] = [];
export function getDecorations(nameValueMap: Map<string, string>, wordToLineNumbersMap: Map<string, number[]>): IDecorationOptions[] {
const lineToNamesMap: Map<number, string[]> = new Map<number, string[]>();
const decorations: 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);
nameValueMap.forEach((value, name) => {
if (wordToLineNumbersMap.has(name)) {
for (let lineNumber of wordToLineNumbersMap.get(name)) {
if (!lineToNamesMap.has(lineNumber)) {
lineToNamesMap.set(lineNumber, []);
}
linesNames[lineNum][name] = true;
lineToNamesMap.get(lineNumber).push(name);
}
}
}
});
// 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);
}
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 decorators;
return decorations;
}
export function getDecoratorFromNames(lineNumber: number, names: string[], nameValueMap: IStringDictionary<string>, linesContent: string[]): IDecorationOptions {
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)';
const lineLength = linesContent[lineNumber - 1].length;
// Wrap with 1em unicode space for readability
let contentText = '\u2003' + names.map(n => `${n} = ${nameValueMap[n]}`).join(', ') + '\u2003';
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 - ELLIPSES.length) + ELLIPSES;
contentText = contentText.substr(0, MAX_INLINE_DECORATOR_LENGTH) + '...';
}
const decorator: IDecorationOptions = {
return {
range: {
startLineNumber: lineNumber,
endLineNumber: lineNumber,
startColumn: lineLength,
endColumn: lineLength + 1
startColumn: Number.MAX_VALUE,
endColumn: Number.MAX_VALUE
},
renderOptions: {
dark: {
......@@ -111,56 +101,40 @@ export function getDecoratorFromNames(lineNumber: number, names: string[], nameV
}
}
};
return decorator;
}
export function getEditorWordRangeMap(editorModel: IModel): IStringDictionary<IRange[]> {
const wordRangeMap: IStringDictionary<IRange[]> = Object.create(null);
const linesContent = editorModel.getLinesContent();
export function getWordToLineNumbersMap(model: IModel): Map<string, number[]> {
const result = new Map<string, number[]>();
// 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];
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 = 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);
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 (lineTokens.getStandardTokenType(j) === StandardTokenType.Other) {
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];
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] = [];
if (!result.has(word)) {
result.set(word, []);
}
wordRangeMap[word].push(range);
result.get(word).push(lineNumber);
}
}
}
}
return wordRangeMap;
return result;
}
......@@ -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<string, string>();
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<IRange[]>, lineNumber: number, lineContent: string): IStringDictionary<IRange[]> {
const wordRegexp = inlineDecorators.WORD_REGEXP;
function getWordToLineMap(lineNumber: number, lineContent: string): Map<string, number[]> {
const result = new Map<string, number[]>();
const wordRegexp = inlineValues.WORD_REGEXP;
wordRegexp.lastIndex = 0; // Reset matching
while (true) {
......@@ -134,26 +120,16 @@ function updateWordRangeMap(wordRangeMap: IStringDictionary<IRange[]>, 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) {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册