296 lines
11 KiB
TypeScript
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());
|
|
}
|
|
|
|
}
|