Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/debug/browser/debugHover.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as dom from '../../../../base/browser/dom.js';
7
import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
8
import { IMouseEvent } from '../../../../base/browser/mouseEvent.js';
9
import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';
10
import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';
11
import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';
12
import { AsyncDataTree } from '../../../../base/browser/ui/tree/asyncDataTree.js';
13
import { ITreeContextMenuEvent } from '../../../../base/browser/ui/tree/tree.js';
14
import { coalesce } from '../../../../base/common/arrays.js';
15
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
16
import { KeyCode } from '../../../../base/common/keyCodes.js';
17
import * as lifecycle from '../../../../base/common/lifecycle.js';
18
import { clamp } from '../../../../base/common/numbers.js';
19
import { isMacintosh } from '../../../../base/common/platform.js';
20
import { ScrollbarVisibility } from '../../../../base/common/scrollable.js';
21
import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js';
22
import { ConfigurationChangedEvent, EditorOption } from '../../../../editor/common/config/editorOptions.js';
23
import { IDimension } from '../../../../editor/common/core/2d/dimension.js';
24
import { Position } from '../../../../editor/common/core/position.js';
25
import { Range } from '../../../../editor/common/core/range.js';
26
import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js';
27
import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js';
28
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
29
import * as nls from '../../../../nls.js';
30
import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';
31
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
32
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
33
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
34
import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js';
35
import { ILogService } from '../../../../platform/log/common/log.js';
36
import { asCssVariable, editorHoverBackground, editorHoverBorder, editorHoverForeground } from '../../../../platform/theme/common/colorRegistry.js';
37
import { IDebugService, IDebugSession, IExpression, IExpressionContainer, IStackFrame } from '../common/debug.js';
38
import { Expression, Variable, VisualizedExpression } from '../common/debugModel.js';
39
import { getEvaluatableExpressionAtPosition } from '../common/debugUtils.js';
40
import { AbstractExpressionDataSource } from './baseDebugView.js';
41
import { DebugExpressionRenderer } from './debugExpressionRenderer.js';
42
import { VariablesRenderer, VisualizedVariableRenderer, openContextMenuForVariableTreeElement } from './variablesView.js';
43
44
const $ = dom.$;
45
46
export const enum ShowDebugHoverResult {
47
NOT_CHANGED,
48
NOT_AVAILABLE,
49
CANCELLED,
50
}
51
52
async function doFindExpression(container: IExpressionContainer, namesToFind: string[]): Promise<IExpression | null> {
53
if (!container) {
54
return null;
55
}
56
57
const children = await container.getChildren();
58
// look for our variable in the list. First find the parents of the hovered variable if there are any.
59
const filtered = children.filter(v => namesToFind[0] === v.name);
60
if (filtered.length !== 1) {
61
return null;
62
}
63
64
if (namesToFind.length === 1) {
65
return filtered[0];
66
} else {
67
return doFindExpression(filtered[0], namesToFind.slice(1));
68
}
69
}
70
71
export async function findExpressionInStackFrame(stackFrame: IStackFrame, namesToFind: string[]): Promise<IExpression | undefined> {
72
const scopes = await stackFrame.getScopes();
73
const nonExpensive = scopes.filter(s => !s.expensive);
74
const expressions = coalesce(await Promise.all(nonExpensive.map(scope => doFindExpression(scope, namesToFind))));
75
76
// only show if all expressions found have the same value
77
return expressions.length > 0 && expressions.every(e => e.value === expressions[0].value) ? expressions[0] : undefined;
78
}
79
80
export class DebugHoverWidget implements IContentWidget {
81
82
static readonly ID = 'debug.hoverWidget';
83
// editor.IContentWidget.allowEditorOverflow
84
readonly allowEditorOverflow = true;
85
86
// todo@connor4312: move more properties that are only valid while a hover
87
// is happening into `_isVisible`
88
private _isVisible?: {
89
store: lifecycle.DisposableStore;
90
};
91
private safeTriangle?: dom.SafeTriangle;
92
private showCancellationSource?: CancellationTokenSource;
93
private domNode!: HTMLElement;
94
private tree!: AsyncDataTree<IExpression, IExpression, any>;
95
private showAtPosition: Position | null;
96
private positionPreference: ContentWidgetPositionPreference[];
97
private readonly highlightDecorations: IEditorDecorationsCollection;
98
private complexValueContainer!: HTMLElement;
99
private complexValueTitle!: HTMLElement;
100
private valueContainer!: HTMLElement;
101
private treeContainer!: HTMLElement;
102
private toDispose: lifecycle.IDisposable[];
103
private scrollbar!: DomScrollableElement;
104
private debugHoverComputer: DebugHoverComputer;
105
private expressionRenderer: DebugExpressionRenderer;
106
107
private expressionToRender: IExpression | undefined;
108
private isUpdatingTree = false;
109
110
public get isShowingComplexValue() {
111
return this.complexValueContainer?.hidden === false;
112
}
113
114
constructor(
115
private editor: ICodeEditor,
116
@IDebugService private readonly debugService: IDebugService,
117
@IInstantiationService private readonly instantiationService: IInstantiationService,
118
@IMenuService private readonly menuService: IMenuService,
119
@IContextKeyService private readonly contextKeyService: IContextKeyService,
120
@IContextMenuService private readonly contextMenuService: IContextMenuService,
121
) {
122
this.highlightDecorations = this.editor.createDecorationsCollection();
123
this.toDispose = [];
124
125
this.showAtPosition = null;
126
this.positionPreference = [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW];
127
this.debugHoverComputer = this.instantiationService.createInstance(DebugHoverComputer, this.editor);
128
this.expressionRenderer = this.instantiationService.createInstance(DebugExpressionRenderer);
129
}
130
131
private create(): void {
132
this.domNode = $('.debug-hover-widget');
133
this.complexValueContainer = dom.append(this.domNode, $('.complex-value'));
134
this.complexValueTitle = dom.append(this.complexValueContainer, $('.title'));
135
this.treeContainer = dom.append(this.complexValueContainer, $('.debug-hover-tree'));
136
this.treeContainer.setAttribute('role', 'tree');
137
const tip = dom.append(this.complexValueContainer, $('.tip'));
138
tip.textContent = nls.localize({ key: 'quickTip', comment: ['"switch to editor language hover" means to show the programming language hover widget instead of the debug hover'] }, 'Hold {0} key to switch to editor language hover', isMacintosh ? 'Option' : 'Alt');
139
const dataSource = this.instantiationService.createInstance(DebugHoverDataSource);
140
this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree<IExpression, IExpression, any>, 'DebugHover', this.treeContainer, new DebugHoverDelegate(), [
141
this.instantiationService.createInstance(VariablesRenderer, this.expressionRenderer),
142
this.instantiationService.createInstance(VisualizedVariableRenderer, this.expressionRenderer),
143
],
144
dataSource, {
145
accessibilityProvider: new DebugHoverAccessibilityProvider(),
146
mouseSupport: false,
147
horizontalScrolling: true,
148
useShadows: false,
149
keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression) => e.name },
150
overrideStyles: {
151
listBackground: editorHoverBackground
152
}
153
});
154
155
this.toDispose.push(VisualizedVariableRenderer.rendererOnVisualizationRange(this.debugService.getViewModel(), this.tree));
156
157
this.valueContainer = $('.value');
158
this.valueContainer.tabIndex = 0;
159
this.valueContainer.setAttribute('role', 'tooltip');
160
this.scrollbar = new DomScrollableElement(this.valueContainer, { horizontal: ScrollbarVisibility.Hidden });
161
this.domNode.appendChild(this.scrollbar.getDomNode());
162
this.toDispose.push(this.scrollbar);
163
164
this.editor.applyFontInfo(this.domNode);
165
this.domNode.style.backgroundColor = asCssVariable(editorHoverBackground);
166
this.domNode.style.border = `1px solid ${asCssVariable(editorHoverBorder)}`;
167
this.domNode.style.color = asCssVariable(editorHoverForeground);
168
169
this.toDispose.push(this.tree.onContextMenu(async e => await this.onContextMenu(e)));
170
171
this.toDispose.push(this.tree.onDidChangeContentHeight(() => {
172
if (!this.isUpdatingTree) {
173
// Don't do a layout in the middle of the async setInput
174
this.layoutTreeAndContainer();
175
}
176
}));
177
this.toDispose.push(this.tree.onDidChangeContentWidth(() => {
178
if (!this.isUpdatingTree) {
179
// Don't do a layout in the middle of the async setInput
180
this.layoutTreeAndContainer();
181
}
182
}));
183
184
this.registerListeners();
185
this.editor.addContentWidget(this);
186
}
187
188
private async onContextMenu(e: ITreeContextMenuEvent<IExpression>): Promise<void> {
189
const variable = e.element;
190
if (!(variable instanceof Variable) || !variable.value) {
191
return;
192
}
193
194
return openContextMenuForVariableTreeElement(this.contextKeyService, this.menuService, this.contextMenuService, MenuId.DebugHoverContext, e);
195
}
196
197
private registerListeners(): void {
198
this.toDispose.push(dom.addStandardDisposableListener(this.domNode, 'keydown', (e: IKeyboardEvent) => {
199
if (e.equals(KeyCode.Escape)) {
200
this.hide();
201
}
202
}));
203
this.toDispose.push(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {
204
if (e.hasChanged(EditorOption.fontInfo)) {
205
this.editor.applyFontInfo(this.domNode);
206
}
207
}));
208
209
this.toDispose.push(this.debugService.getViewModel().onDidEvaluateLazyExpression(async e => {
210
if (e instanceof Variable && this.tree.hasNode(e)) {
211
await this.tree.updateChildren(e, false, true);
212
await this.tree.expand(e);
213
}
214
}));
215
}
216
217
isHovered(): boolean {
218
return !!this.domNode?.matches(':hover');
219
}
220
221
isVisible(): boolean {
222
return !!this._isVisible;
223
}
224
225
willBeVisible(): boolean {
226
return !!this.showCancellationSource;
227
}
228
229
getId(): string {
230
return DebugHoverWidget.ID;
231
}
232
233
getDomNode(): HTMLElement {
234
return this.domNode;
235
}
236
237
/**
238
* Gets whether the given coordinates are in the safe triangle formed from
239
* the position at which the hover was initiated.
240
*/
241
isInSafeTriangle(x: number, y: number) {
242
return this._isVisible && !!this.safeTriangle?.contains(x, y);
243
}
244
245
async showAt(position: Position, focus: boolean, mouseEvent?: IMouseEvent): Promise<void | ShowDebugHoverResult> {
246
this.showCancellationSource?.dispose(true);
247
const cancellationSource = this.showCancellationSource = new CancellationTokenSource();
248
const session = this.debugService.getViewModel().focusedSession;
249
250
if (!session || !this.editor.hasModel()) {
251
this.hide();
252
return ShowDebugHoverResult.NOT_AVAILABLE;
253
}
254
255
const result = await this.debugHoverComputer.compute(position, cancellationSource.token);
256
if (cancellationSource.token.isCancellationRequested) {
257
this.hide();
258
return ShowDebugHoverResult.CANCELLED;
259
}
260
261
if (!result.range) {
262
this.hide();
263
return ShowDebugHoverResult.NOT_AVAILABLE;
264
}
265
266
if (this.isVisible() && !result.rangeChanged) {
267
return ShowDebugHoverResult.NOT_CHANGED;
268
}
269
270
const expression = await this.debugHoverComputer.evaluate(session);
271
if (cancellationSource.token.isCancellationRequested) {
272
this.hide();
273
return ShowDebugHoverResult.CANCELLED;
274
}
275
276
if (!expression || (expression instanceof Expression && !expression.available)) {
277
this.hide();
278
return ShowDebugHoverResult.NOT_AVAILABLE;
279
}
280
281
this.highlightDecorations.set([{
282
range: result.range,
283
options: DebugHoverWidget._HOVER_HIGHLIGHT_DECORATION_OPTIONS
284
}]);
285
286
return this.doShow(session, result.range.getStartPosition(), expression, focus, mouseEvent);
287
}
288
289
private static readonly _HOVER_HIGHLIGHT_DECORATION_OPTIONS = ModelDecorationOptions.register({
290
description: 'bdebug-hover-highlight',
291
className: 'hoverHighlight'
292
});
293
294
private async doShow(session: IDebugSession | undefined, position: Position, expression: IExpression, focus: boolean, mouseEvent: IMouseEvent | undefined): Promise<void> {
295
if (!this.domNode) {
296
this.create();
297
}
298
299
this.showAtPosition = position;
300
const store = new lifecycle.DisposableStore();
301
this._isVisible = { store };
302
303
if (!expression.hasChildren) {
304
this.complexValueContainer.hidden = true;
305
this.valueContainer.hidden = false;
306
store.add(this.expressionRenderer.renderValue(this.valueContainer, expression, {
307
showChanged: false,
308
colorize: true,
309
hover: false,
310
session,
311
}));
312
this.valueContainer.title = '';
313
this.editor.layoutContentWidget(this);
314
this.safeTriangle = mouseEvent && new dom.SafeTriangle(mouseEvent.posx, mouseEvent.posy, this.domNode);
315
this.scrollbar.scanDomNode();
316
if (focus) {
317
this.editor.render();
318
this.valueContainer.focus();
319
}
320
321
return undefined;
322
}
323
324
this.valueContainer.hidden = true;
325
326
this.expressionToRender = expression;
327
store.add(this.expressionRenderer.renderValue(this.complexValueTitle, expression, { hover: false, session }));
328
this.editor.layoutContentWidget(this);
329
this.safeTriangle = mouseEvent && new dom.SafeTriangle(mouseEvent.posx, mouseEvent.posy, this.domNode);
330
this.tree.scrollTop = 0;
331
this.tree.scrollLeft = 0;
332
this.complexValueContainer.hidden = false;
333
334
if (focus) {
335
this.editor.render();
336
this.tree.domFocus();
337
}
338
}
339
340
private layoutTreeAndContainer(): void {
341
this.layoutTree();
342
this.editor.layoutContentWidget(this);
343
}
344
345
private layoutTree(): void {
346
const scrollBarHeight = 10;
347
let maxHeightToAvoidCursorOverlay = Infinity;
348
if (this.showAtPosition) {
349
const editorTop = this.editor.getDomNode()?.offsetTop || 0;
350
const containerTop = this.treeContainer.offsetTop + editorTop;
351
const hoveredCharTop = this.editor.getTopForLineNumber(this.showAtPosition.lineNumber, true) - this.editor.getScrollTop();
352
if (containerTop < hoveredCharTop) {
353
maxHeightToAvoidCursorOverlay = hoveredCharTop + editorTop - 22; // 22 is monaco top padding https://github.com/microsoft/vscode/blob/a1df2d7319382d42f66ad7f411af01e4cc49c80a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts#L364
354
}
355
}
356
const treeHeight = Math.min(Math.max(266, this.editor.getLayoutInfo().height * 0.55), this.tree.contentHeight + scrollBarHeight, maxHeightToAvoidCursorOverlay);
357
358
const realTreeWidth = this.tree.contentWidth;
359
const treeWidth = clamp(realTreeWidth, 400, 550);
360
this.tree.layout(treeHeight, treeWidth);
361
this.treeContainer.style.height = `${treeHeight}px`;
362
this.scrollbar.scanDomNode();
363
}
364
365
beforeRender(): IDimension | null {
366
// beforeRender will be called each time the hover size changes, and the content widget is layed out again.
367
if (this.expressionToRender) {
368
const expression = this.expressionToRender;
369
this.expressionToRender = undefined;
370
371
// Do this in beforeRender once the content widget is no longer display=none so that its elements' sizes will be measured correctly.
372
this.isUpdatingTree = true;
373
this.tree.setInput(expression).finally(() => {
374
this.isUpdatingTree = false;
375
});
376
}
377
378
return null;
379
}
380
381
afterRender(positionPreference: ContentWidgetPositionPreference | null) {
382
if (positionPreference) {
383
// Remember where the editor placed you to keep position stable #109226
384
this.positionPreference = [positionPreference];
385
}
386
}
387
388
389
hide(): void {
390
if (this.showCancellationSource) {
391
this.showCancellationSource.dispose(true);
392
this.showCancellationSource = undefined;
393
}
394
395
if (!this._isVisible) {
396
return;
397
}
398
399
if (dom.isAncestorOfActiveElement(this.domNode)) {
400
this.editor.focus();
401
}
402
this._isVisible.store.dispose();
403
this._isVisible = undefined;
404
405
this.highlightDecorations.clear();
406
this.editor.layoutContentWidget(this);
407
this.positionPreference = [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW];
408
}
409
410
getPosition(): IContentWidgetPosition | null {
411
return this._isVisible ? {
412
position: this.showAtPosition,
413
preference: this.positionPreference
414
} : null;
415
}
416
417
dispose(): void {
418
this.toDispose = lifecycle.dispose(this.toDispose);
419
}
420
}
421
422
class DebugHoverAccessibilityProvider implements IListAccessibilityProvider<IExpression> {
423
424
getWidgetAriaLabel(): string {
425
return nls.localize('treeAriaLabel', "Debug Hover");
426
}
427
428
getAriaLabel(element: IExpression): string {
429
return nls.localize({ key: 'variableAriaLabel', comment: ['Do not translate placeholders. Placeholders are name and value of a variable.'] }, "{0}, value {1}, variables, debug", element.name, element.value);
430
}
431
}
432
433
class DebugHoverDataSource extends AbstractExpressionDataSource<IExpression, IExpression> {
434
435
public override hasChildren(element: IExpression): boolean {
436
return element.hasChildren;
437
}
438
439
protected override doGetChildren(element: IExpression): Promise<IExpression[]> {
440
return element.getChildren();
441
}
442
}
443
444
class DebugHoverDelegate implements IListVirtualDelegate<IExpression> {
445
getHeight(element: IExpression): number {
446
return 18;
447
}
448
449
getTemplateId(element: IExpression): string {
450
if (element instanceof VisualizedExpression) {
451
return VisualizedVariableRenderer.ID;
452
}
453
return VariablesRenderer.ID;
454
}
455
}
456
457
interface IDebugHoverComputeResult {
458
rangeChanged: boolean;
459
range?: Range;
460
}
461
462
class DebugHoverComputer {
463
private _current?: {
464
range: Range;
465
expression: string;
466
};
467
468
constructor(
469
private editor: ICodeEditor,
470
@IDebugService private readonly debugService: IDebugService,
471
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
472
@ILogService private readonly logService: ILogService,
473
) { }
474
475
public async compute(position: Position, token: CancellationToken): Promise<IDebugHoverComputeResult> {
476
const session = this.debugService.getViewModel().focusedSession;
477
if (!session || !this.editor.hasModel()) {
478
return { rangeChanged: false };
479
}
480
481
const model = this.editor.getModel();
482
const result = await getEvaluatableExpressionAtPosition(this.languageFeaturesService, model, position, token);
483
if (!result) {
484
return { rangeChanged: false };
485
}
486
487
const { range, matchingExpression } = result;
488
const rangeChanged = !this._current?.range.equalsRange(range);
489
this._current = { expression: matchingExpression, range: Range.lift(range) };
490
return { rangeChanged, range: this._current.range };
491
}
492
493
async evaluate(session: IDebugSession): Promise<IExpression | undefined> {
494
if (!this._current) {
495
this.logService.error('No expression to evaluate');
496
return;
497
}
498
499
const textModel = this.editor.getModel();
500
const debugSource = textModel && session.getSourceForUri(textModel?.uri);
501
502
if (session.capabilities.supportsEvaluateForHovers) {
503
const expression = new Expression(this._current.expression);
504
await expression.evaluate(session, this.debugService.getViewModel().focusedStackFrame, 'hover', undefined, debugSource ? {
505
line: this._current.range.startLineNumber,
506
column: this._current.range.startColumn,
507
source: debugSource.raw,
508
} : undefined);
509
return expression;
510
} else {
511
const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;
512
if (focusedStackFrame) {
513
return await findExpressionInStackFrame(
514
focusedStackFrame,
515
coalesce(this._current.expression.split('.').map(word => word.trim()))
516
);
517
}
518
}
519
520
return undefined;
521
}
522
}
523
524