Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CodeActionProvider#resolveCodeAction to plugin API #10730

Merged
merged 1 commit into from
Feb 16, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

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

Expand Down
1 change: 1 addition & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -338,6 +338,7 @@ export interface CodeLensSymbol {
}

export interface CodeAction {
cacheId: number;
title: string;
command?: Command;
edit?: WorkspaceEdit;
Expand Down
4 changes: 3 additions & 1 deletion packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -1441,6 +1441,8 @@ export interface LanguagesExt {
context: CodeActionContext,
token: CancellationToken
): Promise<CodeAction[] | undefined>;
$releaseCodeActions(handle: number, cacheIds: number[]): void;
$resolveCodeAction(handle: number, cacheId: number, token: CancellationToken): Promise<WorkspaceEditDto | undefined>;
$provideDocumentSymbols(handle: number, resource: UriComponents, token: CancellationToken): Promise<DocumentSymbol[] | undefined>;
$provideWorkspaceSymbols(handle: number, query: string, token: CancellationToken): PromiseLike<SymbolInformation[]>;
$resolveWorkspaceSymbol(handle: number, symbol: SymbolInformation, token: CancellationToken): PromiseLike<SymbolInformation | undefined>;
Expand Down
16 changes: 13 additions & 3 deletions packages/plugin-ext/src/main/browser/languages-main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<monaco.languages.CodeAction> =>
this.resolveCodeAction(handle, codeAction, token),
providedCodeActionKinds
};
this.register(handle, monaco.modes.CodeActionProviderRegistry.register(languageSelector, quickFixProvider));
Expand All @@ -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<monaco.languages.CodeAction> {
// 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);
Expand Down
9 changes: 9 additions & 0 deletions packages/plugin-ext/src/plugin/languages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,6 +463,15 @@ export class LanguagesExtImpl implements LanguagesExt {
): Promise<CodeAction[] | undefined> {
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<WorkspaceEditDto | undefined> {
return this.withAdapter(handle, CodeActionAdapter, adapter => adapter.resolveCodeAction(cacheId, token), undefined);
};

// ### Code Actions Provider end

// ### Code Lens Provider begin
Expand Down
49 changes: 46 additions & 3 deletions packages/plugin-ext/src/plugin/languages/code-action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -35,6 +35,11 @@ export class CodeActionAdapter {
private readonly commands: CommandRegistryImpl
) { }

private readonly cache = new Map<number, theia.CodeAction | theia.Command>();
private readonly disposables = new Map<number, DisposableCollection>();

private cacheId = 0;

async provideCodeAction(resource: URI, rangeOrSelection: Range | Selection,
context: CodeActionContext, token: theia.CancellationToken): Promise<CodeAction[] | undefined> {
const document = this.document.getDocumentData(resource);
Expand Down Expand Up @@ -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)
});
Expand All @@ -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),
Expand All @@ -100,6 +112,37 @@ export class CodeActionAdapter {
return result;
}

async releaseCodeActions(cacheIds: number[]): Promise<void> {
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<WorkspaceEditDto | undefined> {
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 (<theia.Command>smth).command === 'string';
Expand Down
16 changes: 16 additions & 0 deletions packages/plugin/src/theia.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CodeAction>;
}

/**
Expand Down