diff --git a/src/vs/editor/contrib/codeAction/codeAction.ts b/src/vs/editor/contrib/codeAction/codeAction.ts index 62c19dcf150..2cfc82d83cc 100644 --- a/src/vs/editor/contrib/codeAction/codeAction.ts +++ b/src/vs/editor/contrib/codeAction/codeAction.ts @@ -66,7 +66,7 @@ export function getCodeActions( const filter = trigger.filter || {}; const codeActionContext: CodeActionContext = { - only: filter.kind?.value, + only: filter.include?.value, trigger: trigger.type === 'manual' ? CodeActionTriggerKind.Manual : CodeActionTriggerKind.Automatic }; @@ -146,7 +146,7 @@ registerLanguageCommand('_executeCodeActionProvider', async function (accessor, const codeActionSet = await getCodeActions( model, validatedRangeOrSelection, - { type: 'manual', filter: { includeSourceActions: true, kind: kind && kind.value ? new CodeActionKind(kind.value) : undefined } }, + { type: 'manual', filter: { includeSourceActions: true, include: kind && kind.value ? new CodeActionKind(kind.value) : undefined } }, CancellationToken.None); setTimeout(() => codeActionSet.dispose(), 100); diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index ba92b413603..c555ceec6d6 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -240,7 +240,7 @@ export class CodeActionCommand extends EditorCommand { ? nls.localize('editor.action.codeAction.noneMessage.preferred', "No preferred code actions available") : nls.localize('editor.action.codeAction.noneMessage', "No code actions available"), { - kind: args.kind, + include: args.kind, includeSourceActions: true, onlyIncludePreferredActions: args.preferred, }, @@ -293,7 +293,7 @@ export class RefactorAction extends EditorAction { ? nls.localize('editor.action.refactor.noneMessage.preferred', "No preferred refactorings available") : nls.localize('editor.action.refactor.noneMessage', "No refactorings available"), { - kind: CodeActionKind.Refactor.contains(args.kind) ? args.kind : CodeActionKind.None, + include: CodeActionKind.Refactor.contains(args.kind) ? args.kind : CodeActionKind.None, onlyIncludePreferredActions: args.preferred, }, args.apply); @@ -336,7 +336,7 @@ export class SourceAction extends EditorAction { ? nls.localize('editor.action.source.noneMessage.preferred', "No preferred source actions available") : nls.localize('editor.action.source.noneMessage', "No source actions available"), { - kind: CodeActionKind.Source.contains(args.kind) ? args.kind : CodeActionKind.None, + include: CodeActionKind.Source.contains(args.kind) ? args.kind : CodeActionKind.None, includeSourceActions: true, onlyIncludePreferredActions: args.preferred, }, @@ -365,7 +365,7 @@ export class OrganizeImportsAction extends EditorAction { public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { return triggerCodeActionsForEditorSelection(editor, nls.localize('editor.action.organize.noneMessage', "No organize imports action available"), - { kind: CodeActionKind.SourceOrganizeImports, includeSourceActions: true }, + { include: CodeActionKind.SourceOrganizeImports, includeSourceActions: true }, CodeActionAutoApply.IfSingle); } } @@ -386,7 +386,7 @@ export class FixAllAction extends EditorAction { public run(_accessor: ServicesAccessor, editor: ICodeEditor): void { return triggerCodeActionsForEditorSelection(editor, nls.localize('fixAll.noneMessage', "No fix all action available"), - { kind: CodeActionKind.SourceFixAll, includeSourceActions: true }, + { include: CodeActionKind.SourceFixAll, includeSourceActions: true }, CodeActionAutoApply.IfSingle); } } @@ -418,7 +418,7 @@ export class AutoFixAction extends EditorAction { return triggerCodeActionsForEditorSelection(editor, nls.localize('editor.action.autoFix.noneMessage', "No auto fixes available"), { - kind: CodeActionKind.QuickFix, + include: CodeActionKind.QuickFix, onlyIncludePreferredActions: true }, CodeActionAutoApply.IfSingle); diff --git a/src/vs/editor/contrib/codeAction/codeActionUi.ts b/src/vs/editor/contrib/codeAction/codeActionUi.ts index 8db5d28fa24..5dfd9228ff0 100644 --- a/src/vs/editor/contrib/codeAction/codeActionUi.ts +++ b/src/vs/editor/contrib/codeAction/codeActionUi.ts @@ -75,7 +75,7 @@ export class CodeActionUi extends Disposable { } if (newState.trigger.type === 'manual') { - if (newState.trigger.filter && newState.trigger.filter.kind) { + if (newState.trigger.filter && newState.trigger.filter.include) { // Triggered for specific scope if (actions.actions.length > 0) { // Apply if we only have one action or requested autoApply diff --git a/src/vs/editor/contrib/codeAction/test/codeAction.test.ts b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts index a85b4a1ea58..730d96fc9a5 100644 --- a/src/vs/editor/contrib/codeAction/test/codeAction.test.ts +++ b/src/vs/editor/contrib/codeAction/test/codeAction.test.ts @@ -140,20 +140,20 @@ suite('CodeAction', () => { disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider)); { - const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a') } }, CancellationToken.None); + const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { include: new CodeActionKind('a') } }, CancellationToken.None); assert.equal(actions.length, 2); assert.strictEqual(actions[0].title, 'a'); assert.strictEqual(actions[1].title, 'a.b'); } { - const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a.b') } }, CancellationToken.None); + const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { include: new CodeActionKind('a.b') } }, CancellationToken.None); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a.b'); } { - const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a.b.c') } }, CancellationToken.None); + const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { include: new CodeActionKind('a.b.c') } }, CancellationToken.None); assert.equal(actions.length, 0); } }); @@ -172,7 +172,7 @@ suite('CodeAction', () => { disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider)); - const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: new CodeActionKind('a') } }, CancellationToken.None); + const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { include: new CodeActionKind('a') } }, CancellationToken.None); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a'); }); @@ -192,12 +192,34 @@ suite('CodeAction', () => { } { - const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { kind: CodeActionKind.Source, includeSourceActions: true } }, CancellationToken.None); + const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { include: CodeActionKind.Source, includeSourceActions: true } }, CancellationToken.None); assert.equal(actions.length, 1); assert.strictEqual(actions[0].title, 'a'); } }); + test('getCodeActions should support filtering out some requested source code actions #84602', async function () { + const provider = staticCodeActionProvider( + { title: 'a', kind: CodeActionKind.Source.value }, + { title: 'b', kind: CodeActionKind.Source.append('test').value }, + { title: 'c', kind: 'c' } + ); + + disposables.add(modes.CodeActionProviderRegistry.register('fooLang', provider)); + + { + const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { + type: 'auto', filter: { + include: CodeActionKind.Source.append('test'), + excludes: [CodeActionKind.Source], + includeSourceActions: true, + } + }, CancellationToken.None); + assert.equal(actions.length, 1); + assert.strictEqual(actions[0].title, 'b'); + } + }); + test('getCodeActions should not invoke code action providers filtered out by providedCodeActionKinds', async function () { let wasInvoked = false; const provider = new class implements modes.CodeActionProvider { @@ -214,7 +236,7 @@ suite('CodeAction', () => { const { actions } = await getCodeActions(model, new Range(1, 1, 2, 1), { type: 'auto', filter: { - kind: CodeActionKind.QuickFix + include: CodeActionKind.QuickFix } }, CancellationToken.None); assert.strictEqual(actions.length, 0); diff --git a/src/vs/editor/contrib/codeAction/types.ts b/src/vs/editor/contrib/codeAction/types.ts index ffbf8bbe94d..650be89e98c 100644 --- a/src/vs/editor/contrib/codeAction/types.ts +++ b/src/vs/editor/contrib/codeAction/types.ts @@ -46,19 +46,20 @@ export const enum CodeActionAutoApply { } export interface CodeActionFilter { - readonly kind?: CodeActionKind; + readonly include?: CodeActionKind; + readonly excludes?: readonly CodeActionKind[]; readonly includeSourceActions?: boolean; readonly onlyIncludePreferredActions?: boolean; } export function mayIncludeActionsOfKind(filter: CodeActionFilter, providedKind: CodeActionKind): boolean { // A provided kind may be a subset or superset of our filtered kind. - if (filter.kind && !filter.kind.intersects(providedKind)) { + if (filter.include && !filter.include.intersects(providedKind)) { return false; } // Don't return source actions unless they are explicitly requested - if (CodeActionKind.Source.contains(providedKind) && !filter.includeSourceActions) { + if (!filter.includeSourceActions && CodeActionKind.Source.contains(providedKind)) { return false; } @@ -69,8 +70,17 @@ export function filtersAction(filter: CodeActionFilter, action: CodeAction): boo const actionKind = action.kind ? new CodeActionKind(action.kind) : undefined; // Filter out actions by kind - if (filter.kind) { - if (!actionKind || !filter.kind.contains(actionKind)) { + if (filter.include) { + if (!actionKind || !filter.include.contains(actionKind)) { + return false; + } + } + + if (filter.excludes) { + if (actionKind && filter.excludes.some(exclude => { + // Excludes are overwritten by includes + return exclude.contains(actionKind) && (!filter.include || !filter.include.contains(actionKind)); + })) { return false; } } diff --git a/src/vs/editor/contrib/hover/modesContentHover.ts b/src/vs/editor/contrib/hover/modesContentHover.ts index 4efa784f4be..f37254d776f 100644 --- a/src/vs/editor/contrib/hover/modesContentHover.ts +++ b/src/vs/editor/contrib/hover/modesContentHover.ts @@ -586,7 +586,7 @@ export class ModesContentHoverWidget extends ContentHoverWidget { return getCodeActions( this._editor.getModel()!, new Range(marker.startLineNumber, marker.startColumn, marker.endLineNumber, marker.endColumn), - { type: 'manual', filter: { kind: CodeActionKind.QuickFix } }, + { type: 'manual', filter: { include: CodeActionKind.QuickFix } }, cancellationToken); }); } diff --git a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts index 5fdd5b7c951..3e79332f086 100644 --- a/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/browser/mainThreadSaveParticipant.ts @@ -282,6 +282,10 @@ class CodeActionOnSaveParticipant implements ISaveParticipant { return undefined; } + const excludedActions = Object.keys(setting) + .filter(x => setting[x] === false) + .map(x => new CodeActionKind(x)); + const tokenSource = new CancellationTokenSource(); const timeout = this._configurationService.getValue('editor.codeActionsOnSaveTimeout', settingsOverrides); @@ -292,15 +296,15 @@ class CodeActionOnSaveParticipant implements ISaveParticipant { tokenSource.cancel(); reject(localize('codeActionsOnSave.didTimeout', "Aborted codeActionsOnSave after {0}ms", timeout)); }, timeout)), - this.applyOnSaveActions(model, codeActionsOnSave, tokenSource.token) + this.applyOnSaveActions(model, codeActionsOnSave, excludedActions, tokenSource.token) ]).finally(() => { tokenSource.cancel(); }); } - private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: CodeActionKind[], token: CancellationToken): Promise { + private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], token: CancellationToken): Promise { for (const codeActionKind of codeActionsOnSave) { - const actionsToRun = await this.getActionsToRun(model, codeActionKind, token); + const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, token); try { await this.applyCodeActions(actionsToRun.actions); } catch { @@ -317,10 +321,10 @@ class CodeActionOnSaveParticipant implements ISaveParticipant { } } - private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, token: CancellationToken) { + private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], token: CancellationToken) { return getCodeActions(model, model.getFullModelRange(), { type: 'auto', - filter: { kind: codeActionKind, includeSourceActions: true }, + filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true }, }, token); } } diff --git a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts index c085096f822..2201cbe11d0 100644 --- a/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts +++ b/src/vs/workbench/contrib/markers/browser/markersTreeViewer.ts @@ -551,7 +551,7 @@ export class MarkerViewModel extends Disposable { if (model) { if (!this.codeActionsPromise) { this.codeActionsPromise = createCancelablePromise(cancellationToken => { - return getCodeActions(model, new Range(this.marker.range.startLineNumber, this.marker.range.startColumn, this.marker.range.endLineNumber, this.marker.range.endColumn), { type: 'manual', filter: { kind: CodeActionKind.QuickFix } }, cancellationToken).then(actions => { + return getCodeActions(model, new Range(this.marker.range.startLineNumber, this.marker.range.startColumn, this.marker.range.endLineNumber, this.marker.range.endColumn), { type: 'manual', filter: { include: CodeActionKind.QuickFix } }, cancellationToken).then(actions => { return this._register(actions); }); });