Files
theia-code-os/packages/debug/src/browser/editor/debug-hover-widget.ts
mawkone 8bb5110148
Some checks failed
Playwright Tests / Playwright Tests (ubuntu-22.04, Node.js 22.x) (push) Has been cancelled
3PP License Check / 3PP License Check (11, 22.x, ubuntu-22.04) (push) Has been cancelled
Publish packages to NPM / Perform Publishing (push) Has been cancelled
deploy: current vibn theia state
Made-with: Cursor
2026-02-27 12:01:08 -08:00

296 lines
11 KiB
TypeScript

// *****************************************************************************
// Copyright (C) 2018 TypeFox and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************
import debounce = require('@theia/core/shared/lodash.debounce');
import { MenuPath } from '@theia/core';
import { Key } from '@theia/core/lib/browser';
import { SourceTreeWidget } from '@theia/core/lib/browser/source-tree';
import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable';
import { Message } from '@theia/core/shared/@lumino/messaging';
import { Widget } from '@theia/core/shared/@lumino/widgets';
import { Container, inject, injectable, interfaces, postConstruct } from '@theia/core/shared/inversify';
import { URI as CodeUri } from '@theia/core/shared/vscode-uri';
import * as monaco from '@theia/monaco-editor-core';
import { IEditorHoverOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/config/editorOptions';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { IConfigurationService } from '@theia/monaco-editor-core/esm/vs/platform/configuration/common/configuration';
import { DebugVariable } from '../console/debug-console-items';
import { DebugSessionManager } from '../debug-session-manager';
import { DebugEditor } from './debug-editor';
import { DebugExpressionProvider } from './debug-expression-provider';
import { DebugHoverSource } from './debug-hover-source';
export interface ShowDebugHoverOptions {
selection: monaco.Range
/** default: false */
focus?: boolean
/** default: true */
immediate?: boolean
}
export interface HideDebugHoverOptions {
/** default: true */
immediate?: boolean
}
export function createDebugHoverWidgetContainer(parent: interfaces.Container, editor: DebugEditor): Container {
const child = SourceTreeWidget.createContainer(parent, {
contextMenuPath: DebugHoverWidget.CONTEXT_MENU,
virtualized: false
});
child.bind(DebugEditor).toConstantValue(editor);
child.bind(DebugHoverSource).toSelf();
child.unbind(SourceTreeWidget);
child.bind(DebugHoverWidget).toSelf();
return child;
}
@injectable()
export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor.IContentWidget {
static CONTEXT_MENU: MenuPath = ['debug-hover-context-menu'];
static EDIT_MENU: MenuPath = [...DebugHoverWidget.CONTEXT_MENU, 'a_edit'];
static WATCH_MENU: MenuPath = [...DebugHoverWidget.CONTEXT_MENU, 'b_watch'];
@inject(DebugEditor)
protected readonly editor: DebugEditor;
@inject(DebugSessionManager)
protected readonly sessions: DebugSessionManager;
@inject(DebugHoverSource)
protected readonly hoverSource: DebugHoverSource;
@inject(DebugExpressionProvider)
protected readonly expressionProvider: DebugExpressionProvider;
allowEditorOverflow = true;
protected suppressEditorHoverToDispose = new DisposableCollection();
static ID = 'debug.editor.hover';
getId(): string {
return DebugHoverWidget.ID;
}
protected readonly domNode = document.createElement('div');
protected readonly titleNode = document.createElement('div');
protected readonly contentNode = document.createElement('div');
getDomNode(): HTMLElement {
return this.domNode;
}
@postConstruct()
protected override init(): void {
super.init();
this.domNode.className = 'theia-debug-hover';
this.titleNode.className = 'theia-debug-hover-title';
this.domNode.appendChild(this.titleNode);
this.contentNode.className = 'theia-debug-hover-content';
this.domNode.appendChild(this.contentNode);
// for stopping scroll events from contentNode going to the editor
this.contentNode.addEventListener('wheel', e => e.stopPropagation());
this.editor.getControl().addContentWidget(this);
this.source = this.hoverSource;
this.toDispose.pushAll([
this.hoverSource,
Disposable.create(() => this.editor.getControl().removeContentWidget(this)),
Disposable.create(() => this.hide()),
this.sessions.onDidChange(() => {
if (!this.isEditorFrame()) {
this.hide();
}
})
]);
}
override dispose(): void {
this.suppressEditorHoverToDispose.dispose();
this.toDispose.dispose();
}
override show(options?: ShowDebugHoverOptions): void {
this.schedule(() => this.doShow(options), options && options.immediate);
}
override hide(options?: HideDebugHoverOptions): void {
this.schedule(() => this.doHide(), options && options.immediate);
}
protected readonly doSchedule = debounce((fn: () => void) => fn(), 300);
protected schedule(fn: () => void, immediate: boolean = true): void {
if (immediate) {
this.doSchedule.cancel();
fn();
} else {
this.doSchedule(fn);
}
}
protected options: ShowDebugHoverOptions | undefined;
protected doHide(): void {
if (!this.isVisible) {
return;
}
this.suppressEditorHoverToDispose.dispose();
if (this.domNode.contains(document.activeElement)) {
this.editor.getControl().focus();
}
if (this.isAttached) {
Widget.detach(this);
}
this.hoverSource.reset();
super.hide();
this.options = undefined;
this.editor.getControl().layoutContentWidget(this);
}
protected async doShow(options: ShowDebugHoverOptions | undefined = this.options): Promise<void> {
if (!this.isEditorFrame()) {
this.hide();
return;
}
if (!options) {
this.hide();
return;
}
if (this.options && this.options.selection.equalsRange(options.selection)) {
return;
}
if (!this.isAttached) {
Widget.attach(this, this.contentNode);
}
this.options = options;
const result = await this.expressionProvider.getEvaluatableExpression(this.editor, options.selection);
if (!result?.matchingExpression) {
this.hide();
return;
}
this.options.selection = monaco.Range.lift(result.range);
const toFocus = new DisposableCollection();
if (this.options.focus === true) {
toFocus.push(this.model.onNodeRefreshed(() => {
toFocus.dispose();
this.activate();
}));
}
const expression = await this.hoverSource.evaluate(result.matchingExpression);
if (!expression) {
toFocus.dispose();
this.hide();
return;
}
this.contentNode.hidden = false;
['number', 'boolean', 'string'].forEach(token => this.titleNode.classList.remove(token));
this.domNode.classList.remove('complex-value');
if (expression.hasElements) {
this.domNode.classList.add('complex-value');
} else {
this.contentNode.hidden = true;
if (expression.type === 'number' || expression.type === 'boolean' || expression.type === 'string') {
this.titleNode.classList.add(expression.type);
} else if (!isNaN(+expression.value)) {
this.titleNode.classList.add('number');
} else if (DebugVariable.booleanRegex.test(expression.value)) {
this.titleNode.classList.add('boolean');
} else if (DebugVariable.stringRegex.test(expression.value)) {
this.titleNode.classList.add('string');
}
}
this.suppressEditorHover();
super.show();
await new Promise<void>(resolve => {
setTimeout(() => window.requestAnimationFrame(() => {
this.editor.getControl().layoutContentWidget(this);
resolve();
}), 0);
});
}
/**
* Suppress the default editor-contribution hover from Code.
* Otherwise, both `textdocument/hover` and the debug hovers are visible
* at the same time when hovering over a symbol.
* This will priorize the debug hover over the editor hover.
*/
protected suppressEditorHover(): void {
const codeEditor = this.editor.getControl();
codeEditor.updateOptions({ hover: { enabled: false } });
this.suppressEditorHoverToDispose.push(Disposable.create(() => {
const model = codeEditor.getModel();
const overrides = {
resource: CodeUri.parse(this.editor.getResourceUri().toString()),
overrideIdentifier: model?.getLanguageId(),
};
const { enabled, delay, sticky } = StandaloneServices.get(IConfigurationService).getValue<IEditorHoverOptions>('editor.hover', overrides);
codeEditor.updateOptions({
hover: {
enabled,
delay,
sticky
}
});
}));
}
protected isEditorFrame(): boolean {
return this.sessions.isCurrentEditorFrame(this.editor.getResourceUri());
}
getPosition(): monaco.editor.IContentWidgetPosition {
if (!this.isVisible) {
return undefined!;
}
const position = this.options && this.options.selection.getStartPosition();
return position
? {
position: new monaco.Position(position.lineNumber, position.column),
preference: [
monaco.editor.ContentWidgetPositionPreference.ABOVE,
monaco.editor.ContentWidgetPositionPreference.BELOW,
],
}
: undefined!;
}
protected override onUpdateRequest(msg: Message): void {
super.onUpdateRequest(msg);
const { expression } = this.hoverSource;
const value = expression && expression.value || '';
this.titleNode.textContent = value;
this.titleNode.title = value;
}
protected override onAfterAttach(msg: Message): void {
super.onAfterAttach(msg);
this.addKeyListener(this.domNode, Key.ESCAPE, () => this.hide());
}
}