Path: blob/main/src/vs/workbench/contrib/debug/browser/debugHover.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as dom from '../../../../base/browser/dom.js';6import { IKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';7import { IMouseEvent } from '../../../../base/browser/mouseEvent.js';8import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js';9import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js';10import { DomScrollableElement } from '../../../../base/browser/ui/scrollbar/scrollableElement.js';11import { AsyncDataTree } from '../../../../base/browser/ui/tree/asyncDataTree.js';12import { ITreeContextMenuEvent } from '../../../../base/browser/ui/tree/tree.js';13import { coalesce } from '../../../../base/common/arrays.js';14import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';15import { KeyCode } from '../../../../base/common/keyCodes.js';16import * as lifecycle from '../../../../base/common/lifecycle.js';17import { clamp } from '../../../../base/common/numbers.js';18import { isMacintosh } from '../../../../base/common/platform.js';19import { ScrollbarVisibility } from '../../../../base/common/scrollable.js';20import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js';21import { ConfigurationChangedEvent, EditorOption } from '../../../../editor/common/config/editorOptions.js';22import { IDimension } from '../../../../editor/common/core/2d/dimension.js';23import { Position } from '../../../../editor/common/core/position.js';24import { Range } from '../../../../editor/common/core/range.js';25import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js';26import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js';27import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';28import * as nls from '../../../../nls.js';29import { IMenuService, MenuId } from '../../../../platform/actions/common/actions.js';30import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';31import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';32import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';33import { WorkbenchAsyncDataTree } from '../../../../platform/list/browser/listService.js';34import { ILogService } from '../../../../platform/log/common/log.js';35import { asCssVariable, editorHoverBackground, editorHoverBorder, editorHoverForeground } from '../../../../platform/theme/common/colorRegistry.js';36import { IDebugService, IDebugSession, IExpression, IExpressionContainer, IStackFrame } from '../common/debug.js';37import { Expression, Variable, VisualizedExpression } from '../common/debugModel.js';38import { getEvaluatableExpressionAtPosition } from '../common/debugUtils.js';39import { AbstractExpressionDataSource } from './baseDebugView.js';40import { DebugExpressionRenderer } from './debugExpressionRenderer.js';41import { VariablesRenderer, VisualizedVariableRenderer, openContextMenuForVariableTreeElement } from './variablesView.js';4243const $ = dom.$;4445export const enum ShowDebugHoverResult {46NOT_CHANGED,47NOT_AVAILABLE,48CANCELLED,49}5051async function doFindExpression(container: IExpressionContainer, namesToFind: string[]): Promise<IExpression | null> {52if (!container) {53return null;54}5556const children = await container.getChildren();57// look for our variable in the list. First find the parents of the hovered variable if there are any.58const filtered = children.filter(v => namesToFind[0] === v.name);59if (filtered.length !== 1) {60return null;61}6263if (namesToFind.length === 1) {64return filtered[0];65} else {66return doFindExpression(filtered[0], namesToFind.slice(1));67}68}6970export async function findExpressionInStackFrame(stackFrame: IStackFrame, namesToFind: string[]): Promise<IExpression | undefined> {71const scopes = await stackFrame.getScopes();72const nonExpensive = scopes.filter(s => !s.expensive);73const expressions = coalesce(await Promise.all(nonExpensive.map(scope => doFindExpression(scope, namesToFind))));7475// only show if all expressions found have the same value76return expressions.length > 0 && expressions.every(e => e.value === expressions[0].value) ? expressions[0] : undefined;77}7879export class DebugHoverWidget implements IContentWidget {8081static readonly ID = 'debug.hoverWidget';82// editor.IContentWidget.allowEditorOverflow83readonly allowEditorOverflow = true;8485// todo@connor4312: move more properties that are only valid while a hover86// is happening into `_isVisible`87private _isVisible?: {88store: lifecycle.DisposableStore;89};90private safeTriangle?: dom.SafeTriangle;91private showCancellationSource?: CancellationTokenSource;92private domNode!: HTMLElement;93private tree!: AsyncDataTree<IExpression, IExpression, any>;94private showAtPosition: Position | null;95private positionPreference: ContentWidgetPositionPreference[];96private readonly highlightDecorations: IEditorDecorationsCollection;97private complexValueContainer!: HTMLElement;98private complexValueTitle!: HTMLElement;99private valueContainer!: HTMLElement;100private treeContainer!: HTMLElement;101private toDispose: lifecycle.IDisposable[];102private scrollbar!: DomScrollableElement;103private debugHoverComputer: DebugHoverComputer;104private expressionRenderer: DebugExpressionRenderer;105106private expressionToRender: IExpression | undefined;107private isUpdatingTree = false;108109public get isShowingComplexValue() {110return this.complexValueContainer?.hidden === false;111}112113constructor(114private editor: ICodeEditor,115@IDebugService private readonly debugService: IDebugService,116@IInstantiationService private readonly instantiationService: IInstantiationService,117@IMenuService private readonly menuService: IMenuService,118@IContextKeyService private readonly contextKeyService: IContextKeyService,119@IContextMenuService private readonly contextMenuService: IContextMenuService,120) {121this.highlightDecorations = this.editor.createDecorationsCollection();122this.toDispose = [];123124this.showAtPosition = null;125this.positionPreference = [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW];126this.debugHoverComputer = this.instantiationService.createInstance(DebugHoverComputer, this.editor);127this.expressionRenderer = this.instantiationService.createInstance(DebugExpressionRenderer);128}129130private create(): void {131this.domNode = $('.debug-hover-widget');132this.complexValueContainer = dom.append(this.domNode, $('.complex-value'));133this.complexValueTitle = dom.append(this.complexValueContainer, $('.title'));134this.treeContainer = dom.append(this.complexValueContainer, $('.debug-hover-tree'));135this.treeContainer.setAttribute('role', 'tree');136const tip = dom.append(this.complexValueContainer, $('.tip'));137tip.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');138const dataSource = this.instantiationService.createInstance(DebugHoverDataSource);139this.tree = this.instantiationService.createInstance(WorkbenchAsyncDataTree<IExpression, IExpression, any>, 'DebugHover', this.treeContainer, new DebugHoverDelegate(), [140this.instantiationService.createInstance(VariablesRenderer, this.expressionRenderer),141this.instantiationService.createInstance(VisualizedVariableRenderer, this.expressionRenderer),142],143dataSource, {144accessibilityProvider: new DebugHoverAccessibilityProvider(),145mouseSupport: false,146horizontalScrolling: true,147useShadows: false,148keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: IExpression) => e.name },149overrideStyles: {150listBackground: editorHoverBackground151}152});153154this.toDispose.push(VisualizedVariableRenderer.rendererOnVisualizationRange(this.debugService.getViewModel(), this.tree));155156this.valueContainer = $('.value');157this.valueContainer.tabIndex = 0;158this.valueContainer.setAttribute('role', 'tooltip');159this.scrollbar = new DomScrollableElement(this.valueContainer, { horizontal: ScrollbarVisibility.Hidden });160this.domNode.appendChild(this.scrollbar.getDomNode());161this.toDispose.push(this.scrollbar);162163this.editor.applyFontInfo(this.domNode);164this.domNode.style.backgroundColor = asCssVariable(editorHoverBackground);165this.domNode.style.border = `1px solid ${asCssVariable(editorHoverBorder)}`;166this.domNode.style.color = asCssVariable(editorHoverForeground);167168this.toDispose.push(this.tree.onContextMenu(async e => await this.onContextMenu(e)));169170this.toDispose.push(this.tree.onDidChangeContentHeight(() => {171if (!this.isUpdatingTree) {172// Don't do a layout in the middle of the async setInput173this.layoutTreeAndContainer();174}175}));176this.toDispose.push(this.tree.onDidChangeContentWidth(() => {177if (!this.isUpdatingTree) {178// Don't do a layout in the middle of the async setInput179this.layoutTreeAndContainer();180}181}));182183this.registerListeners();184this.editor.addContentWidget(this);185}186187private async onContextMenu(e: ITreeContextMenuEvent<IExpression>): Promise<void> {188const variable = e.element;189if (!(variable instanceof Variable) || !variable.value) {190return;191}192193return openContextMenuForVariableTreeElement(this.contextKeyService, this.menuService, this.contextMenuService, MenuId.DebugHoverContext, e);194}195196private registerListeners(): void {197this.toDispose.push(dom.addStandardDisposableListener(this.domNode, 'keydown', (e: IKeyboardEvent) => {198if (e.equals(KeyCode.Escape)) {199this.hide();200}201}));202this.toDispose.push(this.editor.onDidChangeConfiguration((e: ConfigurationChangedEvent) => {203if (e.hasChanged(EditorOption.fontInfo)) {204this.editor.applyFontInfo(this.domNode);205}206}));207208this.toDispose.push(this.debugService.getViewModel().onDidEvaluateLazyExpression(async e => {209if (e instanceof Variable && this.tree.hasNode(e)) {210await this.tree.updateChildren(e, false, true);211await this.tree.expand(e);212}213}));214}215216isHovered(): boolean {217return !!this.domNode?.matches(':hover');218}219220isVisible(): boolean {221return !!this._isVisible;222}223224willBeVisible(): boolean {225return !!this.showCancellationSource;226}227228getId(): string {229return DebugHoverWidget.ID;230}231232getDomNode(): HTMLElement {233return this.domNode;234}235236/**237* Gets whether the given coordinates are in the safe triangle formed from238* the position at which the hover was initiated.239*/240isInSafeTriangle(x: number, y: number) {241return this._isVisible && !!this.safeTriangle?.contains(x, y);242}243244async showAt(position: Position, focus: boolean, mouseEvent?: IMouseEvent): Promise<void | ShowDebugHoverResult> {245this.showCancellationSource?.dispose(true);246const cancellationSource = this.showCancellationSource = new CancellationTokenSource();247const session = this.debugService.getViewModel().focusedSession;248249if (!session || !this.editor.hasModel()) {250this.hide();251return ShowDebugHoverResult.NOT_AVAILABLE;252}253254const result = await this.debugHoverComputer.compute(position, cancellationSource.token);255if (cancellationSource.token.isCancellationRequested) {256this.hide();257return ShowDebugHoverResult.CANCELLED;258}259260if (!result.range) {261this.hide();262return ShowDebugHoverResult.NOT_AVAILABLE;263}264265if (this.isVisible() && !result.rangeChanged) {266return ShowDebugHoverResult.NOT_CHANGED;267}268269const expression = await this.debugHoverComputer.evaluate(session);270if (cancellationSource.token.isCancellationRequested) {271this.hide();272return ShowDebugHoverResult.CANCELLED;273}274275if (!expression || (expression instanceof Expression && !expression.available)) {276this.hide();277return ShowDebugHoverResult.NOT_AVAILABLE;278}279280this.highlightDecorations.set([{281range: result.range,282options: DebugHoverWidget._HOVER_HIGHLIGHT_DECORATION_OPTIONS283}]);284285return this.doShow(session, result.range.getStartPosition(), expression, focus, mouseEvent);286}287288private static readonly _HOVER_HIGHLIGHT_DECORATION_OPTIONS = ModelDecorationOptions.register({289description: 'bdebug-hover-highlight',290className: 'hoverHighlight'291});292293private async doShow(session: IDebugSession | undefined, position: Position, expression: IExpression, focus: boolean, mouseEvent: IMouseEvent | undefined): Promise<void> {294if (!this.domNode) {295this.create();296}297298this.showAtPosition = position;299const store = new lifecycle.DisposableStore();300this._isVisible = { store };301302if (!expression.hasChildren) {303this.complexValueContainer.hidden = true;304this.valueContainer.hidden = false;305store.add(this.expressionRenderer.renderValue(this.valueContainer, expression, {306showChanged: false,307colorize: true,308hover: false,309session,310}));311this.valueContainer.title = '';312this.editor.layoutContentWidget(this);313this.safeTriangle = mouseEvent && new dom.SafeTriangle(mouseEvent.posx, mouseEvent.posy, this.domNode);314this.scrollbar.scanDomNode();315if (focus) {316this.editor.render();317this.valueContainer.focus();318}319320return undefined;321}322323this.valueContainer.hidden = true;324325this.expressionToRender = expression;326store.add(this.expressionRenderer.renderValue(this.complexValueTitle, expression, { hover: false, session }));327this.editor.layoutContentWidget(this);328this.safeTriangle = mouseEvent && new dom.SafeTriangle(mouseEvent.posx, mouseEvent.posy, this.domNode);329this.tree.scrollTop = 0;330this.tree.scrollLeft = 0;331this.complexValueContainer.hidden = false;332333if (focus) {334this.editor.render();335this.tree.domFocus();336}337}338339private layoutTreeAndContainer(): void {340this.layoutTree();341this.editor.layoutContentWidget(this);342}343344private layoutTree(): void {345const scrollBarHeight = 10;346let maxHeightToAvoidCursorOverlay = Infinity;347if (this.showAtPosition) {348const editorTop = this.editor.getDomNode()?.offsetTop || 0;349const containerTop = this.treeContainer.offsetTop + editorTop;350const hoveredCharTop = this.editor.getTopForLineNumber(this.showAtPosition.lineNumber, true) - this.editor.getScrollTop();351if (containerTop < hoveredCharTop) {352maxHeightToAvoidCursorOverlay = hoveredCharTop + editorTop - 22; // 22 is monaco top padding https://github.com/microsoft/vscode/blob/a1df2d7319382d42f66ad7f411af01e4cc49c80a/src/vs/editor/browser/viewParts/contentWidgets/contentWidgets.ts#L364353}354}355const treeHeight = Math.min(Math.max(266, this.editor.getLayoutInfo().height * 0.55), this.tree.contentHeight + scrollBarHeight, maxHeightToAvoidCursorOverlay);356357const realTreeWidth = this.tree.contentWidth;358const treeWidth = clamp(realTreeWidth, 400, 550);359this.tree.layout(treeHeight, treeWidth);360this.treeContainer.style.height = `${treeHeight}px`;361this.scrollbar.scanDomNode();362}363364beforeRender(): IDimension | null {365// beforeRender will be called each time the hover size changes, and the content widget is layed out again.366if (this.expressionToRender) {367const expression = this.expressionToRender;368this.expressionToRender = undefined;369370// Do this in beforeRender once the content widget is no longer display=none so that its elements' sizes will be measured correctly.371this.isUpdatingTree = true;372this.tree.setInput(expression).finally(() => {373this.isUpdatingTree = false;374});375}376377return null;378}379380afterRender(positionPreference: ContentWidgetPositionPreference | null) {381if (positionPreference) {382// Remember where the editor placed you to keep position stable #109226383this.positionPreference = [positionPreference];384}385}386387388hide(): void {389if (this.showCancellationSource) {390this.showCancellationSource.dispose(true);391this.showCancellationSource = undefined;392}393394if (!this._isVisible) {395return;396}397398if (dom.isAncestorOfActiveElement(this.domNode)) {399this.editor.focus();400}401this._isVisible.store.dispose();402this._isVisible = undefined;403404this.highlightDecorations.clear();405this.editor.layoutContentWidget(this);406this.positionPreference = [ContentWidgetPositionPreference.ABOVE, ContentWidgetPositionPreference.BELOW];407}408409getPosition(): IContentWidgetPosition | null {410return this._isVisible ? {411position: this.showAtPosition,412preference: this.positionPreference413} : null;414}415416dispose(): void {417this.toDispose = lifecycle.dispose(this.toDispose);418}419}420421class DebugHoverAccessibilityProvider implements IListAccessibilityProvider<IExpression> {422423getWidgetAriaLabel(): string {424return nls.localize('treeAriaLabel', "Debug Hover");425}426427getAriaLabel(element: IExpression): string {428return 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);429}430}431432class DebugHoverDataSource extends AbstractExpressionDataSource<IExpression, IExpression> {433434public override hasChildren(element: IExpression): boolean {435return element.hasChildren;436}437438protected override doGetChildren(element: IExpression): Promise<IExpression[]> {439return element.getChildren();440}441}442443class DebugHoverDelegate implements IListVirtualDelegate<IExpression> {444getHeight(element: IExpression): number {445return 18;446}447448getTemplateId(element: IExpression): string {449if (element instanceof VisualizedExpression) {450return VisualizedVariableRenderer.ID;451}452return VariablesRenderer.ID;453}454}455456interface IDebugHoverComputeResult {457rangeChanged: boolean;458range?: Range;459}460461class DebugHoverComputer {462private _current?: {463range: Range;464expression: string;465};466467constructor(468private editor: ICodeEditor,469@IDebugService private readonly debugService: IDebugService,470@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,471@ILogService private readonly logService: ILogService,472) { }473474public async compute(position: Position, token: CancellationToken): Promise<IDebugHoverComputeResult> {475const session = this.debugService.getViewModel().focusedSession;476if (!session || !this.editor.hasModel()) {477return { rangeChanged: false };478}479480const model = this.editor.getModel();481const result = await getEvaluatableExpressionAtPosition(this.languageFeaturesService, model, position, token);482if (!result) {483return { rangeChanged: false };484}485486const { range, matchingExpression } = result;487const rangeChanged = !this._current?.range.equalsRange(range);488this._current = { expression: matchingExpression, range: Range.lift(range) };489return { rangeChanged, range: this._current.range };490}491492async evaluate(session: IDebugSession): Promise<IExpression | undefined> {493if (!this._current) {494this.logService.error('No expression to evaluate');495return;496}497498const textModel = this.editor.getModel();499const debugSource = textModel && session.getSourceForUri(textModel?.uri);500501if (session.capabilities.supportsEvaluateForHovers) {502const expression = new Expression(this._current.expression);503await expression.evaluate(session, this.debugService.getViewModel().focusedStackFrame, 'hover', undefined, debugSource ? {504line: this._current.range.startLineNumber,505column: this._current.range.startColumn,506source: debugSource.raw,507} : undefined);508return expression;509} else {510const focusedStackFrame = this.debugService.getViewModel().focusedStackFrame;511if (focusedStackFrame) {512return await findExpressionInStackFrame(513focusedStackFrame,514coalesce(this._current.expression.split('.').map(word => word.trim()))515);516}517}518519return undefined;520}521}522523524