Skip to content

Commit

Permalink
eclipse-theia#10027 Support EvaluatableExpressions
Browse files Browse the repository at this point in the history
- 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 <ndoschek@eclipsesource.com>

Fixes eclipse-theia#10027
  • Loading branch information
ndoschek committed Jul 29, 2022
1 parent d971520 commit d1439da
Show file tree
Hide file tree
Showing 11 changed files with 232 additions and 2 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
41 changes: 40 additions & 1 deletion packages/debug/src/browser/editor/debug-hover-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -146,6 +150,8 @@ export class DebugHoverWidget extends SourceTreeWidget implements monaco.editor.
}

protected async doShow(options: ShowDebugHoverOptions | undefined = this.options): Promise<void> {
const cancellationSource = new CancellationTokenSource();

if (!this.isEditorFrame()) {
this.hide();
return;
Expand All @@ -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;
Expand Down Expand Up @@ -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<T>(array: ReadonlyArray<T | undefined | null>): T[] {
return <T[]>array.filter(e => !!e);
}
10 changes: 10 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,16 @@ export interface HoverProvider {
provideHover(model: monaco.editor.ITextModel, position: monaco.Position, token: monaco.CancellationToken): Hover | undefined | Thenable<Hover | undefined>;
}

export interface EvaluatableExpression {
range: Range;
expression?: string;
}

export interface EvaluatableExpressionProvider {
provideEvaluatableExpression(model: monaco.editor.ITextModel, position: monaco.Position,
token: monaco.CancellationToken): EvaluatableExpression | undefined | Thenable<EvaluatableExpression | undefined>;
}

export enum DocumentHighlightKind {
Text = 0,
Read = 1,
Expand Down
3 changes: 3 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
MarkerData,
SignatureHelp,
Hover,
EvaluatableExpression,
DocumentHighlight,
FormattingOptions,
ChainedCacheId,
Expand Down Expand Up @@ -1464,6 +1465,7 @@ export interface LanguagesExt {
): Promise<SignatureHelp | undefined>;
$releaseSignatureHelp(handle: number, id: number): void;
$provideHover(handle: number, resource: UriComponents, position: Position, token: CancellationToken): Promise<Hover | undefined>;
$provideEvaluatableExpression(handle: number, resource: UriComponents, position: Position, token: CancellationToken): Promise<EvaluatableExpression | undefined>;
$provideDocumentHighlights(handle: number, resource: UriComponents, position: Position, token: CancellationToken): Promise<DocumentHighlight[] | undefined>;
$provideDocumentFormattingEdits(handle: number, resource: UriComponents,
options: FormattingOptions, token: CancellationToken): Promise<TextEdit[] | undefined>;
Expand Down Expand Up @@ -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;
Expand Down
24 changes: 23 additions & 1 deletion packages/plugin-ext/src/main/browser/languages-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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<EvaluatableExpressionProvider>)
(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<EvaluatableExpression | undefined> {
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<monaco.languages.DocumentHighlightProvider>)
Expand Down
15 changes: 15 additions & 0 deletions packages/plugin-ext/src/plugin/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -100,6 +102,7 @@ import { serializeEnterRules, serializeIndentation, serializeRegExp } from './la
type Adapter = CompletionAdapter |
SignatureHelpAdapter |
HoverAdapter |
EvaluatableExpressionAdapter |
DocumentHighlightAdapter |
DocumentFormattingAdapter |
RangeFormattingAdapter |
Expand Down Expand Up @@ -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<EvaluatableExpression | undefined> {
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));
Expand Down
47 changes: 47 additions & 0 deletions packages/plugin-ext/src/plugin/languages/evaluatable-expression.ts
Original file line number Diff line number Diff line change
@@ -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<EvaluatableExpression | undefined> {
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);
});
}
}
5 changes: 5 additions & 0 deletions packages/plugin-ext/src/plugin/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ import {
SignatureHelp,
SignatureHelpTriggerKind,
Hover,
EvaluatableExpression,
DocumentHighlightKind,
DocumentHighlight,
DocumentLink,
Expand Down Expand Up @@ -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));
},
Expand Down Expand Up @@ -991,6 +995,7 @@ export function createAPIFactory(
SignatureHelp,
SignatureHelpTriggerKind,
Hover,
EvaluatableExpression,
DocumentHighlightKind,
DocumentHighlight,
DocumentLink,
Expand Down
7 changes: 7 additions & 0 deletions packages/plugin-ext/src/plugin/type-converters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,6 +396,13 @@ export function fromHover(hover: theia.Hover): model.Hover {
};
}

export function fromEvaluatableExpression(evaluatableExpression: theia.EvaluatableExpression): model.EvaluatableExpression {
return <model.EvaluatableExpression>{
range: fromRange(evaluatableExpression.range),
expression: evaluatableExpression.expression
};
}

export function fromLocation(location: theia.Location): model.Location {
return <model.Location>{
uri: location.uri,
Expand Down
18 changes: 18 additions & 0 deletions packages/plugin-ext/src/plugin/types-impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
60 changes: 60 additions & 0 deletions packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -9580,6 +9592,54 @@ export module '@theia/plugin' {
provideHover(document: TextDocument, position: Position, token: CancellationToken | undefined): ProviderResult<Hover>;
}

/**
* 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<EvaluatableExpression>;
}

/**
* A document highlight kind.
*/
Expand Down

0 comments on commit d1439da

Please sign in to comment.