提交 df2d1d26 编写于 作者: I Isidor Nikolic 提交者: GitHub

Merge pull request #16129 from nojvek/master

inline values as decorators when debugging
......@@ -14,9 +14,11 @@ 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 { IModelDecorationOptions, MouseTargetType, IModelDeltaDecoration, TrackedRangeStickiness, IPosition } from 'vs/editor/common/editorCommon';
import { IRange, 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';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
......@@ -29,9 +31,12 @@ import { RemoveBreakpointAction, EditConditionalBreakpointAction, EnableBreakpoi
import { IDebugEditorContribution, IDebugService, State, IBreakpoint, EDITOR_CONTRIBUTION_ID, CONTEXT_BREAKPOINT_WIDGET_VISIBLE, IStackFrame } 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';
const HOVER_DELAY = 300;
const LAUNCH_JSON_REGEX = /launch\.json$/;
const REMOVE_DECORATORS_DEBOUNCE_INTERVAL = 100; // If we receive a break in this interval, don't reset decorators as it causes a UI flash.
const INLINE_DECORATOR_KEY = 'inlineDecorator';
@editorContribution
export class DebugEditorContribution implements IDebugEditorContribution {
......@@ -45,6 +50,8 @@ export class DebugEditorContribution implements IDebugEditorContribution {
private breakpointHintDecoration: string[];
private breakpointWidget: BreakpointWidget;
private breakpointWidgetVisible: IContextKey<boolean>;
private removeDecorationsTimeoutId = 0;
private editorModelWordRangeMap: IStringDictionary<IRange[]>;
private configurationWidget: FloatingClickWidget;
......@@ -55,6 +62,7 @@ export class DebugEditorContribution implements IDebugEditorContribution {
@IInstantiationService private instantiationService: IInstantiationService,
@IContextKeyService contextKeyService: IContextKeyService,
@ICommandService private commandService: ICommandService,
@ICodeEditorService private codeEditorService: ICodeEditorService,
@ITelemetryService private telemetryService: ITelemetryService
) {
this.breakpointHintDecoration = [];
......@@ -65,6 +73,7 @@ export class DebugEditorContribution implements IDebugEditorContribution {
this.registerListeners();
this.breakpointWidgetVisible = CONTEXT_BREAKPOINT_WIDGET_VISIBLE.bindTo(contextKeyService);
this.updateConfigurationWidgetVisibility();
this.codeEditorService.registerDecorationType(INLINE_DECORATOR_KEY, {});
}
private getContextMenuActions(breakpoint: IBreakpoint, uri: uri, lineNumber: number): TPromise<IAction[]> {
......@@ -200,6 +209,45 @@ export class DebugEditorContribution implements IDebugEditorContribution {
this.editor.updateOptions({ hover: true });
this.hideHoverWidget();
}
this.updateInlineDecorators(sf);
}
private updateInlineDecorators(stackFrame: IStackFrame): void {
// Since step over, step out is a fast continue + break. Continue clears stack.
// This means we'll get a null stackFrame followed quickly by a valid stackFrame.
// Removing all decorators and adding them again causes a noticeable UI flash due to relayout and paint.
// We want to only remove inline decorations if a null stackFrame isn't followed by a valid stackFrame in a short interval.
clearTimeout(this.removeDecorationsTimeoutId);
if (!stackFrame) {
this.removeDecorationsTimeoutId = setTimeout(() => {
this.editor.removeDecorations(INLINE_DECORATOR_KEY);
this.editorModelWordRangeMap = 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;
}
stackFrame.getScopes()
// Get all top level children in the scope chain
.then(scopes => TPromise.join(scopes.map(scope => scope.getChildren())))
.then(children => {
const editorModel = this.editor.getModel();
// Compute name-value map for all variables in scope chain
const expressions = [].concat.apply([], children);
const nameValueMap = getNameValueMapFromScopeChildren(expressions);
// Build wordRangeMap if not already computed for the editor model
if (!this.editorModelWordRangeMap) {
this.editorModelWordRangeMap = getEditorWordRangeMap(editorModel);
}
// Compute decorators from nameValueMap and wordRangeMap and apply to editor
const decorators = getDecorators(nameValueMap, this.editorModelWordRangeMap, editorModel.getLinesContent());
this.editor.setDecorations(INLINE_DECORATOR_KEY, decorators);
});
}
private hideHoverWidget(): void {
......
/*---------------------------------------------------------------------------------------------
* 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<string> {
const nameValueMap: IStringDictionary<string> = 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<string>, wordRangeMap: IStringDictionary<IRange[]>, linesContent: string[]): IDecorationOptions[] {
const linesNames: IStringDictionary<IStringDictionary<boolean>> = 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<string>, 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<IRange[]> {
const wordRangeMap: IStringDictionary<IRange[]> = 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.Comment) {
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;
}
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
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 { 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';
// 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: '"𝄞 ♪ ♫"'
};
suite('Debug - Inline Value Decorators', () => {
test('getNameValueMapFromScopeChildren trims long values', () => {
const expressions = [
createExpression('hello', 'world'),
createExpression('blah', createLongString())
];
const nameValueMap = inlineDecorators.getNameValueMapFromScopeChildren(expressions);
// Ensure blah is capped and ellipses added
assert.deepEqual(nameValueMap, {
hello: 'world',
blah: '"blah blah blah blah blah blah blah blah blah bla…"'
});
});
test('getNameValueMapFromScopeChildren caps scopes to a MAX_NUM_INLINE_VALUES limit', () => {
const scopeChildren: IExpression[][] = new Array(5);
const expectedNameValueMap: IStringDictionary<string> = Object.create(null);
// 10 Stack Frames with a 100 scope expressions each
// JS Global Scope has 700+ expressions so this is close to a real world scenario
for (let i = 0; i < scopeChildren.length; i++) {
const expressions = new Array(50);
for (let j = 0; j < expressions.length; ++j) {
const name = `name${i}.${j}`;
const val = `val${i}.${j}`;
expressions[j] = createExpression(name, val);
if ((i * expressions.length + j) < inlineDecorators.MAX_NUM_INLINE_VALUES) {
expectedNameValueMap[name] = val;
}
}
scopeChildren[i] = expressions;
}
const expressions = [].concat.apply([], scopeChildren);
const nameValueMap = inlineDecorators.getNameValueMapFromScopeChildren(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 expectedDecoratorText = ' everything = {emmet: true, batman: true, legoUniverse: true}, part = "𝄞 ♪ ♫" ';
assert.equal(decorators[0].renderOptions.dark.after.contentText, expectedDecoratorText);
});
test('getEditorWordRangeMap ignores comments and long lines', () => {
const expectedWords = 'function, doit, everything, is, awesome, when, youre, part, of, a, team'.split(', ');
const editorModel = EditorModel.createFromString(`/** Copyright comment */\n \n${testLine}\n// Test comment\n${createLongString()}\n`);
mockEditorModelLineTokens(editorModel);
const wordRangeMap = inlineDecorators.getEditorWordRangeMap(editorModel);
const words = Object.keys(wordRangeMap);
assert.deepEqual(words, expectedWords);
});
});
// Test helpers
function createExpression(name: string, value: string): IExpression {
return {
name,
value,
getId: () => name,
hasChildren: false,
getChildren: null
};
}
function createLongString(): string {
let longStr = '';
for (let i = 0; i < 100; ++i) {
longStr += 'blah blah blah ';
}
return `"${longStr}"`;
}
// 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;
wordRegexp.lastIndex = 0; // Reset matching
while (true) {
const wordMatch = wordRegexp.exec(lineContent);
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] = [];
}
wordRangeMap[word].push(range);
}
return wordRangeMap;
}
interface MockToken {
tokenType: StandardTokenType;
startOffset: number;
endOffset: number;
}
// Simple tokenizer that separates comments from words
function mockLineTokens(lineContent: string): LineTokens {
const tokens: MockToken[] = [];
if (lineContent.match(/^\s*\/(\/|\*)/)) {
tokens.push({
tokenType: StandardTokenType.Comment,
startOffset: 0,
endOffset: lineContent.length
});
}
// Tokenizer should ignore pure whitespace token
else if (lineContent.match(/^\s+$/)) {
tokens.push({
tokenType: StandardTokenType.Other,
startOffset: 0,
endOffset: lineContent.length
});
}
else {
const wordRegexp = inlineDecorators.WORD_REGEXP;
wordRegexp.lastIndex = 0;
while (true) {
const wordMatch = wordRegexp.exec(lineContent);
if (!wordMatch) {
break;
}
tokens.push({
tokenType: StandardTokenType.String,
startOffset: wordMatch.index,
endOffset: wordMatch.index + wordMatch[0].length
});
}
}
return <LineTokens>{
getLineContent: (): string => lineContent,
getTokenCount: (): number => tokens.length,
getTokenStartOffset: (tokenIndex: number): number => tokens[tokenIndex].startOffset,
getTokenEndOffset: (tokenIndex: number): number => tokens[tokenIndex].endOffset,
getStandardTokenType: (tokenIndex: number): StandardTokenType => tokens[tokenIndex].tokenType
};
};
function mockEditorModelLineTokens(editorModel: IModel): void {
const linesContent = editorModel.getLinesContent();
editorModel.getLineTokens = (lineNumber: number): LineTokens => mockLineTokens(linesContent[lineNumber - 1]);
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册