Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#10027 Support EvaluatableExpressions #11484

Merged
merged 1 commit into from
Aug 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

<a name="breaking_changes_1.29.0">[Breaking Changes:](#breaking_changes_1.29.0)</a>

- [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)
Expand Down
7 changes: 7 additions & 0 deletions packages/core/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>(array: ReadonlyArray<T | undefined | null>): T[] {
return <T[]>array.filter(e => !!e);
}
}

/**
Expand Down
35 changes: 34 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,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
Expand Down Expand Up @@ -146,6 +151,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 +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;
Expand Down
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 @@ -1470,6 +1471,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 @@ -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;
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 @@ -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.
*
Expand Down Expand Up @@ -9582,6 +9594,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