Skip to content

Commit c837b6c

Browse files
Akos Kittakittaakos
Akos Kitta
authored andcommitted
GH-7909: Inline variable values in editor.
Closes #7909. Signed-off-by: Akos Kitta <[email protected]>
1 parent 3eb0678 commit c837b6c

File tree

8 files changed

+358
-4
lines changed

8 files changed

+358
-4
lines changed

packages/debug/src/browser/console/debug-console-items.tsx

+12
Original file line numberDiff line numberDiff line change
@@ -359,4 +359,16 @@ export class DebugScope extends ExpressionContainer {
359359
return this.raw.name;
360360
}
361361

362+
get expensive(): boolean {
363+
return this.raw.expensive;
364+
}
365+
366+
get range(): monaco.Range | undefined {
367+
const { line, column, endLine, endColumn } = this.raw;
368+
if (line !== undefined && column !== undefined && endLine !== undefined && endColumn !== undefined) {
369+
return new monaco.Range(line, column, endLine, endColumn);
370+
}
371+
return undefined;
372+
}
373+
362374
}

packages/debug/src/browser/debug-frontend-module.ts

+4
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { ColorContribution } from '@theia/core/lib/browser/color-application-con
5555
import { DebugWatchManager } from './debug-watch-manager';
5656
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
5757
import { DebugBreakpointWidget } from './editor/debug-breakpoint-widget';
58+
import { DebugInlineValueDecorator } from './editor/debug-inline-value-decorator';
5859

5960
export default new ContainerModule((bind: interfaces.Bind) => {
6061
bind(DebugCallStackItemTypeKey).toDynamicValue(({ container }) =>
@@ -86,6 +87,9 @@ export default new ContainerModule((bind: interfaces.Bind) => {
8687
bind(DebugSchemaUpdater).toSelf().inSingletonScope();
8788
bind(DebugConfigurationManager).toSelf().inSingletonScope();
8889

90+
bind(DebugInlineValueDecorator).toSelf().inSingletonScope();
91+
bind(FrontendApplicationContribution).toService(DebugInlineValueDecorator);
92+
8993
bind(DebugService).toDynamicValue(context => WebSocketConnectionProvider.createProxy(context.container, DebugPath)).inSingletonScope();
9094
bind(DebugResourceResolver).toSelf().inSingletonScope();
9195
bind(ResourceResolver).toService(DebugResourceResolver);

packages/debug/src/browser/debug-preferences.ts

+6
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@ export const debugPreferencesSchema: PreferenceSchema = {
3939
enum: ['neverOpen', 'openOnSessionStart', 'openOnFirstSessionStart'],
4040
default: 'openOnFirstSessionStart',
4141
description: 'Controls when the internal debug console should open.'
42+
},
43+
'debug.inlineValues': {
44+
type: 'boolean',
45+
default: false,
46+
description: 'Show variable values inline in editor while debugging.'
4247
}
4348
}
4449
};
@@ -48,6 +53,7 @@ export class DebugConfiguration {
4853
'debug.debugViewLocation': 'default' | 'left' | 'right' | 'bottom';
4954
'debug.openDebug': 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart' | 'openOnDebugBreak';
5055
'debug.internalConsoleOptions': 'neverOpen' | 'openOnSessionStart' | 'openOnFirstSessionStart';
56+
'debug.inlineValues': boolean;
5157
}
5258

5359
export const DebugPreferences = Symbol('DebugPreferences');

packages/debug/src/browser/editor/debug-editor-model.ts

+23-3
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { DebugHoverWidget, createDebugHoverWidgetContainer } from './debug-hover
2828
import { DebugBreakpointWidget } from './debug-breakpoint-widget';
2929
import { DebugExceptionWidget } from './debug-exception-widget';
3030
import { DebugProtocol } from 'vscode-debugprotocol';
31+
import { DebugInlineValueDecorator, INLINE_VALUE_DECORATION_KEY } from './debug-inline-value-decorator';
3132

3233
export const DebugEditorModelFactory = Symbol('DebugEditorModelFactory');
3334
export type DebugEditorModelFactory = (editor: DebugEditor) => DebugEditorModel;
@@ -83,6 +84,9 @@ export class DebugEditorModel implements Disposable {
8384
@inject(DebugExceptionWidget)
8485
readonly exceptionWidget: DebugExceptionWidget;
8586

87+
@inject(DebugInlineValueDecorator)
88+
readonly inlineValueDecorator: DebugInlineValueDecorator;
89+
8690
@postConstruct()
8791
protected init(): void {
8892
this.uri = new URI(this.editor.getControl().getModel()!.uri.toString());
@@ -94,6 +98,7 @@ export class DebugEditorModel implements Disposable {
9498
this.editor.getControl().onMouseMove(event => this.handleMouseMove(event)),
9599
this.editor.getControl().onMouseLeave(event => this.handleMouseLeave(event)),
96100
this.editor.getControl().onKeyDown(() => this.hover.hide({ immediate: false })),
101+
this.editor.getControl().onDidChangeModelContent(() => this.renderFrames()),
97102
this.editor.getControl().getModel()!.onDidChangeDecorations(() => this.updateBreakpoints()),
98103
this.sessions.onDidChange(() => this.renderFrames())
99104
]);
@@ -105,14 +110,29 @@ export class DebugEditorModel implements Disposable {
105110
this.toDispose.dispose();
106111
}
107112

108-
protected readonly renderFrames = debounce(() => {
113+
protected readonly renderFrames = debounce(async () => {
109114
if (this.toDispose.disposed) {
110115
return;
111116
}
112117
this.toggleExceptionWidget();
113-
const decorations = this.createFrameDecorations();
114-
this.frameDecorations = this.deltaDecorations(this.frameDecorations, decorations);
118+
const [newFrameDecorations, inlineValueDecorations] = await Promise.all([
119+
this.createFrameDecorations(),
120+
this.createInlineValueDecorations()
121+
]);
122+
const codeEditor = this.editor.getControl();
123+
codeEditor.removeDecorations(INLINE_VALUE_DECORATION_KEY);
124+
codeEditor.setDecorations(INLINE_VALUE_DECORATION_KEY, inlineValueDecorations);
125+
this.frameDecorations = this.deltaDecorations(this.frameDecorations, newFrameDecorations);
115126
}, 100);
127+
128+
protected async createInlineValueDecorations(): Promise<monaco.editor.IDecorationOptions[]> {
129+
const { currentFrame } = this.sessions;
130+
if (!currentFrame || !currentFrame.source || currentFrame.source.uri.toString() !== this.uri.toString()) {
131+
return [];
132+
}
133+
return this.inlineValueDecorator.calculateDecorations(this, currentFrame);
134+
}
135+
116136
protected createFrameDecorations(): monaco.editor.IModelDeltaDecoration[] {
117137
const decorations: monaco.editor.IModelDeltaDecoration[] = [];
118138
const { currentFrame, topFrame } = this.sessions;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/********************************************************************************
2+
* Copyright (C) 2020 TypeFox and others.
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License v. 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0.
7+
*
8+
* This Source Code may also be made available under the following Secondary
9+
* Licenses when the conditions for such availability set forth in the Eclipse
10+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
11+
* with the GNU Classpath Exception which is available at
12+
* https://www.gnu.org/software/classpath/license.html.
13+
*
14+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15+
********************************************************************************/
16+
17+
/*---------------------------------------------------------------------------------------------
18+
* Copyright (c) Microsoft Corporation. All rights reserved.
19+
* Licensed under the MIT License. See License.txt in the project root for license information.
20+
*--------------------------------------------------------------------------------------------*/
21+
// Based on https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts
22+
23+
import { inject, injectable } from 'inversify';
24+
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
25+
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
26+
import { ExpressionContainer, DebugVariable } from '../console/debug-console-items';
27+
import { DebugPreferences } from '../debug-preferences';
28+
import { DebugEditorModel } from './debug-editor-model';
29+
import { DebugStackFrame } from '../model/debug-stack-frame';
30+
31+
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L40-L43
32+
export const INLINE_VALUE_DECORATION_KEY = 'inlinevaluedecoration';
33+
const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We want to limit ourselves for perf reasons
34+
const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added
35+
const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline values for the line are skipped
36+
const { DEFAULT_WORD_REGEXP } = monaco.wordHelper;
37+
38+
/**
39+
* MAX SMI (SMall Integer) as defined in v8.
40+
* one bit is lost for boxing/unboxing flag.
41+
* one bit is lost for sign flag.
42+
* See https://thibaultlaurens.github.io/javascript/2013/04/29/how-the-v8-engine-works/#tagged-values
43+
*/
44+
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/base/common/uint.ts#L7-L13
45+
const MAX_SAFE_SMALL_INTEGER = 1 << 30;
46+
47+
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/editor/common/modes.ts#L88-L97
48+
const enum StandardTokenType {
49+
Other = 0,
50+
Comment = 1,
51+
String = 2,
52+
RegEx = 4
53+
};
54+
55+
@injectable()
56+
export class DebugInlineValueDecorator implements FrontendApplicationContribution {
57+
58+
@inject(MonacoEditorService)
59+
protected readonly editorService: MonacoEditorService;
60+
61+
@inject(DebugPreferences)
62+
protected readonly preferences: DebugPreferences;
63+
64+
protected enabled = false;
65+
protected wordToLineNumbersMap: Map<string, monaco.Position[]> | undefined = new Map(); // TODO: can we get rid of this field?
66+
67+
onStart(): void {
68+
this.editorService.registerDecorationType(INLINE_VALUE_DECORATION_KEY, {});
69+
this.enabled = !!this.preferences['debug.inlineValues'];
70+
this.preferences.onPreferenceChanged(({ preferenceName, newValue }) => {
71+
if (preferenceName === 'debug.inlineValues' && !!newValue !== this.enabled) {
72+
this.enabled = !!newValue;
73+
}
74+
});
75+
}
76+
77+
async calculateDecorations(debugEditorModel: DebugEditorModel, stackFrame: DebugStackFrame | undefined): Promise<monaco.editor.IDecorationOptions[]> {
78+
this.wordToLineNumbersMap = undefined;
79+
const model = debugEditorModel.editor.getControl().getModel() || undefined;
80+
return this.updateInlineValueDecorations(model, stackFrame);
81+
}
82+
83+
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L382-L408
84+
protected async updateInlineValueDecorations(
85+
model: monaco.editor.ITextModel | undefined,
86+
stackFrame: DebugStackFrame | undefined): Promise<monaco.editor.IDecorationOptions[]> {
87+
88+
if (!this.enabled || !model || !stackFrame || !stackFrame.source || model.uri.toString() !== stackFrame.source.uri.toString()) {
89+
return [];
90+
}
91+
92+
// XXX: Here is a difference between the VS Code's `IStackFrame` and the `DebugProtocol.StackFrame`.
93+
// In DAP, `source` is optional, hence `range` is optional too.
94+
const { range: stackFrameRange } = stackFrame;
95+
if (!stackFrameRange) {
96+
return [];
97+
}
98+
99+
const scopes = await stackFrame.getMostSpecificScopes(stackFrameRange);
100+
// Get all top level children in the scope chain
101+
const decorationsPerScope = await Promise.all(scopes.map(async scope => {
102+
const children = Array.from(await scope.getElements());
103+
let range = new monaco.Range(0, 0, stackFrameRange.startLineNumber, stackFrameRange.startColumn);
104+
if (scope.range) {
105+
range = range.setStartPosition(scope.range.startLineNumber, scope.range.startColumn);
106+
}
107+
108+
return this.createInlineValueDecorationsInsideRange(children, range, model);
109+
}));
110+
111+
return decorationsPerScope.reduce((previous, current) => previous.concat(current), []);
112+
}
113+
114+
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L410-L452
115+
private createInlineValueDecorationsInsideRange(
116+
expressions: ReadonlyArray<ExpressionContainer>,
117+
range: monaco.Range,
118+
model: monaco.editor.ITextModel): monaco.editor.IDecorationOptions[] {
119+
120+
const nameValueMap = new Map<string, string>();
121+
for (const expr of expressions) {
122+
if (expr instanceof DebugVariable) { // XXX: VS Code uses `IExpression` that has `name` and `value`.
123+
nameValueMap.set(expr.name, expr.value);
124+
}
125+
// Limit the size of map. Too large can have a perf impact
126+
if (nameValueMap.size >= MAX_NUM_INLINE_VALUES) {
127+
break;
128+
}
129+
}
130+
131+
const lineToNamesMap: Map<number, string[]> = new Map<number, string[]>();
132+
const wordToPositionsMap = this.getWordToPositionsMap(model);
133+
134+
// Compute unique set of names on each line
135+
nameValueMap.forEach((_, name) => {
136+
const positions = wordToPositionsMap.get(name);
137+
if (positions) {
138+
for (const position of positions) {
139+
if (range.containsPosition(position)) {
140+
if (!lineToNamesMap.has(position.lineNumber)) {
141+
lineToNamesMap.set(position.lineNumber, []);
142+
}
143+
144+
if (lineToNamesMap.get(position.lineNumber)!.indexOf(name) === -1) {
145+
lineToNamesMap.get(position.lineNumber)!.push(name);
146+
}
147+
}
148+
}
149+
}
150+
});
151+
152+
const decorations: monaco.editor.IDecorationOptions[] = [];
153+
// Compute decorators for each line
154+
lineToNamesMap.forEach((names, line) => {
155+
const contentText = names.sort((first, second) => {
156+
const content = model.getLineContent(line);
157+
return content.indexOf(first) - content.indexOf(second);
158+
}).map(name => `${name} = ${nameValueMap.get(name)}`).join(', ');
159+
decorations.push(this.createInlineValueDecoration(line, contentText));
160+
});
161+
162+
return decorations;
163+
}
164+
165+
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L454-L485
166+
private createInlineValueDecoration(lineNumber: number, contentText: string): monaco.editor.IDecorationOptions {
167+
// If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line
168+
if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) {
169+
contentText = contentText.substr(0, MAX_INLINE_DECORATOR_LENGTH) + '...';
170+
}
171+
172+
return {
173+
color: undefined, // XXX: check inconsistency between APIs. `color` seems to be mandatory from `monaco-editor-core`.
174+
range: {
175+
startLineNumber: lineNumber,
176+
endLineNumber: lineNumber,
177+
startColumn: MAX_SAFE_SMALL_INTEGER,
178+
endColumn: MAX_SAFE_SMALL_INTEGER
179+
},
180+
renderOptions: {
181+
after: {
182+
contentText,
183+
backgroundColor: 'rgba(255, 200, 0, 0.2)',
184+
margin: '10px'
185+
},
186+
dark: {
187+
after: {
188+
color: 'rgba(255, 255, 255, 0.5)',
189+
}
190+
},
191+
light: {
192+
after: {
193+
color: 'rgba(0, 0, 0, 0.5)',
194+
}
195+
}
196+
}
197+
};
198+
}
199+
200+
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L487-L531
201+
private getWordToPositionsMap(model: monaco.editor.ITextModel): Map<string, monaco.Position[]> {
202+
if (!this.wordToLineNumbersMap) {
203+
this.wordToLineNumbersMap = new Map<string, monaco.Position[]>();
204+
if (!model) {
205+
return this.wordToLineNumbersMap;
206+
}
207+
208+
// For every word in every line, map its ranges for fast lookup
209+
for (let lineNumber = 1, len = model.getLineCount(); lineNumber <= len; ++lineNumber) {
210+
const lineContent = model.getLineContent(lineNumber);
211+
212+
// If line is too long then skip the line
213+
if (lineContent.length > MAX_TOKENIZATION_LINE_LEN) {
214+
continue;
215+
}
216+
217+
model.forceTokenization(lineNumber);
218+
const lineTokens = model.getLineTokens(lineNumber);
219+
for (let tokenIndex = 0, tokenCount = lineTokens.getCount(); tokenIndex < tokenCount; tokenIndex++) {
220+
const tokenStartOffset = lineTokens.getStartOffset(tokenIndex);
221+
const tokenEndOffset = lineTokens.getEndOffset(tokenIndex);
222+
const tokenType = lineTokens.getStandardTokenType(tokenIndex);
223+
const tokenStr = lineContent.substring(tokenStartOffset, tokenEndOffset);
224+
225+
// Token is a word and not a comment
226+
if (tokenType === StandardTokenType.Other) {
227+
DEFAULT_WORD_REGEXP.lastIndex = 0; // We assume tokens will usually map 1:1 to words if they match
228+
const wordMatch = DEFAULT_WORD_REGEXP.exec(tokenStr);
229+
230+
if (wordMatch) {
231+
const word = wordMatch[0];
232+
if (!this.wordToLineNumbersMap.has(word)) {
233+
this.wordToLineNumbersMap.set(word, []);
234+
}
235+
236+
this.wordToLineNumbersMap.get(word)!.push(new monaco.Position(lineNumber, tokenStartOffset));
237+
}
238+
}
239+
}
240+
}
241+
}
242+
243+
return this.wordToLineNumbersMap;
244+
}
245+
246+
}

0 commit comments

Comments
 (0)