Skip to content

Commit

Permalink
eclipse-theia#10028 WIP Support InlineValues
Browse files Browse the repository at this point in the history
- Implement support for plugins providing inline values

Contributed on behalf of STMicroelectronics

Signed-off-by: Nina Doschek <ndoschek@eclipsesource.com>

Fixes eclipse-theia#10028
  • Loading branch information
ndoschek committed Jul 29, 2022
1 parent d1439da commit df5434f
Show file tree
Hide file tree
Showing 12 changed files with 487 additions and 44 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
## v1.29.0 - Unreleased

- [plugin] added support for `EvaluatableExpressions` [#11484](https://github.com/eclipse-theia/theia/pull/11484) - Contributed on behalf of STMicroelectronics
- [plugin] added support for `InlineValues` [#tbd](https://github.com/eclipse-theia/theia/pull/tbd) - Contributed on behalf of STMicroelectronics


## v1.28.0 - 7/28/2022

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
"browser": "yarn -s --cwd examples/browser",
"build": "yarn -s compile && yarn -s build:examples",
"build-p": "yarn -s compile && lerna run --scope=\"@theia/example-*\" bundle --parallel",
"build:examples": "yarn -s download:plugins && lerna run --scope=\"@theia/example-*\" bundle --parallel",
"build:examples": "lerna run --scope=\"@theia/example-*\" bundle --parallel",
"clean": "yarn -s rebuild:clean && yarn -s lint:clean && node scripts/run-reverse-topo.js yarn -s clean",
"compile": "echo Compiling TypeScript sources... && yarn -s compile:clean && yarn -s compile:tsc",
"compile:clean": "ts-clean dev-packages/* packages/*",
Expand Down
122 changes: 82 additions & 40 deletions packages/debug/src/browser/editor/debug-inline-value-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@ import { inject, injectable } from '@theia/core/shared/inversify';
import * as monaco from '@theia/monaco-editor-core';
import { IDecorationOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/editorCommon';
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';
import { StandardTokenType } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
import { InlineValueContext, StandardTokenType } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
import { DEFAULT_WORD_REGEXP } from '@theia/monaco-editor-core/esm/vs/editor/common/core/wordHelper';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { ExpressionContainer, DebugVariable } from '../console/debug-console-items';
import { DebugPreferences } from '../debug-preferences';
import { DebugEditorModel } from './debug-editor-model';
import { DebugStackFrame } from '../model/debug-stack-frame';
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';

// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L40-L43
export const INLINE_VALUE_DECORATION_KEY = 'inlinevaluedecoration';
Expand All @@ -48,6 +51,11 @@ const MAX_TOKENIZATION_LINE_LEN = 500; // If line is too long, then inline value
// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/base/common/uint.ts#L7-L13
const MAX_SAFE_SMALL_INTEGER = 1 << 30;

class InlineSegment {
constructor(public column: number, public text: string) {
}
}

@injectable()
export class DebugInlineValueDecorator implements FrontendApplicationContribution {

Expand All @@ -73,11 +81,12 @@ export class DebugInlineValueDecorator implements FrontendApplicationContributio
async calculateDecorations(debugEditorModel: DebugEditorModel, stackFrame: DebugStackFrame | undefined): Promise<IDecorationOptions[]> {
this.wordToLineNumbersMap = undefined;
const model = debugEditorModel.editor.getControl().getModel() || undefined;
return this.updateInlineValueDecorations(model, stackFrame);
return this.updateInlineValueDecorations(debugEditorModel, model, stackFrame);
}

// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L382-L408
protected async updateInlineValueDecorations(
debugEditorModel: DebugEditorModel,
model: monaco.editor.ITextModel | undefined,
stackFrame: DebugStackFrame | undefined): Promise<IDecorationOptions[]> {

Expand All @@ -101,59 +110,86 @@ export class DebugInlineValueDecorator implements FrontendApplicationContributio
range = range.setStartPosition(scope.range.startLineNumber, scope.range.startColumn);
}

return this.createInlineValueDecorationsInsideRange(children, range, model);
return this.createInlineValueDecorationsInsideRange(children, range, model, debugEditorModel, stackFrame);
}));

return decorationsPerScope.reduce((previous, current) => previous.concat(current), []);
}

// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L410-L452
private createInlineValueDecorationsInsideRange(
private async createInlineValueDecorationsInsideRange(
expressions: ReadonlyArray<ExpressionContainer>,
range: monaco.Range,
model: monaco.editor.ITextModel): IDecorationOptions[] {
model: monaco.editor.ITextModel,
debugEditorModel: DebugEditorModel,
stackFrame: DebugStackFrame): Promise<IDecorationOptions[]> {

const nameValueMap = new Map<string, string>();
for (const expr of expressions) {
if (expr instanceof DebugVariable) { // XXX: VS Code uses `IExpression` that has `name` and `value`.
nameValueMap.set(expr.name, expr.value);
}
// Limit the size of map. Too large can have a perf impact
if (nameValueMap.size >= MAX_NUM_INLINE_VALUES) {
break;
const decorations: IDecorationOptions[] = [];

const inlineValuesProvider = StandaloneServices.get(ILanguageFeaturesService).inlineValuesProvider;
const textEditorModel = debugEditorModel.editor.document.textEditorModel;

if (inlineValuesProvider && inlineValuesProvider.has(textEditorModel)) {

const context: InlineValueContext = {
frameId: stackFrame.raw.id,
stoppedLocation: range
};

const cancellationToken = new CancellationTokenSource().token;
const registeredProviders = inlineValuesProvider.ordered(textEditorModel).reverse();
// const ranges = debugEditorModel.editor.getVisibleRanges();

const lineDecorations = new Map<number, InlineSegment[]>();

const promises = registeredProviders.map(provider =>
Promise.resolve(provider.provideInlineValues(textEditorModel, range, context, cancellationToken)));


await Promise.all(promises);

} else { // use fallback if no provider was registered
const lineToNamesMap: Map<number, string[]> = new Map<number, string[]>();
const nameValueMap = new Map<string, string>();
for (const expr of expressions) {
if (expr instanceof DebugVariable) { // XXX: VS Code uses `IExpression` that has `name` and `value`.
nameValueMap.set(expr.name, expr.value);
}
// Limit the size of map. Too large can have a perf impact
if (nameValueMap.size >= MAX_NUM_INLINE_VALUES) {
break;
}
}
}

const lineToNamesMap: Map<number, string[]> = new Map<number, string[]>();
const wordToPositionsMap = this.getWordToPositionsMap(model);

// Compute unique set of names on each line
nameValueMap.forEach((_, name) => {
const positions = wordToPositionsMap.get(name);
if (positions) {
for (const position of positions) {
if (range.containsPosition(position)) {
if (!lineToNamesMap.has(position.lineNumber)) {
lineToNamesMap.set(position.lineNumber, []);
}
const wordToPositionsMap = this.getWordToPositionsMap(model);

// Compute unique set of names on each line
nameValueMap.forEach((_, name) => {
const positions = wordToPositionsMap.get(name);
if (positions) {
for (const position of positions) {
if (range.containsPosition(position)) {
if (!lineToNamesMap.has(position.lineNumber)) {
lineToNamesMap.set(position.lineNumber, []);
}

if (lineToNamesMap.get(position.lineNumber)!.indexOf(name) === -1) {
lineToNamesMap.get(position.lineNumber)!.push(name);
if (lineToNamesMap.get(position.lineNumber)!.indexOf(name) === -1) {
lineToNamesMap.get(position.lineNumber)!.push(name);
}
}
}
}
}
});

const decorations: IDecorationOptions[] = [];
// Compute decorators for each line
lineToNamesMap.forEach((names, line) => {
const contentText = names.sort((first, second) => {
const content = model.getLineContent(line);
return content.indexOf(first) - content.indexOf(second);
}).map(name => `${name} = ${nameValueMap.get(name)}`).join(', ');
decorations.push(this.createInlineValueDecoration(line, contentText));
});
});

// Compute decorators for each line
lineToNamesMap.forEach((names, line) => {
const contentText = names.sort((first, second) => {
const content = model.getLineContent(line);
return content.indexOf(first) - content.indexOf(second);
}).map(name => `${name} = ${nameValueMap.get(name)}`).join(', ');
decorations.push(this.createInlineValueDecoration(line, contentText));
});
}

return decorations;
}
Expand Down Expand Up @@ -240,3 +276,9 @@ export class DebugInlineValueDecorator implements FrontendApplicationContributio
}

}
/**
* @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);
}
32 changes: 32 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 @@ -265,6 +265,38 @@ export interface EvaluatableExpressionProvider {
token: monaco.CancellationToken): EvaluatableExpression | undefined | Thenable<EvaluatableExpression | undefined>;
}

export interface InlineValueContext {
frameId: number;
stoppedLocation: Range;
}

export interface InlineValueText {
type: 'text';
range: Range;
text: string;
}

export interface InlineValueVariableLookup {
type: 'variable';
range: Range;
variableName?: string;
caseSensitiveLookup: boolean;
}

export interface InlineValueEvaluatableExpression {
type: 'expression';
range: Range;
expression?: string;
}

export type InlineValue = InlineValueText | InlineValueVariableLookup | InlineValueEvaluatableExpression;

export interface InlineValuesProvider {
onDidChangeInlineValues?: TheiaEvent<void> | undefined;
provideInlineValues(model: monaco.editor.ITextModel, viewPort: Range, context: InlineValueContext, token: monaco.CancellationToken):
InlineValue[] | undefined | Thenable<InlineValue[] | undefined>;
}

export enum DocumentHighlightKind {
Text = 0,
Read = 1,
Expand Down
5 changes: 5 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import {
SignatureHelp,
Hover,
EvaluatableExpression,
InlineValue,
InlineValueContext,
DocumentHighlight,
FormattingOptions,
ChainedCacheId,
Expand Down Expand Up @@ -1466,6 +1468,7 @@ export interface LanguagesExt {
$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>;
$provideInlineValues(handle: number, resource: UriComponents, range: Range, context: InlineValueContext, token: CancellationToken): Promise<InlineValue[] | 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 @@ -1543,6 +1546,8 @@ export interface LanguagesMain {
$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;
$registerInlineValuesProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void;
$emitInlineValuesEvent(eventHandle: number, event?: any): 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
35 changes: 34 additions & 1 deletion packages/plugin-ext/src/main/browser/languages-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,13 @@ import { IRelativePattern } from '@theia/monaco-editor-core/esm/vs/base/common/g
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 {
EvaluatableExpression,
EvaluatableExpressionProvider,
InlineValue,
InlineValueContext,
InlineValuesProvider
} from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
import { ITextModel } from '@theia/monaco-editor-core/esm/vs/editor/common/model';

/**
Expand Down Expand Up @@ -360,6 +366,33 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable {
return this.proxy.$provideEvaluatableExpression(handle, model.uri, position, token);
}

$registerInlineValuesProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void {
const languageSelector = this.toLanguageSelector(selector);
const inlineValuesProvider = this.createInlineValuesProvider(handle);
this.register(handle,
(StandaloneServices.get(ILanguageFeaturesService).inlineValuesProvider.register as RegistrationFunction<InlineValuesProvider>)
(languageSelector, inlineValuesProvider));
}

protected createInlineValuesProvider(handle: number): InlineValuesProvider {
return {
provideInlineValues: (model, range, context, token) => this.provideInlineValues(handle, model, range, context, token)
};
}

protected provideInlineValues(handle: number, model: ITextModel, range: Range,
context: InlineValueContext, token: monaco.CancellationToken): monaco.languages.ProviderResult<InlineValue[] | undefined> {
return this.proxy.$provideInlineValues(handle, model.uri, range, context, token);
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
$emitInlineValuesEvent(eventHandle: number, event?: any): void {
const obj = this.services.get(eventHandle);
if (obj instanceof Emitter) {
obj.fire(event);
}
}

$registerDocumentHighlightProvider(handle: number, _pluginInfo: PluginInfo, selector: SerializedDocumentFilter[]): void {
const languageSelector = this.toLanguageSelector(selector);
const documentHighlightProvider = this.createDocumentHighlightProvider(handle);
Expand Down
25 changes: 24 additions & 1 deletion packages/plugin-ext/src/plugin/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,16 @@ import {
CallHierarchyIncomingCall,
CallHierarchyOutgoingCall,
LinkedEditingRanges,
EvaluatableExpression
EvaluatableExpression,
InlineValue,
InlineValueContext
} 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 { InlineValuesAdapter } from './languages/inline-values';
import { DocumentHighlightAdapter } from './languages/document-highlight';
import { DocumentFormattingAdapter } from './languages/document-formatting';
import { RangeFormattingAdapter } from './languages/range-formatting';
Expand Down Expand Up @@ -103,6 +106,7 @@ type Adapter = CompletionAdapter |
SignatureHelpAdapter |
HoverAdapter |
EvaluatableExpressionAdapter |
InlineValuesAdapter |
DocumentHighlightAdapter |
DocumentFormattingAdapter |
RangeFormattingAdapter |
Expand Down Expand Up @@ -365,6 +369,25 @@ export class LanguagesExtImpl implements LanguagesExt {
}
// ### EvaluatableExpression Provider end

// ### InlineValues Provider begin
registerInlineValuesProvider(selector: theia.DocumentSelector, provider: theia.InlineValuesProvider, pluginInfo: PluginInfo): theia.Disposable {
const eventHandle = typeof provider.onDidChangeInlineValues === 'function' ? this.nextCallId() : undefined;
const callId = this.addNewAdapter(new InlineValuesAdapter(provider, this.documents));
this.proxy.$registerInlineValuesProvider(callId, pluginInfo, this.transformDocumentSelector(selector));
let result = this.createDisposable(callId);

if (eventHandle !== undefined) {
const subscription = provider.onDidChangeInlineValues!(_ => this.proxy.$emitInlineValuesEvent(eventHandle));
result = Disposable.from(result, subscription);
}
return result;
}

$provideInlineValues(handle: number, resource: UriComponents, range: Range, context: InlineValueContext, token: theia.CancellationToken): Promise<InlineValue[] | undefined> {
return this.withAdapter(handle, InlineValuesAdapter, adapter => adapter.provideInlineValues(URI.revive(resource), range, context, token), undefined);
}
// ### InlineValue 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
Loading

0 comments on commit df5434f

Please sign in to comment.