From d1439da0e6124b22acfb6e1152e3ee2351c5c0d7 Mon Sep 17 00:00:00 2001 From: Nina Doschek Date: Fri, 29 Jul 2022 12:03:29 +0200 Subject: [PATCH] #10027 Support EvaluatableExpressions - Implement support for plugins providing evalutable epressions - Update the debug hover widget to consume evaluatable expressions from the registered providers. Keep the former implementation of guessing the expression from the current line as fallback. Contributed on behalf of STMicroelectronics Signed-off-by: Nina Doschek Fixes #10027 --- CHANGELOG.md | 4 ++ .../src/browser/editor/debug-hover-widget.ts | 41 ++++++++++++- .../src/common/plugin-api-rpc-model.ts | 10 ++++ .../plugin-ext/src/common/plugin-api-rpc.ts | 3 + .../src/main/browser/languages-main.ts | 24 +++++++- packages/plugin-ext/src/plugin/languages.ts | 15 +++++ .../languages/evaluatable-expression.ts | 47 +++++++++++++++ .../plugin-ext/src/plugin/plugin-context.ts | 5 ++ .../plugin-ext/src/plugin/type-converters.ts | 7 +++ packages/plugin-ext/src/plugin/types-impl.ts | 18 ++++++ packages/plugin/src/theia.d.ts | 60 +++++++++++++++++++ 11 files changed, 232 insertions(+), 2 deletions(-) create mode 100644 packages/plugin-ext/src/plugin/languages/evaluatable-expression.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fe333beaf3571..e278c60a44488 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ - [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/) +## v1.29.0 - Unreleased + +- [plugin] added support for `EvaluatableExpressions` [#11484](https://github.com/eclipse-theia/theia/pull/11484) - Contributed on behalf of STMicroelectronics + ## v1.28.0 - 7/28/2022 - [cli] improved error handling when interacting with the API [#11454](https://github.com/eclipse-theia/theia/issues/11454) diff --git a/packages/debug/src/browser/editor/debug-hover-widget.ts b/packages/debug/src/browser/editor/debug-hover-widget.ts index fa5e4f208c921..dc83c4176512e 100644 --- a/packages/debug/src/browser/editor/debug-hover-widget.ts +++ b/packages/debug/src/browser/editor/debug-hover-widget.ts @@ -28,6 +28,10 @@ import { DebugExpressionProvider } from './debug-expression-provider'; import { DebugHoverSource } from './debug-hover-source'; import { DebugVariable } from '../console/debug-console-items'; import * as monaco from '@theia/monaco-editor-core'; +import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices'; +import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures'; +import { CancellationTokenSource } from '@theia/monaco-editor-core/esm/vs/base/common/cancellation'; +import { Position } from '@theia/monaco-editor-core/esm/vs/editor/common/core/position'; export interface ShowDebugHoverOptions { selection: monaco.Range @@ -146,6 +150,8 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. } protected async doShow(options: ShowDebugHoverOptions | undefined = this.options): Promise { + const cancellationSource = new CancellationTokenSource(); + if (!this.isEditorFrame()) { this.hide(); return; @@ -162,7 +168,33 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. } this.options = options; - const matchingExpression = this.expressionProvider.get(this.editor.getControl().getModel()!, options.selection); + let matchingExpression: string | undefined; + + const pluginExpressionProvider = StandaloneServices.get(ILanguageFeaturesService).evaluatableExpressionProvider; + const textEditorModel = this.editor.document.textEditorModel; + + if (pluginExpressionProvider && pluginExpressionProvider.has(textEditorModel)) { + const registeredProviders = pluginExpressionProvider.ordered(textEditorModel); + const position = new Position(this.options!.selection.startLineNumber, this.options!.selection.startColumn); + + const promises = registeredProviders.map(support => + Promise.resolve(support.provideEvaluatableExpression(textEditorModel, position, cancellationSource.token)).then(exp => exp) + ); + + const results = await Promise.all(promises).then(coalesce); + if (results.length > 0) { + matchingExpression = results[0].expression; + const range = results[0].range; + + if (!matchingExpression) { + const lineContent = textEditorModel.getLineContent(position.lineNumber); + matchingExpression = lineContent.substring(range.startColumn - 1, range.endColumn - 1); + } + } + } else { // use fallback if no provider was registered + matchingExpression = this.expressionProvider.get(this.editor.getControl().getModel()!, options.selection); + } + if (!matchingExpression) { this.hide(); return; @@ -241,3 +273,10 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor. } } + +/** + * @returns New array with all falsy values removed. The original array IS NOT modified. + */ +export function coalesce(array: ReadonlyArray): T[] { + return array.filter(e => !!e); +} diff --git a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts index 681247c2a1310..976bf2cfc25fe 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -255,6 +255,16 @@ export interface HoverProvider { provideHover(model: monaco.editor.ITextModel, position: monaco.Position, token: monaco.CancellationToken): Hover | undefined | Thenable; } +export interface EvaluatableExpression { + range: Range; + expression?: string; +} + +export interface EvaluatableExpressionProvider { + provideEvaluatableExpression(model: monaco.editor.ITextModel, position: monaco.Position, + token: monaco.CancellationToken): EvaluatableExpression | undefined | Thenable; +} + export enum DocumentHighlightKind { Text = 0, Read = 1, diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index c95739ac93ca5..3500c06965a9a 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -41,6 +41,7 @@ import { MarkerData, SignatureHelp, Hover, + EvaluatableExpression, DocumentHighlight, FormattingOptions, ChainedCacheId, @@ -1464,6 +1465,7 @@ export interface LanguagesExt { ): Promise; $releaseSignatureHelp(handle: number, id: number): void; $provideHover(handle: number, resource: UriComponents, position: Position, token: CancellationToken): Promise; + $provideEvaluatableExpression(handle: number, resource: UriComponents, position: Position, token: CancellationToken): Promise; $provideDocumentHighlights(handle: number, resource: UriComponents, position: Position, token: CancellationToken): Promise; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: FormattingOptions, token: CancellationToken): Promise; @@ -1540,6 +1542,7 @@ export interface LanguagesMain { $registerReferenceProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; $registerSignatureHelpProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], metadata: theia.SignatureHelpProviderMetadata): void; $registerHoverProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; + $registerEvaluatableExpressionProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; $registerDocumentHighlightProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void; $registerQuickFixProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], codeActionKinds?: string[], documentation?: CodeActionProviderDocumentation): void; $clearDiagnostics(id: string): void; diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index 2b7438967ab26..078d4e20d668d 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -66,6 +66,9 @@ import * as MonacoPath from '@theia/monaco-editor-core/esm/vs/base/common/path'; import { IRelativePattern } from '@theia/monaco-editor-core/esm/vs/base/common/glob'; import { EditorLanguageStatusService, LanguageStatus as EditorLanguageStatus } from '@theia/editor/lib/browser/language-status/editor-language-status-service'; import { LanguageSelector, RelativePattern } from '@theia/editor/lib/common/language-selector'; +import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures'; +import { EvaluatableExpression, EvaluatableExpressionProvider } from '@theia/monaco-editor-core/esm/vs/editor/common/languages'; +import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model'; /** * @monaco-uplift The public API declares these functions as (languageId: string, service). @@ -338,7 +341,26 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { return this.proxy.$provideHover(handle, model.uri, position, token); } - $registerDocumentHighlightProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void { + $registerEvaluatableExpressionProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void { + const languageSelector = this.toLanguageSelector(selector); + const evaluatableExpressionProvider = this.createEvaluatableExpressionProvider(handle); + this.register(handle, + (StandaloneServices.get(ILanguageFeaturesService).evaluatableExpressionProvider.register as RegistrationFunction) + (languageSelector, evaluatableExpressionProvider)); + } + + protected createEvaluatableExpressionProvider(handle: number): EvaluatableExpressionProvider { + return { + provideEvaluatableExpression: (model, position, token) => this.provideEvaluatableExpression(handle, model, position, token) + }; + } + + protected provideEvaluatableExpression(handle: number, model: ITextModel, position: monaco.Position, + token: monaco.CancellationToken): monaco.languages.ProviderResult { + return this.proxy.$provideEvaluatableExpression(handle, model.uri, position, token); + } + + $registerDocumentHighlightProvider(handle: number, _pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void { const languageSelector = this.toLanguageSelector(selector); const documentHighlightProvider = this.createDocumentHighlightProvider(handle); this.register(handle, (monaco.languages.registerDocumentHighlightProvider as RegistrationFunction) diff --git a/packages/plugin-ext/src/plugin/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index 3e80545fbe764..c438d85eb924e 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -62,11 +62,13 @@ import { CallHierarchyIncomingCall, CallHierarchyOutgoingCall, LinkedEditingRanges, + EvaluatableExpression } from '../common/plugin-api-rpc-model'; import { CompletionAdapter } from './languages/completion'; import { Diagnostics } from './languages/diagnostics'; import { SignatureHelpAdapter } from './languages/signature'; import { HoverAdapter } from './languages/hover'; +import { EvaluatableExpressionAdapter } from './languages/evaluatable-expression'; import { DocumentHighlightAdapter } from './languages/document-highlight'; import { DocumentFormattingAdapter } from './languages/document-formatting'; import { RangeFormattingAdapter } from './languages/range-formatting'; @@ -100,6 +102,7 @@ import { serializeEnterRules, serializeIndentation, serializeRegExp } from './la type Adapter = CompletionAdapter | SignatureHelpAdapter | HoverAdapter | + EvaluatableExpressionAdapter | DocumentHighlightAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | @@ -350,6 +353,18 @@ export class LanguagesExtImpl implements LanguagesExt { } // ### Hover Provider end + // ### EvaluatableExpression Provider begin + registerEvaluatableExpressionProvider(selector: theia.DocumentSelector, provider: theia.EvaluatableExpressionProvider, pluginInfo: PluginInfo): theia.Disposable { + const callId = this.addNewAdapter(new EvaluatableExpressionAdapter(provider, this.documents)); + this.proxy.$registerEvaluatableExpressionProvider(callId, pluginInfo, this.transformDocumentSelector(selector)); + return this.createDisposable(callId); + } + + $provideEvaluatableExpression(handle: number, resource: UriComponents, position: Position, token: theia.CancellationToken): Promise { + return this.withAdapter(handle, EvaluatableExpressionAdapter, adapter => adapter.provideEvaluatableExpression(URI.revive(resource), position, token), undefined); + } + // ### EvaluatableExpression Provider end + // ### Document Highlight Provider begin registerDocumentHighlightProvider(selector: theia.DocumentSelector, provider: theia.DocumentHighlightProvider, pluginInfo: PluginInfo): theia.Disposable { const callId = this.addNewAdapter(new DocumentHighlightAdapter(provider, this.documents)); diff --git a/packages/plugin-ext/src/plugin/languages/evaluatable-expression.ts b/packages/plugin-ext/src/plugin/languages/evaluatable-expression.ts new file mode 100644 index 0000000000000..ea7fb6f643b3f --- /dev/null +++ b/packages/plugin-ext/src/plugin/languages/evaluatable-expression.ts @@ -0,0 +1,47 @@ +// ***************************************************************************** +// Copyright (C) 2022 STMicroelectronics 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 WITH Classpath-exception-2.0 +// ***************************************************************************** + +import { URI } from '@theia/core/shared/vscode-uri'; +import * as theia from '@theia/plugin'; +import { Position } from '../../common/plugin-api-rpc'; +import { EvaluatableExpression } from '../../common/plugin-api-rpc-model'; +import { DocumentsExtImpl } from '../documents'; +import * as Converter from '../type-converters'; + +export class EvaluatableExpressionAdapter { + + constructor( + private readonly provider: theia.EvaluatableExpressionProvider, + private readonly documents: DocumentsExtImpl + ) { } + + async provideEvaluatableExpression(resource: URI, position: Position, token: theia.CancellationToken): Promise { + const documentData = this.documents.getDocumentData(resource); + if (!documentData) { + return Promise.reject(new Error(`There is no document data for ${resource}`)); + } + + const document = documentData.document; + const pos = Converter.toPosition(position); + + return Promise.resolve(this.provider.provideEvaluatableExpression(document, pos, token)).then(expression => { + if (!expression) { + return undefined; + } + return Converter.fromEvaluatableExpression(expression); + }); + } +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 72ea8b2f0b452..014ba4fd47a56 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -78,6 +78,7 @@ import { SignatureHelp, SignatureHelpTriggerKind, Hover, + EvaluatableExpression, DocumentHighlightKind, DocumentHighlight, DocumentLink, @@ -731,6 +732,9 @@ export function createAPIFactory( registerHoverProvider(selector: theia.DocumentSelector, provider: theia.HoverProvider): theia.Disposable { return languagesExt.registerHoverProvider(selector, provider, pluginToPluginInfo(plugin)); }, + registerEvaluatableExpressionProvider(selector: theia.DocumentSelector, provider: theia.EvaluatableExpressionProvider): theia.Disposable { + return languagesExt.registerEvaluatableExpressionProvider(selector, provider, pluginToPluginInfo(plugin)); + }, registerDocumentHighlightProvider(selector: theia.DocumentSelector, provider: theia.DocumentHighlightProvider): theia.Disposable { return languagesExt.registerDocumentHighlightProvider(selector, provider, pluginToPluginInfo(plugin)); }, @@ -991,6 +995,7 @@ export function createAPIFactory( SignatureHelp, SignatureHelpTriggerKind, Hover, + EvaluatableExpression, DocumentHighlightKind, DocumentHighlight, DocumentLink, diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index ae400b0db32f9..3d0813c496542 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -396,6 +396,13 @@ export function fromHover(hover: theia.Hover): model.Hover { }; } +export function fromEvaluatableExpression(evaluatableExpression: theia.EvaluatableExpression): model.EvaluatableExpression { + return { + range: fromRange(evaluatableExpression.range), + expression: evaluatableExpression.expression + }; +} + export function fromLocation(location: theia.Location): model.Location { return { uri: location.uri, diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index 5ababe64d05ac..979670be308b5 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1119,6 +1119,24 @@ export class Hover { } } +@es5ClassCompat +export class EvaluatableExpression { + + public range: Range; + public expression?: string; + + constructor( + range: Range, + expression?: string + ) { + if (!range) { + illegalArgument('range must be defined'); + } + this.range = range; + this.expression = expression; + } +} + export enum DocumentHighlightKind { Text = 0, Read = 1, diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 57eb2db0cb649..6a514fef13b33 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -9267,6 +9267,18 @@ export module '@theia/plugin' { */ export function registerHoverProvider(selector: DocumentSelector, provider: HoverProvider): Disposable; + /** + * Register a provider that locates evaluatable expressions in text documents. + * The editor will evaluate the expression in the active debug session and will show the result in the debug hover. + * + * If multiple providers are registered for a language an arbitrary provider will be used. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An evaluatable expression provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerEvaluatableExpressionProvider(selector: DocumentSelector, provider: EvaluatableExpressionProvider): Disposable; + /** * Register a workspace symbol provider. * @@ -9580,6 +9592,54 @@ export module '@theia/plugin' { provideHover(document: TextDocument, position: Position, token: CancellationToken | undefined): ProviderResult; } + /** + * An EvaluatableExpression represents an expression in a document that can be evaluated by an active debugger or runtime. + * The result of this evaluation is shown in a tooltip-like widget. + * If only a range is specified, the expression will be extracted from the underlying document. + * An optional expression can be used to override the extracted expression. + * In this case the range is still used to highlight the range in the document. + */ + export class EvaluatableExpression { + + /* + * The range is used to extract the evaluatable expression from the underlying document and to highlight it. + */ + readonly range: Range; + + /* + * If specified the expression overrides the extracted expression. + */ + readonly expression?: string | undefined; + + /** + * Creates a new evaluatable expression object. + * + * @param range The range in the underlying document from which the evaluatable expression is extracted. + * @param expression If specified overrides the extracted expression. + */ + constructor(range: Range, expression?: string); + } + + /** + * The evaluatable expression provider interface defines the contract between extensions and + * the debug hover. In this contract the provider returns an evaluatable expression for a given position + * in a document and the editor evaluates this expression in the active debug session and shows the result in a debug hover. + */ + export interface EvaluatableExpressionProvider { + /** + * Provide an evaluatable expression for the given document and position. + * The editor will evaluate this expression in the active debug session and will show the result in the debug hover. + * The expression can be implicitly specified by the range in the underlying document or by explicitly returning an expression. + * + * @param document The document for which the debug hover is about to appear. + * @param position The line and character position in the document where the debug hover is about to appear. + * @param token A cancellation token. + * @return An EvaluatableExpression or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + provideEvaluatableExpression(document: TextDocument, position: Position, token: CancellationToken | undefined): ProviderResult; + } + /** * A document highlight kind. */