diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d7e49f45c4c9..80f58b2fcc679 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ [1.23.0 Milestone](https://github.com/eclipse-theia/theia/milestone/31) - [plugin-ext] add more detail to logging of backend and frontend start-up, especially in plugin management [#10407](https://github.com/eclipse-theia/theia/pull/10407) - Contributed on behalf of STMicroelectronics +- [plugin] Added support for `vscode.CodeActionProvider.resolveCodeAction` [#10730](https://github.com/eclipse-theia/theia/pull/10730) - Contributed on behalf of STMicroelectronics [Breaking Changes:](#breaking_changes_1.23.0) 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 ee7df56efad68..7f11f73c93878 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc-model.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc-model.ts @@ -338,6 +338,7 @@ export interface CodeLensSymbol { } export interface CodeAction { + cacheId: number; title: string; command?: Command; edit?: WorkspaceEdit; diff --git a/packages/plugin-ext/src/common/plugin-api-rpc.ts b/packages/plugin-ext/src/common/plugin-api-rpc.ts index ff6ac37658d8a..2525a29e3974f 100644 --- a/packages/plugin-ext/src/common/plugin-api-rpc.ts +++ b/packages/plugin-ext/src/common/plugin-api-rpc.ts @@ -72,7 +72,7 @@ import { CommentOptions, CommentThreadCollapsibleState, CommentThread, - CommentThreadChangedEvent, + CommentThreadChangedEvent } from './plugin-api-rpc-model'; import { ExtPluginApi } from './plugin-ext-api-contribution'; import { KeysToAnyValues, KeysToKeysToAnyValue } from './types'; @@ -1441,6 +1441,8 @@ export interface LanguagesExt { context: CodeActionContext, token: CancellationToken ): Promise; + $releaseCodeActions(handle: number, cacheIds: number[]): void; + $resolveCodeAction(handle: number, cacheId: number, token: CancellationToken): Promise; $provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise; $provideWorkspaceSymbols(handle: number, query: string, token: CancellationToken): PromiseLike; $resolveWorkspaceSymbol(handle: number, symbol: SymbolInformation, token: CancellationToken): PromiseLike; diff --git a/packages/plugin-ext/src/main/browser/languages-main.ts b/packages/plugin-ext/src/main/browser/languages-main.ts index f04e26b0f082a..8d54e486463f9 100644 --- a/packages/plugin-ext/src/main/browser/languages-main.ts +++ b/packages/plugin-ext/src/main/browser/languages-main.ts @@ -718,6 +718,8 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { const markers = monaco.services.StaticServices.markerService.get().read({ resource: model.uri }).filter(m => monaco.Range.areIntersectingOrTouching(m, range)); return this.provideCodeActions(handle, model, range, { markers, only: context.only }, token); }, + resolveCodeAction: (codeAction: monaco.languages.CodeAction, token: monaco.CancellationToken): Promise => + this.resolveCodeAction(handle, codeAction, token), providedCodeActionKinds }; this.register(handle, monaco.modes.CodeActionProviderRegistry.register(languageSelector, quickFixProvider)); @@ -734,12 +736,20 @@ export class LanguagesMainImpl implements LanguagesMain, Disposable { } return { actions: actions.map(a => toMonacoAction(a)), - dispose: () => { - // TODO this.proxy.$releaseCodeActions(handle, cacheId); - } + dispose: () => this.proxy.$releaseCodeActions(handle, actions.map(a => a.cacheId)) }; } + protected async resolveCodeAction(handle: number, codeAction: monaco.languages.CodeAction, token: monaco.CancellationToken): Promise { + // The cacheId is kept in toMonacoAction when converting a received CodeAction DTO to a monaco code action + const cacheId = (codeAction as CodeAction).cacheId; + if (cacheId !== undefined) { + const resolvedEdit = await this.proxy.$resolveCodeAction(handle, cacheId, token); + codeAction.edit = resolvedEdit && toMonacoWorkspaceEdit(resolvedEdit); + } + return codeAction; + } + $registerRenameProvider(handle: number, pluginInfo: PluginInfo, selector: SerializedDocumentFilter[], supportsResolveLocation: boolean): void { const languageSelector = this.toLanguageSelector(selector); const renameProvider = this.createRenameProvider(handle, supportsResolveLocation); diff --git a/packages/plugin-ext/src/plugin/languages.ts b/packages/plugin-ext/src/plugin/languages.ts index c21a8fd697279..ccdc2c9582538 100644 --- a/packages/plugin-ext/src/plugin/languages.ts +++ b/packages/plugin-ext/src/plugin/languages.ts @@ -463,6 +463,15 @@ export class LanguagesExtImpl implements LanguagesExt { ): Promise { return this.withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeAction(URI.revive(resource), rangeOrSelection, context, token), undefined); } + + $releaseCodeActions(handle: number, cacheIds: number[]): void { + this.withAdapter(handle, CodeActionAdapter, adapter => adapter.releaseCodeActions(cacheIds), undefined); + } + + $resolveCodeAction(handle: number, cacheId: number, token: theia.CancellationToken): Promise { + return this.withAdapter(handle, CodeActionAdapter, adapter => adapter.resolveCodeAction(cacheId, token), undefined); + }; + // ### Code Actions Provider end // ### Code Lens Provider begin diff --git a/packages/plugin-ext/src/plugin/languages/code-action.ts b/packages/plugin-ext/src/plugin/languages/code-action.ts index c9d077e73ee4b..5a4a6272bad95 100644 --- a/packages/plugin-ext/src/plugin/languages/code-action.ts +++ b/packages/plugin-ext/src/plugin/languages/code-action.ts @@ -16,7 +16,7 @@ import * as theia from '@theia/plugin'; import { URI } from '@theia/core/shared/vscode-uri'; -import { Selection } from '../../common/plugin-api-rpc'; +import { Selection, WorkspaceEditDto } from '../../common/plugin-api-rpc'; import { Range, CodeActionContext, CodeAction } from '../../common/plugin-api-rpc-model'; import * as Converter from '../type-converters'; import { DocumentsExtImpl } from '../documents'; @@ -35,6 +35,11 @@ export class CodeActionAdapter { private readonly commands: CommandRegistryImpl ) { } + private readonly cache = new Map(); + private readonly disposables = new Map(); + + private cacheId = 0; + async provideCodeAction(resource: URI, rangeOrSelection: Range | Selection, context: CodeActionContext, token: theia.CancellationToken): Promise { const document = this.document.getDocumentData(resource); @@ -64,15 +69,21 @@ export class CodeActionAdapter { if (!Array.isArray(commandsOrActions) || commandsOrActions.length === 0) { return undefined; } - // TODO cache toDispose and dispose it - const toDispose = new DisposableCollection(); const result: CodeAction[] = []; for (const candidate of commandsOrActions) { if (!candidate) { continue; } + + // Cache candidates and created commands. + const nextCacheId = this.nextCacheId(); + const toDispose = new DisposableCollection(); + this.cache.set(nextCacheId, candidate); + this.disposables.set(nextCacheId, toDispose); + if (CodeActionAdapter._isCommand(candidate)) { result.push({ + cacheId: nextCacheId, title: candidate.title || '', command: this.commands.converter.toSafeCommand(candidate, toDispose) }); @@ -88,6 +99,7 @@ export class CodeActionAdapter { } result.push({ + cacheId: nextCacheId, title: candidate.title, command: this.commands.converter.toSafeCommand(candidate.command, toDispose), diagnostics: candidate.diagnostics && candidate.diagnostics.map(Converter.convertDiagnosticToMarkerData), @@ -100,6 +112,37 @@ export class CodeActionAdapter { return result; } + async releaseCodeActions(cacheIds: number[]): Promise { + cacheIds.forEach(id => { + this.cache.delete(id); + const toDispose = this.disposables.get(id); + if (toDispose) { + toDispose.dispose(); + this.disposables.delete(id); + } + }); + } + + async resolveCodeAction(cacheId: number, token: theia.CancellationToken): Promise { + if (!this.provider.resolveCodeAction) { + return undefined; + } + + // Code actions are only resolved if they are not legacy commands and don't have an edit property + // https://code.visualstudio.com/api/references/vscode-api#CodeActionProvider + const candidate = this.cache.get(cacheId); + if (!candidate || CodeActionAdapter._isCommand(candidate) || candidate.edit) { + return undefined; + } + + const resolved = await this.provider.resolveCodeAction(candidate, token); + return resolved?.edit && Converter.fromWorkspaceEdit(resolved.edit); + } + + private nextCacheId(): number { + return this.cacheId++; + } + // eslint-disable-next-line @typescript-eslint/no-explicit-any private static _isCommand(smth: any): smth is theia.Command { return typeof (smth).command === 'string'; diff --git a/packages/plugin/src/theia.d.ts b/packages/plugin/src/theia.d.ts index 06a0819b5bee3..52d2c66d6e009 100644 --- a/packages/plugin/src/theia.d.ts +++ b/packages/plugin/src/theia.d.ts @@ -7826,6 +7826,22 @@ export module '@theia/plugin' { context: CodeActionContext, token: CancellationToken | undefined ): ProviderResult<(Command | CodeAction)[]>; + + /** + * Given a code action fill in its `edit`-property. Changes to + * all other properties, like title, are ignored. A code action that has an edit + * will not be resolved. + * + * *Note* that a code action provider that returns commands, not code actions, cannot successfully + * implement this function. Returning commands is deprecated and instead code actions should be + * returned. + * + * @param codeAction A code action. + * @param token A cancellation token. + * @return The resolved code action or a thenable that resolves to such. It is OK to return the given + * `item`. When no result is returned, the given `item` will be used. + */ + resolveCodeAction?(codeAction: CodeAction, token: CancellationToken | undefined): ProviderResult; } /**