diff --git a/CHANGELOG.md b/CHANGELOG.md index 4de150e16915e..c737bf21ff8bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ## v1.29.0 - unreleased +- [plugin] added support for `EvaluatableExpressions` [#11484](https://github.com/eclipse-theia/theia/pull/11484) - Contributed on behalf of STMicroelectronics + [Breaking Changes:](#breaking_changes_1.29.0) - [core] `updateThemePreference` and `updateThemeFromPreference` removed from `CommonFrontendContribution`. Corresponding functionality as been moved to the respective theme service. `load` removed from `IconThemeService` [#11473](https://github.com/eclipse-theia/theia/issues/11473) diff --git a/packages/core/src/common/types.ts b/packages/core/src/common/types.ts index cdabbeb69a96c..a01d1c4bef275 100644 --- a/packages/core/src/common/types.ts +++ b/packages/core/src/common/types.ts @@ -162,6 +162,13 @@ export namespace ArrayUtils { } return -(low + 1); } + + /** + * @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/debug/src/browser/editor/debug-hover-widget.ts b/packages/debug/src/browser/editor/debug-hover-widget.ts index fa5e4f208c921..ea1d1c2f87d67 100644 --- a/packages/debug/src/browser/editor/debug-hover-widget.ts +++ b/packages/debug/src/browser/editor/debug-hover-widget.ts @@ -28,6 +28,11 @@ 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'; +import { ArrayUtils } from '@theia/core'; export interface ShowDebugHoverOptions { selection: monaco.Range @@ -146,6 +151,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 +169,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)) + ); + + const results = await Promise.all(promises).then(ArrayUtils.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; 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 8754dcee861ce..7d7311488686b 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, @@ -1470,6 +1471,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; @@ -1546,6 +1548,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 a5cf1e505b1b7..00fddb1f7c806 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 5fccb1e109bf1..713f8276bf8ef 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -9269,6 +9269,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. * @@ -9582,6 +9594,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. */