From 132ce03ed60ba26049b0150fe046ba7ce2309c11 Mon Sep 17 00:00:00 2001 From: Akos Kitta Date: Thu, 25 Mar 2021 18:44:09 +0100 Subject: [PATCH] GH-9262: Fixed multiple hover issues for currentFrame editor. Enabled debug hover coloring for strings, numbers, and booleans. Closes #9262 Signed-off-by: Akos Kitta --- .../src/browser/debug-session-manager.ts | 21 +++++- .../src/browser/editor/debug-editor-model.ts | 72 +++++++++++++++---- .../src/browser/editor/debug-hover-source.tsx | 4 +- .../src/browser/editor/debug-hover-widget.ts | 48 ++++++++++--- packages/debug/src/browser/style/index.css | 21 +++--- packages/monaco/src/typings/monaco/index.d.ts | 8 +++ 6 files changed, 141 insertions(+), 33 deletions(-) diff --git a/packages/debug/src/browser/debug-session-manager.ts b/packages/debug/src/browser/debug-session-manager.ts index 54da78df11546..9b0c09d6bd686 100644 --- a/packages/debug/src/browser/debug-session-manager.ts +++ b/packages/debug/src/browser/debug-session-manager.ts @@ -166,10 +166,29 @@ export class DebugSessionManager { return this.state > DebugState.Inactive; } + isCurrentEditorFrame(uri: URI | string | monaco.Uri): boolean { + return this.currentFrame?.source?.uri.toString() === (uri instanceof URI ? uri : new URI(uri)).toString(); + } + + protected async saveAll(): Promise { + if (!this.shell.canSaveAll()) { + return true; // Nothing to save. + } + try { + await this.shell.saveAll(); + return true; + } catch (error) { + console.error(error); + return false; + } + } + async start(options: DebugSessionOptions): Promise { return this.progressService.withProgress('Start...', 'debug', async () => { try { - await this.shell.saveAll(); + if (!await this.saveAll()) { + return undefined; + } await this.fireWillStartDebugSession(); const resolved = await this.resolveConfiguration(options); diff --git a/packages/debug/src/browser/editor/debug-editor-model.ts b/packages/debug/src/browser/editor/debug-editor-model.ts index 9d122756af3f0..cf63a3173076d 100644 --- a/packages/debug/src/browser/editor/debug-editor-model.ts +++ b/packages/debug/src/browser/editor/debug-editor-model.ts @@ -19,6 +19,7 @@ import { injectable, inject, postConstruct, interfaces, Container } from 'invers import URI from '@theia/core/lib/common/uri'; import { Disposable, DisposableCollection, MenuPath, isOSX } from '@theia/core'; import { ContextMenuRenderer } from '@theia/core/lib/browser'; +import { MonacoConfigurationService } from '@theia/monaco/lib/browser/monaco-frontend-module'; import { BreakpointManager } from '../breakpoint/breakpoint-manager'; import { DebugSourceBreakpoint } from '../model/debug-source-breakpoint'; import { DebugSessionManager } from '../debug-session-manager'; @@ -50,6 +51,7 @@ export class DebugEditorModel implements Disposable { static CONTEXT_MENU: MenuPath = ['debug-editor-context-menu']; protected readonly toDispose = new DisposableCollection(); + protected readonly toDisposeOnUpdate = new DisposableCollection(); protected uri: URI; @@ -58,7 +60,7 @@ export class DebugEditorModel implements Disposable { protected currentBreakpointDecorations: string[] = []; - protected frameDecorations: string[] = []; + protected editorDecorations: string[] = []; protected topFrameRange: monaco.Range | undefined; protected updatingDecorations = false; @@ -87,6 +89,9 @@ export class DebugEditorModel implements Disposable { @inject(DebugInlineValueDecorator) readonly inlineValueDecorator: DebugInlineValueDecorator; + @inject(MonacoConfigurationService) + readonly configurationService: monaco.services.IConfigurationService; + @postConstruct() protected init(): void { this.uri = new URI(this.editor.getControl().getModel()!.uri.toString()); @@ -98,11 +103,12 @@ export class DebugEditorModel implements Disposable { this.editor.getControl().onMouseMove(event => this.handleMouseMove(event)), this.editor.getControl().onMouseLeave(event => this.handleMouseLeave(event)), this.editor.getControl().onKeyDown(() => this.hover.hide({ immediate: false })), - this.editor.getControl().onDidChangeModelContent(() => this.renderFrames()), + this.editor.getControl().onDidChangeModelContent(() => this.update()), this.editor.getControl().getModel()!.onDidChangeDecorations(() => this.updateBreakpoints()), - this.sessions.onDidChange(() => this.renderFrames()) + this.sessions.onDidChange(() => this.update()), + this.toDisposeOnUpdate ]); - this.renderFrames(); + this.update(); this.render(); } @@ -110,11 +116,44 @@ export class DebugEditorModel implements Disposable { this.toDispose.dispose(); } - protected readonly renderFrames = debounce(async () => { + protected readonly update = debounce(async () => { if (this.toDispose.disposed) { return; } + this.toDisposeOnUpdate.dispose(); this.toggleExceptionWidget(); + await this.updateEditorDecorations(); + this.updateEditorHover(); + }, 100); + + /** + * To disable the default editor-contribution hover from Code when + * the editor has the `currentFrame`. Otherwise, both `textdocument/hover` + * and the debug hovers are visible at the same time when hovering over a symbol. + */ + protected async updateEditorHover(): Promise { + if (this.sessions.isCurrentEditorFrame(this.uri)) { + const codeEditor = this.editor.getControl(); + codeEditor.updateOptions({ hover: { enabled: false } }); + this.toDisposeOnUpdate.push(Disposable.create(() => { + const model = codeEditor.getModel()!; + const overrides = { + resource: model.uri, + overrideIdentifier: model.getLanguageIdentifier().language, + }; + const { enabled, delay, sticky } = this.configurationService._configuration.getValue('editor.hover', overrides, undefined); + codeEditor.updateOptions({ + hover: { + enabled, + delay, + sticky + } + }); + })); + } + } + + protected async updateEditorDecorations(): Promise { const [newFrameDecorations, inlineValueDecorations] = await Promise.all([ this.createFrameDecorations(), this.createInlineValueDecorations() @@ -122,24 +161,28 @@ export class DebugEditorModel implements Disposable { const codeEditor = this.editor.getControl(); codeEditor.removeDecorations(INLINE_VALUE_DECORATION_KEY); codeEditor.setDecorations(INLINE_VALUE_DECORATION_KEY, inlineValueDecorations); - this.frameDecorations = this.deltaDecorations(this.frameDecorations, newFrameDecorations); - }, 100); + this.editorDecorations = this.deltaDecorations(this.editorDecorations, newFrameDecorations); + } protected async createInlineValueDecorations(): Promise { - const { currentFrame } = this.sessions; - if (!currentFrame || !currentFrame.source || currentFrame.source.uri.toString() !== this.uri.toString()) { + if (this.sessions.isCurrentEditorFrame(this.uri)) { return []; } + const { currentFrame } = this.sessions; return this.inlineValueDecorator.calculateDecorations(this, currentFrame); } protected createFrameDecorations(): monaco.editor.IModelDeltaDecoration[] { - const decorations: monaco.editor.IModelDeltaDecoration[] = []; const { currentFrame, topFrame } = this.sessions; - if (!currentFrame || !currentFrame.source || currentFrame.source.uri.toString() !== this.uri.toString()) { - return decorations; + if (!currentFrame) { + return []; } + if (!this.sessions.isCurrentEditorFrame(this.uri)) { + return []; + } + + const decorations: monaco.editor.IModelDeltaDecoration[] = []; const columnUntilEOLRange = new monaco.Range(currentFrame.raw.line, currentFrame.raw.column, currentFrame.raw.line, 1 << 30); const range = new monaco.Range(currentFrame.raw.line, currentFrame.raw.column, currentFrame.raw.line, currentFrame.raw.column + 1); @@ -175,7 +218,10 @@ export class DebugEditorModel implements Disposable { protected async toggleExceptionWidget(): Promise { const { currentFrame } = this.sessions; - if (!currentFrame || !currentFrame.source || currentFrame.source.uri.toString() !== this.uri.toString()) { + if (!currentFrame) { + return; + } + if (!this.sessions.isCurrentEditorFrame(this.uri)) { this.exceptionWidget.hide(); return; } diff --git a/packages/debug/src/browser/editor/debug-hover-source.tsx b/packages/debug/src/browser/editor/debug-hover-source.tsx index b86ce847cf24c..3ccc109708def 100644 --- a/packages/debug/src/browser/editor/debug-hover-source.tsx +++ b/packages/debug/src/browser/editor/debug-hover-source.tsx @@ -46,13 +46,13 @@ export class DebugHoverSource extends TreeSource { this.fireDidChange(); } - async evaluate(expression: string): Promise { + async evaluate(expression: string): Promise { const evaluated = await this.doEvaluate(expression); const elements = evaluated && await evaluated.getElements(); this._expression = evaluated; this.elements = elements ? [...elements] : []; this.fireDidChange(); - return !!evaluated; + return evaluated; } protected async doEvaluate(expression: string): Promise { const { currentSession } = this.sessions; diff --git a/packages/debug/src/browser/editor/debug-hover-widget.ts b/packages/debug/src/browser/editor/debug-hover-widget.ts index 1ed6dca22593c..c8dbd1129d9a0 100644 --- a/packages/debug/src/browser/editor/debug-hover-widget.ts +++ b/packages/debug/src/browser/editor/debug-hover-widget.ts @@ -26,6 +26,7 @@ import { DebugSessionManager } from '../debug-session-manager'; import { DebugEditor } from './debug-editor'; import { DebugExpressionProvider } from './debug-expression-provider'; import { DebugHoverSource } from './debug-hover-source'; +import { DebugVariable } from '../console/debug-console-items'; export interface ShowDebugHoverOptions { selection: monaco.Range @@ -113,9 +114,12 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. show(options?: ShowDebugHoverOptions): void { this.schedule(() => this.doShow(options), options && options.immediate); } + 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(); @@ -124,7 +128,6 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. this.doSchedule(fn); } } - protected readonly doSchedule = debounce((fn: () => void) => fn(), 300); protected options: ShowDebugHoverOptions | undefined; protected doHide(): void { @@ -142,6 +145,7 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. this.options = undefined; this.editor.getControl().layoutContentWidget(this); } + protected async doShow(options: ShowDebugHoverOptions | undefined = this.options): Promise { if (!this.isEditorFrame()) { this.hide(); @@ -157,10 +161,10 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. if (!this.isAttached) { Widget.attach(this, this.contentNode); } - super.show(); + this.options = options; - const expression = this.expressionProvider.get(this.editor.getControl().getModel()!, options.selection); - if (!expression) { + const matchingExpression = this.expressionProvider.get(this.editor.getControl().getModel()!, options.selection); + if (!matchingExpression) { this.hide(); return; } @@ -171,17 +175,43 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. this.activate(); })); } - if (!await this.hoverSource.evaluate(expression)) { + const expression = await this.hoverSource.evaluate(matchingExpression); + if (!expression) { toFocus.dispose(); this.hide(); return; } - this.editor.getControl().layoutContentWidget(this); + + this.contentNode.hidden = false; + this.domNode.classList.remove(); + ['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'); + } + } + + super.show(); + new Promise(resolve => { + setTimeout(() => window.requestAnimationFrame(() => { + this.editor.getControl().layoutContentWidget(this); + resolve(); + }), 0); + }); } + protected isEditorFrame(): boolean { - const { currentFrame } = this.sessions; - return !!currentFrame && !!currentFrame.source && - this.editor.getControl().getModel()!.uri.toString() === currentFrame.source.uri.toString(); + return this.sessions.isCurrentEditorFrame(this.editor.getControl().getModel()!.uri); } getPosition(): monaco.editor.IContentWidgetPosition { diff --git a/packages/debug/src/browser/style/index.css b/packages/debug/src/browser/style/index.css index 8431a3b075f3e..2a20ad203372f 100644 --- a/packages/debug/src/browser/style/index.css +++ b/packages/debug/src/browser/style/index.css @@ -185,16 +185,19 @@ color: var(--theia-variable-value-color); } +.theia-debug-hover-title.number, .theia-debug-console-variable.number { - color: var(--theia-number-variable-color); + color: var(--theia-variable-number-variable-color); } +.theia-debug-hover-title.boolean, .theia-debug-console-variable.boolean { - color: var(--theia-boolean-variable-color); + color: var(--theia-variable-boolean-variable-color); } +.theia-debug-hover-title.string, .theia-debug-console-variable.string { - color: var(--theia-string-variable-color); + color: var(--theia-variable-string-variable-color); } .theia-debug-console-variable .name { @@ -342,16 +345,19 @@ .theia-debug-hover { display: flex; flex-direction: column; - min-width: 324px; - min-height: 324px; - width: 324px; - height: 324px; border: 1px solid var(--theia-editorHoverWidget-border); background: var(--theia-editorHoverWidget-background); /* TODO color: var(--theia-editorHoverWidget-foreground); with a newer Monaco version */ color: var(--theia-editorWidget-foreground); } +.theia-debug-hover.complex-value { + min-width: 324px; + min-height: 324px; + width: 324px; + height: 324px; +} + .theia-debug-hover .theia-source-tree { height: 100%; width: 100%; @@ -362,7 +368,6 @@ white-space: nowrap; text-overflow: ellipsis; padding: var(--theia-ui-padding); - padding-left: calc(var(--theia-ui-padding)*2); border-bottom: 1px solid var(--theia-editorHoverWidget-border); } diff --git a/packages/monaco/src/typings/monaco/index.d.ts b/packages/monaco/src/typings/monaco/index.d.ts index 9754aad438bc2..a0f65fe3e9987 100644 --- a/packages/monaco/src/typings/monaco/index.d.ts +++ b/packages/monaco/src/typings/monaco/index.d.ts @@ -449,6 +449,14 @@ declare module monaco.editor { // https://github.com/microsoft/vscode/blob/e683ace9e5acadba0e8bde72d793cb2cb83e58a7/src/vs/editor/common/model.ts#L623 createSnapshot(): ITextSnapshot | null; + + /** + * Get the language associated with this model. + * @internal + */ + // https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/editor/common/model.ts#L833-L837 + getLanguageIdentifier(): monaco.services.LanguageIdentifier; + } }