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

Notebook CodeActionKind Support #192248

Merged
merged 11 commits into from
Sep 18, 2023
10 changes: 8 additions & 2 deletions src/vs/editor/contrib/codeAction/browser/codeAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as nls from 'vs/nls';
import { coalesce, equals, isNonEmptyArray } from 'vs/base/common/arrays';
import { CancellationToken } from 'vs/base/common/cancellation';
import { illegalArgument, isCancellationError, onUnexpectedExternalError } from 'vs/base/common/errors';
Expand All @@ -18,7 +19,6 @@ import { ITextModel } from 'vs/editor/common/model';
import { ILanguageFeaturesService } from 'vs/editor/common/services/languageFeatures';
import { IModelService } from 'vs/editor/common/services/model';
import { TextModelCancellationTokenSource } from 'vs/editor/contrib/editorState/browser/editorState';
import * as nls from 'vs/nls';
import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { INotificationService } from 'vs/platform/notification/common/notification';
Expand Down Expand Up @@ -89,14 +89,20 @@ export async function getCodeActions(
token: CancellationToken,
): Promise<CodeActionSet> {
const filter = trigger.filter || {};
const notebookFilter: CodeActionFilter = {
...filter,
excludes: [...(filter.excludes || []), CodeActionKind.Notebook],
};

const codeActionContext: languages.CodeActionContext = {
only: filter.include?.value,
trigger: trigger.type,
};

const cts = new TextModelCancellationTokenSource(model, token);
const providers = getCodeActionProviders(registry, model, filter);
// if the trigger is auto (autosave, lightbulb, etc), we should exclude notebook codeActions
const excludeNotebookCodeActions = (trigger.type === languages.CodeActionTriggerType.Auto);
const providers = getCodeActionProviders(registry, model, (excludeNotebookCodeActions) ? notebookFilter : filter);

const disposables = new DisposableStore();
const promises = providers.map(async provider => {
Expand Down
1 change: 1 addition & 0 deletions src/vs/editor/contrib/codeAction/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export class CodeActionKind {
public static readonly RefactorInline = CodeActionKind.Refactor.append('inline');
public static readonly RefactorMove = CodeActionKind.Refactor.append('move');
public static readonly RefactorRewrite = CodeActionKind.Refactor.append('rewrite');
public static readonly Notebook = new CodeActionKind('notebook');
public static readonly Source = new CodeActionKind('source');
public static readonly SourceOrganizeImports = CodeActionKind.Source.append('organizeImports');
public static readonly SourceFixAll = CodeActionKind.Source.append('fixAll');
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1398,6 +1398,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
CandidatePortSource: CandidatePortSource,
CodeAction: extHostTypes.CodeAction,
CodeActionKind: extHostTypes.CodeActionKind,
CodeActionKind2: extHostTypes.CodeActionKind,
CodeActionTriggerKind: extHostTypes.CodeActionTriggerKind,
CodeLens: extHostTypes.CodeLens,
Color: extHostTypes.Color,
Expand Down Expand Up @@ -1578,7 +1579,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
InteractiveEditorResponseFeedbackKind: extHostTypes.InteractiveEditorResponseFeedbackKind,
StackFrameFocus: extHostTypes.StackFrameFocus,
ThreadFocus: extHostTypes.ThreadFocus,
NotebookCodeActionKind: extHostTypes.NotebookCodeActionKind,
RelatedInformationType: extHostTypes.RelatedInformationType
};
};
Expand Down
7 changes: 1 addition & 6 deletions src/vs/workbench/api/common/extHostLanguageFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { URI, UriComponents } from 'vs/base/common/uri';
import { equals, mixin } from 'vs/base/common/objects';
import type * as vscode from 'vscode';
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits, SemanticTokens, SemanticTokensEdit, Location, InlineCompletionTriggerKind, InternalDataTransferItem, CodeActionTriggerKind } from 'vs/workbench/api/common/extHostTypes';
import { Range, Disposable, CompletionList, SnippetString, CodeActionKind, SymbolInformation, DocumentSymbol, SemanticTokensEdits, SemanticTokens, SemanticTokensEdit, Location, InlineCompletionTriggerKind, InternalDataTransferItem } from 'vs/workbench/api/common/extHostTypes';
import { ISingleEditOperation } from 'vs/editor/common/core/editOperation';
import * as languages from 'vs/editor/common/languages';
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
Expand Down Expand Up @@ -376,7 +376,6 @@ class CodeActionAdapter {

private readonly _cache = new Cache<vscode.CodeAction | vscode.Command>('CodeAction');
private readonly _disposables = new Map<number, DisposableStore>();
private readonly nbKind = new CodeActionKind('notebook');

constructor(
private readonly _documents: ExtHostDocuments,
Expand Down Expand Up @@ -436,10 +435,6 @@ class CodeActionAdapter {
command: this._commands.toInternal(candidate, disposables),
});
} else {
if (codeActionContext.triggerKind !== CodeActionTriggerKind.Invoke && candidate.kind && this.nbKind.contains(candidate.kind)) {
continue;
}

if (codeActionContext.only) {
if (!candidate.kind) {
this._logService.warn(`${this._extension.identifier.value} - Code actions of kind '${codeActionContext.only.value} 'requested but returned code action does not have a 'kind'. Code action will be dropped. Please set 'CodeAction.kind'.`);
Expand Down
10 changes: 0 additions & 10 deletions src/vs/workbench/api/common/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1397,16 +1397,6 @@ export class CodeActionKind {
}
}

export class NotebookCodeActionKind extends CodeActionKind {
public static override Notebook: CodeActionKind;

constructor(
public override readonly value: string
) {
super(value);
}
}

CodeActionKind.Empty = new CodeActionKind('');
CodeActionKind.QuickFix = CodeActionKind.Empty.append('quickfix');
CodeActionKind.Refactor = CodeActionKind.Empty.append('refactor');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ import { CodeActionTriggerType, CodeActionProvider, IWorkspaceTextEdit } from 'v
import { applyCodeAction, ApplyCodeActionReason, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction';
import { isEqual } from 'vs/base/common/resources';

const NotebookCodeAction = new CodeActionKind('notebook');


class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant {
constructor(
@IEditorWorkerService private readonly editorWorkerService: IEditorWorkerService,
Expand Down Expand Up @@ -107,6 +104,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa
}

async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
const nbDisposable = new DisposableStore();
const isTrusted = this.workspaceTrustManagementService.isWorkspaceTrusted();
if (!isTrusted) {
return;
Expand All @@ -116,31 +114,46 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa
return;
}

let saveTrigger = '';
if (context.reason === SaveReason.AUTO) {
// currently this won't happen, as vs/editor/contrib/codeAction/browser/codeAction.ts L#104 filters out codeactions on autosave. Just future-proofing
// ? notebook CodeActions on autosave seems dangerous (perf-wise)
saveTrigger = 'always';
} else if (context.reason === SaveReason.EXPLICIT) {
saveTrigger = 'explicit';
} else {
// SaveReason.FOCUS_CHANGE, WINDOW_CHANGE need to be addressed when autosaves are enabled
return undefined;
}

const setting = this.configurationService.getValue<{ [kind: string]: boolean } | string[]>(NotebookSetting.codeActionsOnSave);
const notebookModel = workingCopy.model.notebookModel;

const setting = this.configurationService.getValue<{ [kind: string]: string }>(NotebookSetting.codeActionsOnSave);
if (!setting) {
return undefined;
}

const notebookModel = workingCopy.model.notebookModel;

const settingItems: string[] = Array.isArray(setting)
? setting
: Object.keys(setting).filter(x => setting[x]);

if (!settingItems.length) {
return undefined;
}

const codeActionsOnSave = this.createCodeActionsOnSave(settingItems).filter(x => !NotebookCodeAction.contains(x));
const notebookCodeActionsOnSave = this.createCodeActionsOnSave(settingItems).filter(x => NotebookCodeAction.contains(x));
const allCodeActions = this.createCodeActionsOnSave(settingItems);
const excludedActions = allCodeActions
.filter(x => setting[x.value] === 'never');
const includedActions = allCodeActions
.filter(x => setting[x.value] === saveTrigger);

const editorCodeActionsOnSave = includedActions.filter(x => !CodeActionKind.Notebook.contains(x));
const notebookCodeActionsOnSave = includedActions.filter(x => CodeActionKind.Notebook.contains(x));
if (!editorCodeActionsOnSave.length && !notebookCodeActionsOnSave.length) {
return undefined;
}

// prioritize `source.fixAll` code actions
if (!Array.isArray(setting)) {
codeActionsOnSave.sort((a, b) => {
editorCodeActionsOnSave.sort((a, b) => {
if (CodeActionKind.SourceFixAll.contains(a)) {
if (CodeActionKind.SourceFixAll.contains(b)) {
return 0;
Expand All @@ -154,21 +167,6 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa
});
}




if (!codeActionsOnSave.length && !notebookCodeActionsOnSave.length) {
return undefined;
}

const excludedActions = Array.isArray(setting)
? []
: Object.keys(setting)
.filter(x => setting[x] === false)
.map(x => new CodeActionKind(x));

const nbDisposable = new DisposableStore();

// run notebook code actions
progress.report({ message: localize('notebookSaveParticipants.notebookCodeActions', "Running 'Notebook' code actions") });
try {
Expand All @@ -188,15 +186,15 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa

// run cell level code actions
const disposable = new DisposableStore();
progress.report({ message: localize('notebookSaveParticipants.cellCodeActions', "Running code actions") });
progress.report({ message: localize('notebookSaveParticipants.cellCodeActions', "Running 'Cell' code actions") });
try {
await Promise.all(notebookModel.cells.map(async cell => {
const ref = await this.textModelService.createModelReference(cell.uri);
disposable.add(ref);

const textEditorModel = ref.object.textEditorModel;

await this.applyOnSaveActions(textEditorModel, codeActionsOnSave, excludedActions, progress, token);
await this.applyOnSaveActions(textEditorModel, editorCodeActionsOnSave, excludedActions, progress, token);
}));
} catch {
this.logService.error('Failed to apply code action on save');
Expand Down Expand Up @@ -248,7 +246,7 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa
for (const action of actionsToRun.validActions) {
const codeActionEdits = action.action.edit?.edits;
let breakFlag = false;
if (!action.action.kind?.includes('notebook')) {
if (!action.action.kind?.startsWith('notebook')) {
for (const edit of codeActionEdits ?? []) {
const workspaceTextEdit = edit as IWorkspaceTextEdit;
if (workspaceTextEdit.resource && isEqual(workspaceTextEdit.resource, model.uri)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -964,12 +964,16 @@ configurationRegistry.registerConfiguration({
default: false
},
[NotebookSetting.codeActionsOnSave]: {
markdownDescription: nls.localize('notebook.codeActionsOnSave', "Experimental. Run a series of CodeActions for a notebook on save. CodeActions must be specified, the file must not be saved after delay, and the editor must not be shutting down. Example: `source.fixAll: true`"),
markdownDescription: nls.localize('notebook.codeActionsOnSave', "Run a series of CodeActions for a notebook on save. CodeActions must be specified, the file must not be saved after delay, and the editor must not be shutting down. Example: `source.fixAll: true`"),
type: 'object',
additionalProperties: {
type: 'boolean'
type: 'string',
enum: ['explicit', 'never'],
// enum: ['explicit', 'always', 'never'], -- autosave support needs to be built first
// nls.localize('always', 'Always triggers Code Actions on save, including autosave, focus, and window change events.'),
enumDescriptions: [nls.localize('never', 'Never triggers Code Actions on save.'), nls.localize('explicit', 'Triggers Code Actions only when explicitly saved.')],
},
default: {}
default: {},
},
[NotebookSetting.formatOnCellExecution]: {
markdownDescription: nls.localize('notebook.formatOnCellExecution', "Format a notebook cell upon execution. A formatter must be available."),
Expand Down
17 changes: 13 additions & 4 deletions src/vscode-dts/vscode.proposed.notebookCodeActions.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@
*--------------------------------------------------------------------------------------------*/

declare module 'vscode' {

// https://github.com/microsoft/vscode/issues/179213

export class NotebookCodeActionKind {
// can only return MULTI CELL workspaceEdits
// ex: notebook.organizeImprots
export class CodeActionKind2 {

/**
* Base kind for all code actions applying to the enitre notebook's scope. CodeActionKinds using
* this should always begin with `notebook.`
*
* This can be appended to the beginning of existing kinds, or have new kinds created for it by
* extensions contributing CodeActionProviders
*
* Example Kinds/Actions:
* - `notebook.source.organizeImports` (might move all imports to a new top cell)
* - `notebook.normalizeVariableNames` (might rename all variables to a standardized casing format)
*/
static readonly Notebook: CodeActionKind;

constructor(value: string);
Expand Down