diff --git a/CHANGELOG.md b/CHANGELOG.md index e5a32f08c1497..0e81a87b4442f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 + [Breaking Changes:](#breaking_changes_1.31.0) - [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 diff --git a/packages/debug/src/browser/editor/debug-inline-value-decorator.ts b/packages/debug/src/browser/editor/debug-inline-value-decorator.ts index a559163f56aeb..b3bfeb0b78874 100644 --- a/packages/debug/src/browser/editor/debug-inline-value-decorator.ts +++ b/packages/debug/src/browser/editor/debug-inline-value-decorator.ts @@ -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'; @@ -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 { @@ -73,11 +81,12 @@ export class DebugInlineValueDecorator implements FrontendApplicationContributio async calculateDecorations(debugEditorModel: DebugEditorModel, stackFrame: DebugStackFrame | undefined): Promise { 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 { @@ -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, range: monaco.Range, - model: monaco.editor.ITextModel): IDecorationOptions[] { + model: monaco.editor.ITextModel, + debugEditorModel: DebugEditorModel, + stackFrame: DebugStackFrame): Promise { - const nameValueMap = new Map(); - 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 = new Map(); - 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 => { + 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(); + + 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 = new Map(); + const nameValueMap = new Map(); + 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 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 3ac5ad8452ecd..4f14694c89ad9 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -265,6 +265,38 @@ export interface EvaluatableExpressionProvider { token: monaco.CancellationToken): EvaluatableExpression | undefined | Thenable; } +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 | undefined; + provideInlineValues(model: monaco.editor.ITextModel, viewPort: Range, context: InlineValueContext, token: monaco.CancellationToken): + InlineValue[] | 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 35110a13cb237..e16f64fb3125b 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -42,6 +42,8 @@ import { SignatureHelp, Hover, EvaluatableExpression, + InlineValue, + InlineValueContext, DocumentHighlight, FormattingOptions, ChainedCacheId, @@ -1490,6 +1492,7 @@ export interface LanguagesExt { $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; + $provideInlineValues(handle: number, resource: UriComponents, range: Range, context: InlineValueContext, token: CancellationToken): Promise; $provideDocumentHighlights(handle: number, resource: UriComponents, position: Position, token: CancellationToken): Promise; $provideDocumentFormattingEdits(handle: number, resource: UriComponents, options: FormattingOptions, token: CancellationToken): Promise; @@ -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; diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index 23a12b6f7fe19..ed08b07b5707f 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -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'; import { CodeActionTriggerKind } from '../../plugin/types-impl'; @@ -361,6 +367,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) + (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 { + 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); diff --git a/packages/plugin-ext/src/plugin/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index 136dd0e0293ec..860671c56457d 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -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'; @@ -103,6 +106,7 @@ type Adapter = CompletionAdapter | SignatureHelpAdapter | HoverAdapter | EvaluatableExpressionAdapter | + InlineValuesAdapter | DocumentHighlightAdapter | DocumentFormattingAdapter | RangeFormattingAdapter | @@ -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 { + 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)); diff --git a/packages/plugin-ext/src/plugin/languages/inline-values.ts b/packages/plugin-ext/src/plugin/languages/inline-values.ts new file mode 100644 index 0000000000000..0b608780ebfe4 --- /dev/null +++ b/packages/plugin-ext/src/plugin/languages/inline-values.ts @@ -0,0 +1,50 @@ +// ***************************************************************************** +// 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 { InlineValue, InlineValueContext, Range } from '../../common/plugin-api-rpc-model'; +import { DocumentsExtImpl } from '../documents'; +import * as Converter from '../type-converters'; + +export class InlineValuesAdapter { + + constructor( + private readonly provider: theia.InlineValuesProvider, + private readonly documents: DocumentsExtImpl + ) { } + + async provideInlineValues(resource: URI, range: Range, context: InlineValueContext, 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 viewPort = Converter.toRange(range); + const ctx = Converter.toInlineValueContext(context); + + return Promise.resolve(this.provider.provideInlineValues(document, viewPort, ctx, token)).then(inlineValue => { + if (!inlineValue) { + return undefined; + } + if (Array.isArray(inlineValue)) { + return inlineValue.map(iv => Converter.fromInlineValue(iv)); + } + return undefined; + }); + } +} diff --git a/packages/plugin-ext/src/plugin/plugin-context.ts b/packages/plugin-ext/src/plugin/plugin-context.ts index 4f9a383b0f5ea..2538c6089a851 100644 --- a/packages/plugin-ext/src/plugin/plugin-context.ts +++ b/packages/plugin-ext/src/plugin/plugin-context.ts @@ -79,6 +79,10 @@ import { SignatureHelpTriggerKind, Hover, EvaluatableExpression, + InlineValueEvaluatableExpression, + InlineValueText, + InlineValueVariableLookup, + InlineValueContext, DocumentHighlightKind, DocumentHighlight, DocumentLink, @@ -701,6 +705,9 @@ export function createAPIFactory( registerEvaluatableExpressionProvider(selector: theia.DocumentSelector, provider: theia.EvaluatableExpressionProvider): theia.Disposable { return languagesExt.registerEvaluatableExpressionProvider(selector, provider, pluginToPluginInfo(plugin)); }, + registerInlineValuesProvider(selector: theia.DocumentSelector, provider: theia.InlineValuesProvider): theia.Disposable { + return languagesExt.registerInlineValuesProvider(selector, provider, pluginToPluginInfo(plugin)); + }, registerDocumentHighlightProvider(selector: theia.DocumentSelector, provider: theia.DocumentHighlightProvider): theia.Disposable { return languagesExt.registerDocumentHighlightProvider(selector, provider, pluginToPluginInfo(plugin)); }, @@ -962,6 +969,10 @@ export function createAPIFactory( SignatureHelpTriggerKind, Hover, EvaluatableExpression, + InlineValueEvaluatableExpression, + InlineValueText, + InlineValueVariableLookup, + InlineValueContext, DocumentHighlightKind, DocumentHighlight, DocumentLink, diff --git a/packages/plugin-ext/src/plugin/type-converters.ts b/packages/plugin-ext/src/plugin/type-converters.ts index cc46dfb74f008..4b80b2b11a2f9 100644 --- a/packages/plugin-ext/src/plugin/type-converters.ts +++ b/packages/plugin-ext/src/plugin/type-converters.ts @@ -16,7 +16,7 @@ import * as theia from '@theia/plugin'; import * as lstypes from '@theia/core/shared/vscode-languageserver-protocol'; -import { QuickPickItemKind, URI } from './types-impl'; +import { InlineValueEvaluatableExpression, InlineValueText, InlineValueVariableLookup, QuickPickItemKind, URI } from './types-impl'; import * as rpc from '../common/plugin-api-rpc'; import { DecorationOptions, EditorPosition, Plugin, Position, WorkspaceTextEditDto, WorkspaceFileEditDto, Selection, TaskDto, WorkspaceEditDto @@ -401,6 +401,39 @@ export function fromEvaluatableExpression(evaluatableExpression: theia.Evaluatab }; } +export function fromInlineValue(inlineValue: theia.InlineValue): model.InlineValue { + if (inlineValue instanceof InlineValueText) { + return { + type: 'text', + range: fromRange(inlineValue.range), + text: inlineValue.text + }; + } else if (inlineValue instanceof InlineValueVariableLookup) { + return { + type: 'variable', + range: fromRange(inlineValue.range), + variableName: inlineValue.variableName, + caseSensitiveLookup: inlineValue.caseSensitiveLookup + }; + } else if (inlineValue instanceof InlineValueEvaluatableExpression) { + return { + type: 'expression', + range: fromRange(inlineValue.range), + expression: inlineValue.expression + }; + } else { + throw new Error('Unknown InlineValue type'); + } +} + +export function toInlineValueContext(inlineValueContext: model.InlineValueContext): theia.InlineValueContext { + const ivLocation = inlineValueContext.stoppedLocation; + return { + frameId: inlineValueContext.frameId, + stoppedLocation: new types.Range(ivLocation.startLineNumber, ivLocation.startColumn, ivLocation.endLineNumber, ivLocation.endColumn) + }; +} + export function fromLocation(location: theia.Location): model.Location { return { uri: location.uri, @@ -584,7 +617,7 @@ export namespace SymbolKind { } } -export function toCodeActionTriggerKind(triggerKind: model.CodeActionTriggerKind): types.CodeActionTriggerKind { +export function toCodeActionTriggerKind(triggerKind: model.CodeActionTriggerKind): types.CodeActionTriggerKind { switch (triggerKind) { case model.CodeActionTriggerKind.Invoke: return types.CodeActionTriggerKind.Invoke; @@ -1237,3 +1270,4 @@ export function pluginToPluginInfo(plugin: Plugin): rpc.PluginInfo { displayName: plugin.model.displayName }; } + diff --git a/packages/plugin-ext/src/plugin/types-impl.ts b/packages/plugin-ext/src/plugin/types-impl.ts index df97ec4b6f953..ca4ab2c4cc13b 100644 --- a/packages/plugin-ext/src/plugin/types-impl.ts +++ b/packages/plugin-ext/src/plugin/types-impl.ts @@ -1156,6 +1156,75 @@ export class EvaluatableExpression { } } +@es5ClassCompat +export class InlineValueContext implements theia.InlineValueContext { + public frameId: number; + public stoppedLocation: Range; + + constructor(frameId: number, stoppedLocation: Range) { + if (!frameId) { + illegalArgument('frameId must be defined'); + } + if (!stoppedLocation) { + illegalArgument('stoppedLocation must be defined'); + } + this.frameId = frameId; + this.stoppedLocation = stoppedLocation; + } +} + +@es5ClassCompat +export class InlineValueText implements theia.InlineValueText { + public type = 'text'; + public range: Range; + public text: string; + + constructor(range: Range, text: string) { + if (!range) { + illegalArgument('range must be defined'); + } + if (!text) { + illegalArgument('text must be defined'); + } + this.range = range; + this.text = text; + } +} + +@es5ClassCompat +export class InlineValueVariableLookup implements theia.InlineValueVariableLookup { + public type = 'variable'; + public range: Range; + public variableName?: string; + public caseSensitiveLookup: boolean; + + constructor(range: Range, variableName?: string, caseSensitiveLookup?: boolean) { + if (!range) { + illegalArgument('range must be defined'); + } + this.range = range; + this.caseSensitiveLookup = caseSensitiveLookup || true; + this.variableName = variableName; + } +} + +@es5ClassCompat +export class InlineValueEvaluatableExpression implements theia.InlineValueEvaluatableExpression { + public type = 'expression'; + 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 type InlineValue = InlineValueText | InlineValueVariableLookup | InlineValueEvaluatableExpression; + export enum DocumentHighlightKind { Text = 0, Read = 1, diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index aad9c56d5541c..bd47900aff704 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -9506,6 +9506,21 @@ export module '@theia/plugin' { */ export function registerEvaluatableExpressionProvider(selector: DocumentSelector, provider: EvaluatableExpressionProvider): Disposable; + /** + * Register a provider that returns data for the debugger's 'inline value' feature. + * Whenever the generic debugger has stopped in a source file, providers registered for the language of the file + * are called to return textual data that will be shown in the editor at the end of lines. + * + * Multiple providers can be registered for a language. In that case providers are asked in + * parallel and the results are merged. A failing provider (rejected promise or exception) will + * not cause a failure of the whole operation. + * + * @param selector A selector that defines the documents this provider is applicable to. + * @param provider An inline values provider. + * @return A {@link Disposable} that unregisters this provider when being disposed. + */ + export function registerInlineValuesProvider(selector: DocumentSelector, provider: InlineValuesProvider): Disposable; + /** * Register a workspace symbol provider. * @@ -9867,6 +9882,134 @@ export module '@theia/plugin' { provideEvaluatableExpression(document: TextDocument, position: Position, token: CancellationToken | undefined): ProviderResult; } + /** + * Provide inline value as text. + */ + export class InlineValueText { + /** + * The document range for which the inline value applies. + */ + readonly range: Range; + /** + * The text of the inline value. + */ + readonly text: string; + /** + * Creates a new InlineValueText object. + * + * @param range The document line where to show the inline value. + * @param text The value to be shown for the line. + */ + constructor(range: Range, text: string); + } + + /** + * Provide inline value through a variable lookup. + * If only a range is specified, the variable name will be extracted from the underlying document. + * An optional variable name can be used to override the extracted name. + */ + export class InlineValueVariableLookup { + /** + * The document range for which the inline value applies. + * The range is used to extract the variable name from the underlying document. + */ + readonly range: Range; + /** + * If specified the name of the variable to look up. + */ + readonly variableName?: string | undefined; + /** + * How to perform the lookup. + */ + readonly caseSensitiveLookup: boolean; + /** + * Creates a new InlineValueVariableLookup object. + * + * @param range The document line where to show the inline value. + * @param variableName The name of the variable to look up. + * @param caseSensitiveLookup How to perform the lookup. If missing lookup is case sensitive. + */ + constructor(range: Range, variableName?: string, caseSensitiveLookup?: boolean); + } + + /** + * Provide an inline value through an expression evaluation. + * 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. + */ + export class InlineValueEvaluatableExpression { + /** + * The document range for which the inline value applies. + * The range is used to extract the evaluatable expression from the underlying document. + */ + readonly range: Range; + /** + * If specified the expression overrides the extracted expression. + */ + readonly expression?: string | undefined; + /** + * Creates a new InlineValueEvaluatableExpression 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); + } + + /** + * Inline value information can be provided by different means: + * - directly as a text value (class InlineValueText). + * - as a name to use for a variable lookup (class InlineValueVariableLookup) + * - as an evaluatable expression (class InlineValueEvaluatableExpression) + * The InlineValue types combines all inline value types into one type. + */ + export type InlineValue = InlineValueText | InlineValueVariableLookup | InlineValueEvaluatableExpression; + + /** + * A value-object that contains contextual information when requesting inline values from a InlineValuesProvider. + */ + export interface InlineValueContext { + + /** + * The stack frame (as a DAP Id) where the execution has stopped. + */ + readonly frameId: number; + + /** + * The document range where execution has stopped. + * Typically the end position of the range denotes the line where the inline values are shown. + */ + readonly stoppedLocation: Range; + } + + /** + * The inline values provider interface defines the contract between extensions and the editor's debugger inline values feature. + * In this contract the provider returns inline value information for a given document range + * and the editor shows this information in the editor at the end of lines. + */ + export interface InlineValuesProvider { + + /** + * An optional event to signal that inline values have changed. + * @see {@link EventEmitter} + */ + onDidChangeInlineValues?: Event | undefined; + + /** + * Provide "inline value" information for a given document and range. + * The editor calls this method whenever debugging stops in the given document. + * The returned inline values information is rendered in the editor at the end of lines. + * + * @param document The document for which the inline values information is needed. + * @param viewPort The visible document range for which inline values should be computed. + * @param context A bag containing contextual information like the current location. + * @param token A cancellation token. + * @return An array of InlineValueDescriptors or a thenable that resolves to such. The lack of a result can be + * signaled by returning `undefined` or `null`. + */ + provideInlineValues(document: TextDocument, viewPort: Range, context: InlineValueContext, token: CancellationToken): ProviderResult; + } + /** * A document highlight kind. */