Skip to content

Commit

Permalink
#10028 Support InlineValue feature (#11729)
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>

Resolves #10028
  • Loading branch information
ndoschek authored Oct 12, 2022
1 parent c0da67f commit 807cf6d
Show file tree
Hide file tree
Showing 11 changed files with 583 additions and 46 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
- [Previous Changelogs](https://github.com/eclipse-theia/theia/tree/master/doc/changelogs/)

## v1.31.0

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

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

- [core] the generated webpack configuration (`gen-webpack.config.js`) now exports an array of two webpack configs instead of a single one: the first contains the config for
Expand Down
218 changes: 176 additions & 42 deletions packages/debug/src/browser/editor/debug-inline-value-decorator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,18 +20,21 @@
*--------------------------------------------------------------------------------------------*/
// Based on https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts

import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { inject, injectable } from '@theia/core/shared/inversify';
import * as monaco from '@theia/monaco-editor-core';
import { CancellationTokenSource } from '@theia/monaco-editor-core/esm/vs/base/common/cancellation';
import { DEFAULT_WORD_REGEXP } from '@theia/monaco-editor-core/esm/vs/editor/common/core/wordHelper';
import { IDecorationOptions } from '@theia/monaco-editor-core/esm/vs/editor/common/editorCommon';
import { InlineValueContext, StandardTokenType } from '@theia/monaco-editor-core/esm/vs/editor/common/languages';
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 { DEFAULT_WORD_REGEXP } from '@theia/monaco-editor-core/esm/vs/editor/common/core/wordHelper';
import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application';
import { ILanguageFeaturesService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/languageFeatures';
import { StandaloneServices } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneServices';
import { MonacoEditorService } from '@theia/monaco/lib/browser/monaco-editor-service';
import { ExpressionContainer, DebugVariable } from '../console/debug-console-items';
import { DebugVariable, ExpressionContainer, ExpressionItem } from '../console/debug-console-items';
import { DebugPreferences } from '../debug-preferences';
import { DebugEditorModel } from './debug-editor-model';
import { DebugStackFrame } from '../model/debug-stack-frame';
import { DebugEditorModel } from './debug-editor-model';

// 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,63 +110,188 @@ 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 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 inlineValuesProvider = StandaloneServices.get(ILanguageFeaturesService).inlineValuesProvider;
const textEditorModel = debugEditorModel.editor.document.textEditorModel;

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

const findVariable = async (variableName: string, caseSensitiveLookup: boolean): Promise<DebugVariable | undefined> => {
const scopes = await stackFrame.getMostSpecificScopes(stackFrame.range!);
const key = caseSensitiveLookup ? variableName : variableName.toLowerCase();
for (const scope of scopes) {
const expressionContainers = await scope.getElements();
let container = expressionContainers.next();
while (!container.done) {
const debugVariable = container.value;
if (debugVariable && debugVariable instanceof DebugVariable) {
if (caseSensitiveLookup) {
if (debugVariable.name === key) {
return debugVariable;
}
} else {
if (debugVariable.name.toLowerCase() === key) {
return debugVariable;
}
}
}
container = expressionContainers.next();
}
}
return undefined;
};

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

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

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

for (const provider of registeredProviders) {
for (const visibleRange of visibleRanges) {
const result = await provider.provideInlineValues(textEditorModel, visibleRange, context, cancellationToken);
if (result) {
for (const inlineValue of result) {
let text: string | undefined = undefined;
switch (inlineValue.type) {
case 'text':
text = inlineValue.text;
break;
case 'variable': {
let varName = inlineValue.variableName;
if (!varName) {
const lineContent = model.getLineContent(inlineValue.range.startLineNumber);
varName = lineContent.substring(inlineValue.range.startColumn - 1, inlineValue.range.endColumn - 1);
}
const variable = await findVariable(varName, inlineValue.caseSensitiveLookup);
if (variable) {
text = this.formatInlineValue(varName, variable.value);
}
break;
}
case 'expression': {
let expr = inlineValue.expression;
if (!expr) {
const lineContent = model.getLineContent(inlineValue.range.startLineNumber);
expr = lineContent.substring(inlineValue.range.startColumn - 1, inlineValue.range.endColumn - 1);
}
if (expr) {
const expression = new ExpressionItem(expr, () => stackFrame.thread.session);
await expression.evaluate('watch');
if (expression.available) {
text = this.formatInlineValue(expr, expression.value);
}
}
break;
}
}

if (lineToNamesMap.get(position.lineNumber)!.indexOf(name) === -1) {
lineToNamesMap.get(position.lineNumber)!.push(name);
if (text) {
const line = inlineValue.range.startLineNumber;
let lineSegments = lineDecorations.get(line);
if (!lineSegments) {
lineSegments = [];
lineDecorations.set(line, lineSegments);
}
if (!lineSegments.some(segment => segment.text === text)) {
lineSegments.push(new InlineSegment(inlineValue.range.startColumn, text));
}
}
}
}
}
};

// sort line segments and concatenate them into a decoration
const separator = ', ';
lineDecorations.forEach((segments, line) => {
if (segments.length > 0) {
segments = segments.sort((a, b) => a.column - b.column);
const text = segments.map(s => s.text).join(separator);
decorations.push(this.createInlineValueDecoration(line, text));
}
});

} 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 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));
});
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);
}
}
}
}
});

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

protected formatInlineValue(...args: string[]): string {
const valuePattern = '{0} = {1}';
const formatRegExp = /{(\d+)}/g;
if (args.length === 0) {
return valuePattern;
}
return valuePattern.replace(formatRegExp, (match, group) => {
const idx = parseInt(group, 10);
return isNaN(idx) || idx < 0 || idx >= args.length ?
match :
args[idx];
});
}

// https://github.com/theia-ide/vscode/blob/standalone/0.19.x/src/vs/workbench/contrib/debug/browser/debugEditorContribution.ts#L454-L485
private createInlineValueDecoration(lineNumber: number, contentText: string): IDecorationOptions {
// If decoratorText is too long, trim and add ellipses. This could happen for minified files with everything on a single line
Expand Down
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 @@ -1490,6 +1492,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 @@ -1567,6 +1570,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
Loading

0 comments on commit 807cf6d

Please sign in to comment.