Skip to content

Commit 2fbd1ae

Browse files
Yoyokrazyrebornix
andauthored
Support for notebook-cell level CodeActions (microsoft#182799)
* issues with isort not scoring on notebook save * add single cell edit check * add setting for `notebook.codeActionsOnSave` * break loop if error, skip codeaction * update message. --------- Co-authored-by: Peng Lyu <[email protected]>
1 parent 0880f76 commit 2fbd1ae

File tree

3 files changed

+144
-20
lines changed

3 files changed

+144
-20
lines changed

src/vs/workbench/contrib/notebook/browser/contrib/saveParticipants/saveParticipants.ts

Lines changed: 134 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,14 @@ import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle
2222
import { IStoredFileWorkingCopy, IStoredFileWorkingCopyModel } from 'vs/workbench/services/workingCopy/common/storedFileWorkingCopy';
2323
import { IStoredFileWorkingCopySaveParticipant, IWorkingCopyFileService } from 'vs/workbench/services/workingCopy/common/workingCopyFileService';
2424
import { NotebookSetting } from 'vs/workbench/contrib/notebook/common/notebookCommon';
25-
import { ICommandService } from 'vs/platform/commands/common/commands';
25+
import { ITextModel } from 'vs/editor/common/model';
2626
import { ILogService } from 'vs/platform/log/common/log';
2727
import { IWorkspaceTrustManagementService } from 'vs/platform/workspace/common/workspaceTrust';
28+
import { CodeActionKind, CodeActionTriggerSource } from 'vs/editor/contrib/codeAction/common/types';
29+
import { CodeActionTriggerType, CodeActionProvider, IWorkspaceTextEdit } from 'vs/editor/common/languages';
30+
import { applyCodeAction, ApplyCodeActionReason, getCodeActions } from 'vs/editor/contrib/codeAction/browser/codeAction';
31+
import { isEqual } from 'vs/base/common/resources';
32+
2833

2934
class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant {
3035
constructor(
@@ -91,13 +96,15 @@ class FormatOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant {
9196
class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipant {
9297
constructor(
9398
@IConfigurationService private readonly configurationService: IConfigurationService,
94-
@ICommandService private readonly commandService: ICommandService,
9599
@ILogService private readonly logService: ILogService,
96100
@IWorkspaceTrustManagementService private readonly workspaceTrustManagementService: IWorkspaceTrustManagementService,
101+
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
102+
@ITextModelService private readonly textModelService: ITextModelService,
103+
@IInstantiationService private readonly instantiationService: IInstantiationService,
97104
) {
98105
}
99106

100-
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>, _token: CancellationToken): Promise<void> {
107+
async participate(workingCopy: IStoredFileWorkingCopy<IStoredFileWorkingCopyModel>, context: { reason: SaveReason }, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
101108
const isTrusted = this.workspaceTrustManagementService.isWorkspaceTrusted();
102109
if (!isTrusted) {
103110
return;
@@ -111,11 +118,13 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa
111118
return undefined;
112119
}
113120

114-
const setting = this.configurationService.getValue<{ [kind: string]: boolean } | string[]>('notebook.experimental.codeActionsOnSave');
121+
const setting = this.configurationService.getValue<{ [kind: string]: boolean } | string[]>(NotebookSetting.codeActionsOnSave);
115122
if (!setting) {
116123
return undefined;
117124
}
118125

126+
const notebookModel = workingCopy.model.notebookModel;
127+
119128
const settingItems: string[] = Array.isArray(setting)
120129
? setting
121130
: Object.keys(setting).filter(x => setting[x]);
@@ -124,21 +133,136 @@ class CodeActionOnSaveParticipant implements IStoredFileWorkingCopySaveParticipa
124133
return undefined;
125134
}
126135

127-
progress.report({ message: 'CodeActionsOnSave running' });
136+
const codeActionsOnSave = this.createCodeActionsOnSave(settingItems);
137+
138+
// TODO: potentially modify to account for new `Notebook` code action kind
139+
// prioritize `source.fixAll` code actions
140+
if (!Array.isArray(setting)) {
141+
codeActionsOnSave.sort((a, b) => {
142+
if (CodeActionKind.SourceFixAll.contains(a)) {
143+
if (CodeActionKind.SourceFixAll.contains(b)) {
144+
return 0;
145+
}
146+
return -1;
147+
}
148+
if (CodeActionKind.SourceFixAll.contains(b)) {
149+
return 1;
150+
}
151+
return 0;
152+
});
153+
}
154+
155+
if (!codeActionsOnSave.length) {
156+
return undefined;
157+
}
158+
159+
const excludedActions = Array.isArray(setting)
160+
? []
161+
: Object.keys(setting)
162+
.filter(x => setting[x] === false)
163+
.map(x => new CodeActionKind(x));
164+
165+
166+
progress.report({ message: localize('notebookSaveParticipants.codeActions', "Running code actions") });
128167
const disposable = new DisposableStore();
129168
try {
130-
for (const cmd of settingItems) {
131-
await this.commandService.executeCommand(cmd);
132-
}
169+
await Promise.all(notebookModel.cells.map(async cell => {
170+
const ref = await this.textModelService.createModelReference(cell.uri);
171+
disposable.add(ref);
172+
173+
const textEditorModel = ref.object.textEditorModel;
174+
175+
await this.applyOnSaveActions(textEditorModel, codeActionsOnSave, excludedActions, progress, token);
176+
}));
133177
} catch {
134-
// Failure to apply a code action should not block other on save actions
135-
this.logService.warn('CodeActionsOnSave failed to apply a code action');
178+
this.logService.error('Failed to apply code action on save');
136179
} finally {
137180
progress.report({ increment: 100 });
138181
disposable.dispose();
139182
}
140183
}
184+
185+
private createCodeActionsOnSave(settingItems: readonly string[]): CodeActionKind[] {
186+
const kinds = settingItems.map(x => new CodeActionKind(x));
187+
188+
// Remove subsets
189+
return kinds.filter(kind => {
190+
return kinds.every(otherKind => otherKind.equals(kind) || !otherKind.contains(kind));
191+
});
192+
}
193+
194+
private async applyOnSaveActions(model: ITextModel, codeActionsOnSave: readonly CodeActionKind[], excludes: readonly CodeActionKind[], progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
195+
196+
const getActionProgress = new class implements IProgress<CodeActionProvider> {
197+
private _names = new Set<string>();
198+
private _report(): void {
199+
progress.report({
200+
message: localize(
201+
{ key: 'codeaction.get2', comment: ['[configure]({1}) is a link. Only translate `configure`. Do not change brackets and parentheses or {1}'] },
202+
"Getting code actions from '{0}' ([configure]({1})).",
203+
[...this._names].map(name => `'${name}'`).join(', '),
204+
'command:workbench.action.openSettings?%5B%22editor.codeActionsOnSave%22%5D'
205+
)
206+
});
207+
}
208+
report(provider: CodeActionProvider) {
209+
if (provider.displayName && !this._names.has(provider.displayName)) {
210+
this._names.add(provider.displayName);
211+
this._report();
212+
}
213+
}
214+
};
215+
216+
for (const codeActionKind of codeActionsOnSave) {
217+
const actionsToRun = await this.getActionsToRun(model, codeActionKind, excludes, getActionProgress, token);
218+
if (token.isCancellationRequested) {
219+
actionsToRun.dispose();
220+
return;
221+
}
222+
223+
try {
224+
for (const action of actionsToRun.validActions) {
225+
const codeActionEdits = action.action.edit?.edits;
226+
let breakFlag = false;
227+
for (const edit of codeActionEdits ?? []) {
228+
const workspaceTextEdit = edit as IWorkspaceTextEdit;
229+
if (workspaceTextEdit.resource && isEqual(workspaceTextEdit.resource, model.uri)) {
230+
continue;
231+
} else {
232+
// error -> applied to multiple resources
233+
breakFlag = true;
234+
break;
235+
}
236+
}
237+
if (breakFlag) {
238+
this.logService.warn('Failed to apply code action on save, applied to multiple resources.');
239+
continue;
240+
}
241+
progress.report({ message: localize('codeAction.apply', "Applying code action '{0}'.", action.action.title) });
242+
await this.instantiationService.invokeFunction(applyCodeAction, action, ApplyCodeActionReason.OnSave, {}, token);
243+
if (token.isCancellationRequested) {
244+
return;
245+
}
246+
}
247+
} catch {
248+
// Failure to apply a code action should not block other on save actions
249+
} finally {
250+
actionsToRun.dispose();
251+
}
252+
}
253+
}
254+
255+
private getActionsToRun(model: ITextModel, codeActionKind: CodeActionKind, excludes: readonly CodeActionKind[], progress: IProgress<CodeActionProvider>, token: CancellationToken) {
256+
return getCodeActions(this.languageFeaturesService.codeActionProvider, model, model.getFullModelRange(), {
257+
type: CodeActionTriggerType.Auto,
258+
triggerAction: CodeActionTriggerSource.OnSave,
259+
filter: { include: codeActionKind, excludes: excludes, includeSourceActions: true },
260+
}, progress, token);
261+
}
141262
}
263+
264+
265+
142266
export class SaveParticipantsContribution extends Disposable implements IWorkbenchContribution {
143267
constructor(
144268
@IInstantiationService private readonly instantiationService: IInstantiationService,

src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -920,15 +920,15 @@ configurationRegistry.registerConfiguration({
920920
tags: ['notebookLayout'],
921921
default: false
922922
},
923-
// [NotebookSetting.codeActionsOnSave]: {
924-
// 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: `notebook.format: true`"),
925-
// type: 'object',
926-
// additionalProperties: {
927-
// type: 'boolean'
928-
// },
929-
// tags: ['notebookLayout'],
930-
// default: {}
931-
// },
923+
[NotebookSetting.codeActionsOnSave]: {
924+
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: `notebook.format: true`"),
925+
type: 'object',
926+
additionalProperties: {
927+
type: 'boolean'
928+
},
929+
tags: ['notebookLayout'],
930+
default: {}
931+
},
932932
[NotebookSetting.confirmDeleteRunningCell]: {
933933
markdownDescription: nls.localize('notebook.confirmDeleteRunningCell', "Control whether a confirmation prompt is required to delete a running cell."),
934934
type: 'boolean',

src/vs/workbench/contrib/notebook/common/notebookCommon.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -938,7 +938,7 @@ export const NotebookSetting = {
938938
outputScrolling: 'notebook.output.scrolling',
939939
textOutputLineLimit: 'notebook.output.textLineLimit',
940940
formatOnSave: 'notebook.formatOnSave.enabled',
941-
codeActionsOnSave: 'notebook.experimental.codeActionsOnSave',
941+
codeActionsOnSave: 'notebook.codeActionsOnSave',
942942
outputWordWrap: 'notebook.output.wordWrap',
943943
outputLineHeightDeprecated: 'notebook.outputLineHeight',
944944
outputLineHeight: 'notebook.output.lineHeight',

0 commit comments

Comments
 (0)