Skip to content

Commit e95380d

Browse files
authored
Contribute rich hover for inline values for debugger + notebook execution values (microsoft#241713)
* show as a language hover * viewport truncation good to go
1 parent f38e3b0 commit e95380d

File tree

2 files changed

+128
-113
lines changed

2 files changed

+128
-113
lines changed

src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ import { ExceptionWidget } from './exceptionWidget.js';
5252
import { CONTEXT_EXCEPTION_WIDGET_VISIBLE, IDebugConfiguration, IDebugEditorContribution, IDebugService, IDebugSession, IExceptionInfo, IExpression, IStackFrame, State } from '../common/debug.js';
5353
import { Expression } from '../common/debugModel.js';
5454
import { IHostService } from '../../../services/host/browser/host.js';
55+
import { MarkdownString } from '../../../../base/common/htmlContent.js';
5556

5657
const MAX_NUM_INLINE_VALUES = 100; // JS Global scope can have 700+ entries. We want to limit ourselves for perf reasons
5758
const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator when debugging. If exceeded ... is added
@@ -73,10 +74,30 @@ class InlineSegment {
7374
}
7475
}
7576

76-
function createInlineValueDecoration(lineNumber: number, contentText: string, column = Constants.MAX_SAFE_SMALL_INTEGER): IModelDeltaDecoration[] {
77-
// If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line
78-
if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) {
79-
contentText = contentText.substring(0, MAX_INLINE_DECORATOR_LENGTH) + '...';
77+
export function formatHoverContent(contentText: string): MarkdownString {
78+
if (contentText.includes('\n') || contentText.includes('\r')) {
79+
// Split by commas in case of multiple variables
80+
const pairs = contentText.split(/,\s*/);
81+
const formattedPairs = pairs.map(pair => {
82+
const equalsIndex = pair.indexOf('=');
83+
if (equalsIndex !== -1) {
84+
const indent = ' '.repeat(equalsIndex + 2);
85+
const [firstLine, ...restLines] = pair.trim().split(/\r?\n/);
86+
return [firstLine, ...restLines.map(line => indent + line)].join('\n');
87+
}
88+
return pair;
89+
});
90+
return new MarkdownString().appendCodeblock('', formattedPairs.join(',\n'));
91+
}
92+
return new MarkdownString().appendCodeblock('', contentText);
93+
}
94+
95+
export function createInlineValueDecoration(lineNumber: number, contentText: string, classNamePrefix: string, column = Constants.MAX_SAFE_SMALL_INTEGER, viewportMaxCol: number = MAX_INLINE_DECORATOR_LENGTH): IModelDeltaDecoration[] {
96+
const rawText = contentText; // store raw text for hover message
97+
98+
// Truncate contentText if it exceeds the viewport max column
99+
if (contentText.length > viewportMaxCol) {
100+
contentText = contentText.substring(0, viewportMaxCol) + '...';
80101
}
81102

82103
return [
@@ -88,7 +109,7 @@ function createInlineValueDecoration(lineNumber: number, contentText: string, co
88109
endColumn: column
89110
},
90111
options: {
91-
description: 'debug-inline-value-decoration-spacer',
112+
description: `${classNamePrefix}-inline-value-decoration-spacer`,
92113
after: {
93114
content: strings.noBreakWhitespace,
94115
cursorStops: InjectedTextCursorStops.None
@@ -104,14 +125,15 @@ function createInlineValueDecoration(lineNumber: number, contentText: string, co
104125
endColumn: column
105126
},
106127
options: {
107-
description: 'debug-inline-value-decoration',
128+
description: `${classNamePrefix}-inline-value-decoration`,
108129
after: {
109130
content: replaceWsWithNoBreakWs(contentText),
110-
inlineClassName: 'debug-inline-value',
131+
inlineClassName: `${classNamePrefix}-inline-value`,
111132
inlineClassNameAffectsLetterSpacing: true,
112133
cursorStops: InjectedTextCursorStops.None
113134
},
114135
showIfCollapsed: true,
136+
hoverMessage: formatHoverContent(rawText)
115137
}
116138
},
117139
];
@@ -769,7 +791,10 @@ export class DebugEditorContribution implements IDebugEditorContribution {
769791
if (segments.length > 0) {
770792
segments = segments.sort((a, b) => a.column - b.column);
771793
const text = segments.map(s => s.text).join(separator);
772-
allDecorations.push(...createInlineValueDecoration(line, text));
794+
const editorWidth = this.editor.getLayoutInfo().width;
795+
const fontInfo = this.editor.getOption(EditorOption.fontInfo);
796+
const viewportMaxCol = Math.floor((editorWidth - 50) / fontInfo.typicalHalfwidthCharacterWidth);
797+
allDecorations.push(...createInlineValueDecoration(line, text, 'debug', undefined, viewportMaxCol));
773798
}
774799
});
775800

@@ -813,9 +838,13 @@ export class DebugEditorContribution implements IDebugEditorContribution {
813838
}
814839
}
815840

816-
allDecorations = [...valuesPerLine.entries()].flatMap(([line, values]) =>
817-
createInlineValueDecoration(line, [...values].map(([n, v]) => `${n} = ${v}`).join(', '))
818-
);
841+
allDecorations = [...valuesPerLine.entries()].flatMap(([line, values]) => {
842+
const text = [...values].map(([n, v]) => `${n} = ${v}`).join(', ');
843+
const editorWidth = this.editor.getLayoutInfo().width;
844+
const fontInfo = this.editor.getOption(EditorOption.fontInfo);
845+
const viewportMaxCol = Math.floor((editorWidth - 50) / fontInfo.typicalHalfwidthCharacterWidth);
846+
return createInlineValueDecoration(line, text, 'debug', undefined, viewportMaxCol);
847+
});
819848
}
820849

821850
if (cts.token.isCancellationRequested) {

src/vs/workbench/contrib/notebook/browser/contrib/notebookVariables/notebookInlineVariables.ts

Lines changed: 88 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -9,28 +9,25 @@ import { Event } from '../../../../../../base/common/event.js';
99
import { Disposable, IDisposable } from '../../../../../../base/common/lifecycle.js';
1010
import { ResourceMap } from '../../../../../../base/common/map.js';
1111
import { isEqual } from '../../../../../../base/common/resources.js';
12-
import { format, noBreakWhitespace } from '../../../../../../base/common/strings.js';
13-
import { Constants } from '../../../../../../base/common/uint.js';
12+
import { format } from '../../../../../../base/common/strings.js';
1413
import { Position } from '../../../../../../editor/common/core/position.js';
1514
import { Range } from '../../../../../../editor/common/core/range.js';
1615
import { InlineValueContext, InlineValueText, InlineValueVariableLookup } from '../../../../../../editor/common/languages.js';
17-
import { IModelDeltaDecoration, InjectedTextCursorStops, ITextModel } from '../../../../../../editor/common/model.js';
16+
import { IModelDeltaDecoration, ITextModel } from '../../../../../../editor/common/model.js';
1817
import { ILanguageFeaturesService } from '../../../../../../editor/common/services/languageFeatures.js';
1918
import { localize } from '../../../../../../nls.js';
2019
import { registerAction2 } from '../../../../../../platform/actions/common/actions.js';
2120
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
2221
import { ServicesAccessor } from '../../../../../../platform/instantiation/common/instantiation.js';
22+
import { createInlineValueDecoration } from '../../../../debug/browser/debugEditorContribution.js';
2323
import { IDebugService, State } from '../../../../debug/common/debug.js';
2424
import { NotebookSetting } from '../../../common/notebookCommon.js';
2525
import { ICellExecutionStateChangedEvent, INotebookExecutionStateService, NotebookExecutionType } from '../../../common/notebookExecutionStateService.js';
26-
import { INotebookKernelMatchResult, INotebookKernelService, VariablesResult } from '../../../common/notebookKernelService.js';
26+
import { INotebookKernelService, VariablesResult } from '../../../common/notebookKernelService.js';
2727
import { INotebookActionContext, NotebookAction } from '../../controller/coreActions.js';
2828
import { ICellViewModel, INotebookEditor, INotebookEditorContribution } from '../../notebookBrowser.js';
2929
import { registerNotebookContribution } from '../../notebookEditorExtensions.js';
3030

31-
// value from debug, may need to keep an eye on and shorter to account for cells having a narrower viewport width
32-
const MAX_INLINE_DECORATOR_LENGTH = 150; // Max string length of each inline decorator. If exceeded ... is added
33-
3431
class InlineSegment {
3532
constructor(public column: number, public text: string) {
3633
}
@@ -136,57 +133,61 @@ export class NotebookInlineVariablesController extends Disposable implements INo
136133
const fullCellRange = new Range(1, 1, lastLine, lastColumn);
137134

138135
const promises = providers.flatMap(provider => Promise.resolve(provider.provideInlineValues(model, fullCellRange, ctx, token)).then(async (result) => {
139-
if (result) {
136+
if (!result) {
137+
return;
138+
}
140139

141-
let kernel: INotebookKernelMatchResult;
142-
const kernelVars: VariablesResult[] = [];
143-
if (result.some(iv => iv.type === 'variable')) { // if anyone will need a lookup, get vars now to avoid needing to do it multiple times
144-
if (!this.notebookEditor.hasModel()) {
145-
return; // should not happen, a cell will be executed
146-
}
147-
kernel = this.notebookKernelService.getMatchingKernel(this.notebookEditor.textModel);
148-
const variables = kernel.selected?.provideVariables(event.notebook, undefined, 'named', 0, token);
149-
if (!variables) {
150-
return;
151-
}
140+
const notebook = this.notebookEditor.textModel;
141+
if (!notebook) {
142+
return;
143+
}
144+
145+
const kernel = this.notebookKernelService.getMatchingKernel(notebook);
146+
const kernelVars: VariablesResult[] = [];
147+
if (result.some(iv => iv.type === 'variable')) { // if anyone will need a lookup, get vars now to avoid needing to do it multiple times
148+
if (!this.notebookEditor.hasModel()) {
149+
return; // should not happen, a cell will be executed
150+
}
151+
const variables = kernel.selected?.provideVariables(event.notebook, undefined, 'named', 0, token);
152+
if (variables) {
152153
for await (const v of variables) {
153154
kernelVars.push(v);
154155
}
155156
}
157+
}
156158

157-
for (const iv of result) {
158-
let text: string | undefined = undefined;
159-
switch (iv.type) {
160-
case 'text':
161-
text = (iv as InlineValueText).text;
162-
break;
163-
case 'variable': {
164-
const name = (iv as InlineValueVariableLookup).variableName;
165-
if (!name) {
166-
continue; // skip to next var, no valid name to lookup with
167-
}
168-
const value = kernelVars.find(v => v.name === name)?.value;
169-
if (!value) {
170-
continue;
171-
}
172-
text = format('{0} = {1}', name, value);
173-
break;
159+
for (const iv of result) {
160+
let text: string | undefined = undefined;
161+
switch (iv.type) {
162+
case 'text':
163+
text = (iv as InlineValueText).text;
164+
break;
165+
case 'variable': {
166+
const name = (iv as InlineValueVariableLookup).variableName;
167+
if (!name) {
168+
continue; // skip to next var, no valid name to lookup with
174169
}
175-
case 'expression': {
176-
continue; // no active debug session, so evaluate would break
170+
const value = kernelVars.find(v => v.name === name)?.value;
171+
if (!value) {
172+
continue;
177173
}
174+
text = format('{0} = {1}', name, value);
175+
break;
178176
}
177+
case 'expression': {
178+
continue; // no active debug session, so evaluate would break
179+
}
180+
}
179181

180-
if (text) {
181-
const line = iv.range.startLineNumber;
182-
let lineSegments = lineDecorations.get(line);
183-
if (!lineSegments) {
184-
lineSegments = [];
185-
lineDecorations.set(line, lineSegments);
186-
}
187-
if (!lineSegments.some(iv => iv.text === text)) { // de-dupe
188-
lineSegments.push(new InlineSegment(iv.range.startColumn, text));
189-
}
182+
if (text) {
183+
const line = iv.range.startLineNumber;
184+
let lineSegments = lineDecorations.get(line);
185+
if (!lineSegments) {
186+
lineSegments = [];
187+
lineDecorations.set(line, lineSegments);
188+
}
189+
if (!lineSegments.some(iv => iv.text === text)) { // de-dupe
190+
lineSegments.push(new InlineSegment(iv.range.startColumn, text));
190191
}
191192
}
192193
}
@@ -199,10 +200,18 @@ export class NotebookInlineVariablesController extends Disposable implements INo
199200
// sort line segments and concatenate them into a decoration
200201
lineDecorations.forEach((segments, line) => {
201202
if (segments.length > 0) {
202-
segments = segments.sort((a, b) => a.column - b.column);
203+
segments.sort((a, b) => a.column - b.column);
203204
const text = segments.map(s => s.text).join(', ');
204-
inlineDecorations.push(...this.createNotebookInlineValueDecoration(line, text));
205-
205+
const editorWidth = cell.layoutInfo.editorWidth;
206+
const fontInfo = cell.layoutInfo.fontInfo;
207+
if (fontInfo && cell.textModel) {
208+
const base = Math.floor((editorWidth - 50) / fontInfo.typicalHalfwidthCharacterWidth);
209+
const lineLength = cell.textModel.getLineLength(line);
210+
const available = Math.max(0, base - lineLength);
211+
inlineDecorations.push(...createInlineValueDecoration(line, text, 'nb', undefined, available));
212+
} else {
213+
inlineDecorations.push(...createInlineValueDecoration(line, text, 'nb'));
214+
}
206215
}
207216
});
208217

@@ -231,6 +240,7 @@ export class NotebookInlineVariablesController extends Disposable implements INo
231240
const processedVars = new Set<string>();
232241

233242
const functionRanges = this.getFunctionRanges(document);
243+
const lineDecorations = new Map<number, InlineSegment[]>();
234244

235245
// For each variable name found in the kernel results
236246
for (const varName of varNames) {
@@ -270,12 +280,38 @@ export class NotebookInlineVariablesController extends Disposable implements INo
270280

271281
if (lastMatchOutsideFunction) {
272282
const inlineVal = varName + ' = ' + vars.find(v => v.name === varName)?.value;
273-
inlineDecorations.push(...this.createNotebookInlineValueDecoration(lastMatchOutsideFunction.line, inlineVal));
283+
284+
let lineSegments = lineDecorations.get(lastMatchOutsideFunction.line);
285+
if (!lineSegments) {
286+
lineSegments = [];
287+
lineDecorations.set(lastMatchOutsideFunction.line, lineSegments);
288+
}
289+
if (!lineSegments.some(iv => iv.text === inlineVal)) { // de-dupe
290+
lineSegments.push(new InlineSegment(lastMatchOutsideFunction.column, inlineVal));
291+
}
274292
}
275293

276294
processedVars.add(varName);
277295
}
278296

297+
// sort line segments and concatenate them into a decoration
298+
lineDecorations.forEach((segments, line) => {
299+
if (segments.length > 0) {
300+
segments.sort((a, b) => a.column - b.column);
301+
const text = segments.map(s => s.text).join(', ');
302+
const editorWidth = cell.layoutInfo.editorWidth;
303+
const fontInfo = cell.layoutInfo.fontInfo;
304+
if (fontInfo && cell.textModel) {
305+
const base = Math.floor((editorWidth - 50) / fontInfo.typicalHalfwidthCharacterWidth);
306+
const lineLength = cell.textModel.getLineLength(line);
307+
const available = Math.max(0, base - lineLength);
308+
inlineDecorations.push(...createInlineValueDecoration(line, text, 'nb', undefined, available));
309+
} else {
310+
inlineDecorations.push(...createInlineValueDecoration(line, text, 'nb'));
311+
}
312+
}
313+
});
314+
279315
if (inlineDecorations.length > 0) {
280316
this.updateCellInlineDecorations(cell, inlineDecorations);
281317
this.initCellContentListener(cell);
@@ -421,55 +457,6 @@ export class NotebookInlineVariablesController extends Disposable implements INo
421457
this._clearNotebookInlineDecorations();
422458
}
423459

424-
// taken from /src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts
425-
private createNotebookInlineValueDecoration(lineNumber: number, contentText: string, column = Constants.MAX_SAFE_SMALL_INTEGER): IModelDeltaDecoration[] {
426-
// If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line
427-
if (contentText.length > MAX_INLINE_DECORATOR_LENGTH) {
428-
contentText = contentText.substring(0, MAX_INLINE_DECORATOR_LENGTH) + '...';
429-
}
430-
431-
return [
432-
{
433-
range: {
434-
startLineNumber: lineNumber,
435-
endLineNumber: lineNumber,
436-
startColumn: column,
437-
endColumn: column
438-
},
439-
options: {
440-
description: 'nb-inline-value-decoration-spacer',
441-
after: {
442-
content: noBreakWhitespace,
443-
cursorStops: InjectedTextCursorStops.None
444-
},
445-
showIfCollapsed: true,
446-
}
447-
},
448-
{
449-
range: {
450-
startLineNumber: lineNumber,
451-
endLineNumber: lineNumber,
452-
startColumn: column,
453-
endColumn: column
454-
},
455-
options: {
456-
description: 'nb-inline-value-decoration',
457-
after: {
458-
content: this.replaceWsWithNoBreakWs(contentText),
459-
inlineClassName: 'nb-inline-value',
460-
inlineClassNameAffectsLetterSpacing: true,
461-
cursorStops: InjectedTextCursorStops.None
462-
},
463-
showIfCollapsed: true,
464-
}
465-
},
466-
];
467-
}
468-
469-
private replaceWsWithNoBreakWs(str: string): string {
470-
return str.replace(/[ \t]/g, noBreakWhitespace);
471-
}
472-
473460
override dispose(): void {
474461
super.dispose();
475462
this._clearNotebookInlineDecorations();
@@ -478,7 +465,6 @@ export class NotebookInlineVariablesController extends Disposable implements INo
478465
}
479466
}
480467

481-
482468
registerNotebookContribution(NotebookInlineVariablesController.id, NotebookInlineVariablesController);
483469

484470
registerAction2(class ClearNotebookInlineValues extends NotebookAction {

0 commit comments

Comments
 (0)