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