Skip to content

Commit c29f432

Browse files
authored
Add editor.codeActionsOnSave (#48086)
* Add editor.codeActionsOnSave Fixes #42092 Adds a way to run code actions on save using the `editor.codeActionsOnSave` setting. This setting lists code action kinds to be executed automatically when the document is saved. * Use object instead of array for config option * Adding timeout * Fix description * Fix relative path
1 parent 07d85ac commit c29f432

File tree

5 files changed

+157
-9
lines changed

5 files changed

+157
-9
lines changed

src/vs/editor/common/config/commonEditorConfig.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -652,6 +652,25 @@ const editorConfiguration: IConfigurationNode = {
652652
'default': EDITOR_DEFAULTS.contribInfo.lightbulbEnabled,
653653
'description': nls.localize('codeActions', "Enables the code action lightbulb")
654654
},
655+
'editor.codeActionsOnSave': {
656+
'type': 'object',
657+
'properties': {
658+
'source.organizeImports': {
659+
'type': 'boolean',
660+
'description': nls.localize('codeActionsOnSave.organizeImports', "Run organize imports on save?")
661+
}
662+
},
663+
'additionalProperties': {
664+
'type': 'boolean'
665+
},
666+
'default': EDITOR_DEFAULTS.contribInfo.codeActionsOnSave,
667+
'description': nls.localize('codeActionsOnSave', "Code actions kinds to be run on save.")
668+
},
669+
'editor.codeActionsOnSaveTimeout': {
670+
'type': 'number',
671+
'default': EDITOR_DEFAULTS.contribInfo.codeActionsOnSaveTimeout,
672+
'description': nls.localize('codeActionsOnSaveTimeout', "Timeout for code actions run on save.")
673+
},
655674
'editor.selectionClipboard': {
656675
'type': 'boolean',
657676
'default': EDITOR_DEFAULTS.contribInfo.selectionClipboard,

src/vs/editor/common/config/editorOptions.ts

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { FontInfo } from 'vs/editor/common/config/fontInfo';
1111
import { Constants } from 'vs/editor/common/core/uint';
1212
import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper';
1313
import * as arrays from 'vs/base/common/arrays';
14+
import * as objects from 'vs/base/common/objects';
1415

1516
/**
1617
* Configuration options for editor scrollbars
@@ -136,6 +137,13 @@ export interface IEditorLightbulbOptions {
136137
enabled?: boolean;
137138
}
138139

140+
/**
141+
* Configuration map for codeActionsOnSave
142+
*/
143+
export interface ICodeActionsOnSaveOptions {
144+
[kind: string]: boolean;
145+
}
146+
139147
/**
140148
* Configuration options for the editor.
141149
*/
@@ -496,6 +504,14 @@ export interface IEditorOptions {
496504
* Control the behavior and rendering of the code action lightbulb.
497505
*/
498506
lightbulb?: IEditorLightbulbOptions;
507+
/**
508+
* Code action kinds to be run on save.
509+
*/
510+
codeActionsOnSave?: ICodeActionsOnSaveOptions;
511+
/**
512+
* Timeout for running code actions on save.
513+
*/
514+
codeActionsOnSaveTimeout?: number;
499515
/**
500516
* Enable code folding
501517
* Defaults to true.
@@ -850,6 +866,8 @@ export interface EditorContribOptions {
850866
readonly find: InternalEditorFindOptions;
851867
readonly colorDecorators: boolean;
852868
readonly lightbulbEnabled: boolean;
869+
readonly codeActionsOnSave: ICodeActionsOnSaveOptions;
870+
readonly codeActionsOnSaveTimeout: number;
853871
}
854872

855873
/**
@@ -1194,6 +1212,8 @@ export class InternalEditorOptions {
11941212
&& a.matchBrackets === b.matchBrackets
11951213
&& this._equalFindOptions(a.find, b.find)
11961214
&& a.colorDecorators === b.colorDecorators
1215+
&& objects.equals(a.codeActionsOnSave, b.codeActionsOnSave)
1216+
&& a.codeActionsOnSaveTimeout === b.codeActionsOnSaveTimeout
11971217
&& a.lightbulbEnabled === b.lightbulbEnabled
11981218
);
11991219
}
@@ -1391,6 +1411,21 @@ function _boolean<T>(value: any, defaultValue: T): boolean | T {
13911411
return Boolean(value);
13921412
}
13931413

1414+
function _booleanMap(value: { [key: string]: boolean }, defaultValue: { [key: string]: boolean }): { [key: string]: boolean } {
1415+
if (!value) {
1416+
return defaultValue;
1417+
}
1418+
1419+
const out = Object.create(null);
1420+
for (const k of Object.keys(value)) {
1421+
const v = value[k];
1422+
if (typeof v === 'boolean') {
1423+
out[k] = v;
1424+
}
1425+
}
1426+
return out;
1427+
}
1428+
13941429
function _string(value: any, defaultValue: string): string {
13951430
if (typeof value !== 'string') {
13961431
return defaultValue;
@@ -1736,7 +1771,9 @@ export class EditorOptionsValidator {
17361771
matchBrackets: _boolean(opts.matchBrackets, defaults.matchBrackets),
17371772
find: find,
17381773
colorDecorators: _boolean(opts.colorDecorators, defaults.colorDecorators),
1739-
lightbulbEnabled: _boolean(opts.lightbulb ? opts.lightbulb.enabled : false, defaults.lightbulbEnabled)
1774+
lightbulbEnabled: _boolean(opts.lightbulb ? opts.lightbulb.enabled : false, defaults.lightbulbEnabled),
1775+
codeActionsOnSave: _booleanMap(opts.codeActionsOnSave, {}),
1776+
codeActionsOnSaveTimeout: _clampedInt(opts.codeActionsOnSaveTimeout, defaults.codeActionsOnSaveTimeout, 1, 10000)
17401777
};
17411778
}
17421779
}
@@ -1839,7 +1876,9 @@ export class InternalEditorOptionsFactory {
18391876
matchBrackets: (accessibilityIsOn ? false : opts.contribInfo.matchBrackets), // DISABLED WHEN SCREEN READER IS ATTACHED
18401877
find: opts.contribInfo.find,
18411878
colorDecorators: opts.contribInfo.colorDecorators,
1842-
lightbulbEnabled: opts.contribInfo.lightbulbEnabled
1879+
lightbulbEnabled: opts.contribInfo.lightbulbEnabled,
1880+
codeActionsOnSave: opts.contribInfo.codeActionsOnSave,
1881+
codeActionsOnSaveTimeout: opts.contribInfo.codeActionsOnSaveTimeout
18431882
}
18441883
};
18451884
}
@@ -2305,6 +2344,8 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = {
23052344
globalFindClipboard: false
23062345
},
23072346
colorDecorators: true,
2308-
lightbulbEnabled: true
2347+
lightbulbEnabled: true,
2348+
codeActionsOnSave: {},
2349+
codeActionsOnSaveTimeout: 750
23092350
},
23102351
};

src/vs/editor/contrib/codeAction/codeActionCommands.ts

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -131,13 +131,22 @@ export class QuickFixController implements IEditorContribution {
131131
}
132132

133133
private async _onApplyCodeAction(action: CodeAction): TPromise<void> {
134-
if (action.edit) {
135-
await BulkEdit.perform(action.edit.edits, this._textModelService, this._fileService, this._editor);
136-
}
134+
await applyCodeAction(action, this._textModelService, this._fileService, this._commandService, this._editor);
135+
}
136+
}
137137

138-
if (action.command) {
139-
await this._commandService.executeCommand(action.command.id, ...action.command.arguments);
140-
}
138+
export async function applyCodeAction(
139+
action: CodeAction,
140+
textModelService: ITextModelService,
141+
fileService: IFileService,
142+
commandService: ICommandService,
143+
editor: ICodeEditor,
144+
) {
145+
if (action.edit) {
146+
await BulkEdit.perform(action.edit.edits, textModelService, fileService, editor);
147+
}
148+
if (action.command) {
149+
await commandService.executeCommand(action.command.id, ...action.command.arguments);
141150
}
142151
}
143152

src/vs/monaco.d.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2471,6 +2471,13 @@ declare namespace monaco.editor {
24712471
enabled?: boolean;
24722472
}
24732473

2474+
/**
2475+
* Configuration map for codeActionsOnSave
2476+
*/
2477+
export interface ICodeActionsOnSaveOptions {
2478+
[kind: string]: boolean;
2479+
}
2480+
24742481
/**
24752482
* Configuration options for the editor.
24762483
*/
@@ -2823,6 +2830,14 @@ declare namespace monaco.editor {
28232830
* Control the behavior and rendering of the code action lightbulb.
28242831
*/
28252832
lightbulb?: IEditorLightbulbOptions;
2833+
/**
2834+
* Code action kinds to be run on save.
2835+
*/
2836+
codeActionsOnSave?: ICodeActionsOnSaveOptions;
2837+
/**
2838+
* Timeout for running code actions on save.
2839+
*/
2840+
codeActionsOnSaveTimeout?: number;
28262841
/**
28272842
* Enable code folding
28282843
* Defaults to true.
@@ -3118,6 +3133,8 @@ declare namespace monaco.editor {
31183133
readonly find: InternalEditorFindOptions;
31193134
readonly colorDecorators: boolean;
31203135
readonly lightbulbEnabled: boolean;
3136+
readonly codeActionsOnSave: ICodeActionsOnSaveOptions;
3137+
readonly codeActionsOnSaveTimeout: number;
31213138
}
31223139

31233140
/**

src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ import { isFalsyOrEmpty } from 'vs/base/common/arrays';
3030
import { ILogService } from 'vs/platform/log/common/log';
3131
import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService';
3232
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
33+
import { ITextModelService } from 'vs/editor/common/services/resolverService';
34+
import { ICommandService } from 'vs/platform/commands/common/commands';
35+
import { IFileService } from 'vs/platform/files/common/files';
36+
import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger';
37+
import { CodeAction } from 'vs/editor/common/modes';
38+
import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands';
39+
import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction';
40+
import { ICodeActionsOnSaveOptions } from 'vs/editor/common/config/editorOptions';
3341

3442
export interface ISaveParticipantParticipant extends ISaveParticipant {
3543
// progressMessage: string;
@@ -259,6 +267,59 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant {
259267
}
260268
}
261269

270+
class CodeActionOnParticipant implements ISaveParticipant {
271+
272+
constructor(
273+
@ITextModelService private readonly _textModelService: ITextModelService,
274+
@IFileService private readonly _fileService: IFileService,
275+
@ICommandService private readonly _commandService: ICommandService,
276+
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
277+
@IConfigurationService private readonly _configurationService: IConfigurationService
278+
) { }
279+
280+
async participate(editorModel: ITextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
281+
if (env.reason === SaveReason.AUTO) {
282+
return undefined;
283+
}
284+
285+
const model = editorModel.textEditorModel;
286+
const editor = findEditor(model, this._codeEditorService);
287+
if (!editor) {
288+
return undefined;
289+
}
290+
291+
const settingsOverrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.getResource() };
292+
const setting = this._configurationService.getValue<ICodeActionsOnSaveOptions>('editor.codeActionsOnSave', settingsOverrides);
293+
if (!setting) {
294+
return undefined;
295+
}
296+
297+
const codeActionsOnSave = Object.keys(setting).filter(x => setting[x]).map(x => new CodeActionKind(x));
298+
if (!codeActionsOnSave.length) {
299+
return undefined;
300+
}
301+
302+
const timeout = this._configurationService.getValue<number>('editor.codeActionsOnSaveTimeout', settingsOverrides);
303+
304+
return new Promise<CodeAction[]>((resolve, reject) => {
305+
setTimeout(() => reject(localize('codeActionsOnSave.didTimeout', "Aborted codeActionsOnSave after {0}ms", timeout)), timeout);
306+
this.getActionsToRun(model, codeActionsOnSave).then(resolve);
307+
}).then(actionsToRun => this.applyCodeActions(actionsToRun, editor));
308+
}
309+
310+
private async applyCodeActions(actionsToRun: CodeAction[], editor: ICodeEditor) {
311+
for (const action of actionsToRun) {
312+
await applyCodeAction(action, this._textModelService, this._fileService, this._commandService, editor);
313+
}
314+
}
315+
316+
private async getActionsToRun(model: ITextModel, codeActionsOnSave: CodeActionKind[]) {
317+
const actions = await getCodeActions(model, model.getFullModelRange(), { kind: CodeActionKind.Source, includeSourceActions: true });
318+
const actionsToRun = actions.filter(returnedAction => returnedAction.kind && codeActionsOnSave.some(onSaveKind => onSaveKind.contains(returnedAction.kind)));
319+
return actionsToRun;
320+
}
321+
}
322+
262323
class ExtHostSaveParticipant implements ISaveParticipantParticipant {
263324

264325
private _proxy: ExtHostDocumentSaveParticipantShape;
@@ -303,6 +364,7 @@ export class SaveParticipant implements ISaveParticipant {
303364
) {
304365
this._saveParticipants = [
305366
instantiationService.createInstance(TrimWhitespaceParticipant),
367+
instantiationService.createInstance(CodeActionOnParticipant),
306368
instantiationService.createInstance(FormatOnSaveParticipant),
307369
instantiationService.createInstance(FinalNewLineParticipant),
308370
instantiationService.createInstance(TrimFinalNewLinesParticipant),

0 commit comments

Comments
 (0)