Skip to content

Commit

Permalink
Add editor.codeActionsOnSave (#48086)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
mjbvz authored Apr 20, 2018
1 parent 07d85ac commit c29f432
Show file tree
Hide file tree
Showing 5 changed files with 157 additions and 9 deletions.
19 changes: 19 additions & 0 deletions src/vs/editor/common/config/commonEditorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -652,6 +652,25 @@ const editorConfiguration: IConfigurationNode = {
'default': EDITOR_DEFAULTS.contribInfo.lightbulbEnabled,
'description': nls.localize('codeActions', "Enables the code action lightbulb")
},
'editor.codeActionsOnSave': {
'type': 'object',
'properties': {
'source.organizeImports': {
'type': 'boolean',
'description': nls.localize('codeActionsOnSave.organizeImports', "Run organize imports on save?")
}
},
'additionalProperties': {
'type': 'boolean'
},
'default': EDITOR_DEFAULTS.contribInfo.codeActionsOnSave,
'description': nls.localize('codeActionsOnSave', "Code actions kinds to be run on save.")
},
'editor.codeActionsOnSaveTimeout': {
'type': 'number',
'default': EDITOR_DEFAULTS.contribInfo.codeActionsOnSaveTimeout,
'description': nls.localize('codeActionsOnSaveTimeout', "Timeout for code actions run on save.")
},
'editor.selectionClipboard': {
'type': 'boolean',
'default': EDITOR_DEFAULTS.contribInfo.selectionClipboard,
Expand Down
47 changes: 44 additions & 3 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { FontInfo } from 'vs/editor/common/config/fontInfo';
import { Constants } from 'vs/editor/common/core/uint';
import { USUAL_WORD_SEPARATORS } from 'vs/editor/common/model/wordHelper';
import * as arrays from 'vs/base/common/arrays';
import * as objects from 'vs/base/common/objects';

/**
* Configuration options for editor scrollbars
Expand Down Expand Up @@ -136,6 +137,13 @@ export interface IEditorLightbulbOptions {
enabled?: boolean;
}

/**
* Configuration map for codeActionsOnSave
*/
export interface ICodeActionsOnSaveOptions {
[kind: string]: boolean;
}

/**
* Configuration options for the editor.
*/
Expand Down Expand Up @@ -496,6 +504,14 @@ export interface IEditorOptions {
* Control the behavior and rendering of the code action lightbulb.
*/
lightbulb?: IEditorLightbulbOptions;
/**
* Code action kinds to be run on save.
*/
codeActionsOnSave?: ICodeActionsOnSaveOptions;
/**
* Timeout for running code actions on save.
*/
codeActionsOnSaveTimeout?: number;
/**
* Enable code folding
* Defaults to true.
Expand Down Expand Up @@ -850,6 +866,8 @@ export interface EditorContribOptions {
readonly find: InternalEditorFindOptions;
readonly colorDecorators: boolean;
readonly lightbulbEnabled: boolean;
readonly codeActionsOnSave: ICodeActionsOnSaveOptions;
readonly codeActionsOnSaveTimeout: number;
}

/**
Expand Down Expand Up @@ -1194,6 +1212,8 @@ export class InternalEditorOptions {
&& a.matchBrackets === b.matchBrackets
&& this._equalFindOptions(a.find, b.find)
&& a.colorDecorators === b.colorDecorators
&& objects.equals(a.codeActionsOnSave, b.codeActionsOnSave)
&& a.codeActionsOnSaveTimeout === b.codeActionsOnSaveTimeout
&& a.lightbulbEnabled === b.lightbulbEnabled
);
}
Expand Down Expand Up @@ -1391,6 +1411,21 @@ function _boolean<T>(value: any, defaultValue: T): boolean | T {
return Boolean(value);
}

function _booleanMap(value: { [key: string]: boolean }, defaultValue: { [key: string]: boolean }): { [key: string]: boolean } {
if (!value) {
return defaultValue;
}

const out = Object.create(null);
for (const k of Object.keys(value)) {
const v = value[k];
if (typeof v === 'boolean') {
out[k] = v;
}
}
return out;
}

function _string(value: any, defaultValue: string): string {
if (typeof value !== 'string') {
return defaultValue;
Expand Down Expand Up @@ -1736,7 +1771,9 @@ export class EditorOptionsValidator {
matchBrackets: _boolean(opts.matchBrackets, defaults.matchBrackets),
find: find,
colorDecorators: _boolean(opts.colorDecorators, defaults.colorDecorators),
lightbulbEnabled: _boolean(opts.lightbulb ? opts.lightbulb.enabled : false, defaults.lightbulbEnabled)
lightbulbEnabled: _boolean(opts.lightbulb ? opts.lightbulb.enabled : false, defaults.lightbulbEnabled),
codeActionsOnSave: _booleanMap(opts.codeActionsOnSave, {}),
codeActionsOnSaveTimeout: _clampedInt(opts.codeActionsOnSaveTimeout, defaults.codeActionsOnSaveTimeout, 1, 10000)
};
}
}
Expand Down Expand Up @@ -1839,7 +1876,9 @@ export class InternalEditorOptionsFactory {
matchBrackets: (accessibilityIsOn ? false : opts.contribInfo.matchBrackets), // DISABLED WHEN SCREEN READER IS ATTACHED
find: opts.contribInfo.find,
colorDecorators: opts.contribInfo.colorDecorators,
lightbulbEnabled: opts.contribInfo.lightbulbEnabled
lightbulbEnabled: opts.contribInfo.lightbulbEnabled,
codeActionsOnSave: opts.contribInfo.codeActionsOnSave,
codeActionsOnSaveTimeout: opts.contribInfo.codeActionsOnSaveTimeout
}
};
}
Expand Down Expand Up @@ -2305,6 +2344,8 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = {
globalFindClipboard: false
},
colorDecorators: true,
lightbulbEnabled: true
lightbulbEnabled: true,
codeActionsOnSave: {},
codeActionsOnSaveTimeout: 750
},
};
21 changes: 15 additions & 6 deletions src/vs/editor/contrib/codeAction/codeActionCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,13 +131,22 @@ export class QuickFixController implements IEditorContribution {
}

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

if (action.command) {
await this._commandService.executeCommand(action.command.id, ...action.command.arguments);
}
export async function applyCodeAction(
action: CodeAction,
textModelService: ITextModelService,
fileService: IFileService,
commandService: ICommandService,
editor: ICodeEditor,
) {
if (action.edit) {
await BulkEdit.perform(action.edit.edits, textModelService, fileService, editor);
}
if (action.command) {
await commandService.executeCommand(action.command.id, ...action.command.arguments);
}
}

Expand Down
17 changes: 17 additions & 0 deletions src/vs/monaco.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2471,6 +2471,13 @@ declare namespace monaco.editor {
enabled?: boolean;
}

/**
* Configuration map for codeActionsOnSave
*/
export interface ICodeActionsOnSaveOptions {
[kind: string]: boolean;
}

/**
* Configuration options for the editor.
*/
Expand Down Expand Up @@ -2823,6 +2830,14 @@ declare namespace monaco.editor {
* Control the behavior and rendering of the code action lightbulb.
*/
lightbulb?: IEditorLightbulbOptions;
/**
* Code action kinds to be run on save.
*/
codeActionsOnSave?: ICodeActionsOnSaveOptions;
/**
* Timeout for running code actions on save.
*/
codeActionsOnSaveTimeout?: number;
/**
* Enable code folding
* Defaults to true.
Expand Down Expand Up @@ -3118,6 +3133,8 @@ declare namespace monaco.editor {
readonly find: InternalEditorFindOptions;
readonly colorDecorators: boolean;
readonly lightbulbEnabled: boolean;
readonly codeActionsOnSave: ICodeActionsOnSaveOptions;
readonly codeActionsOnSaveTimeout: number;
}

/**
Expand Down
62 changes: 62 additions & 0 deletions src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ import { isFalsyOrEmpty } from 'vs/base/common/arrays';
import { ILogService } from 'vs/platform/log/common/log';
import { shouldSynchronizeModel } from 'vs/editor/common/services/modelService';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { ITextModelService } from 'vs/editor/common/services/resolverService';
import { ICommandService } from 'vs/platform/commands/common/commands';
import { IFileService } from 'vs/platform/files/common/files';
import { CodeActionKind } from 'vs/editor/contrib/codeAction/codeActionTrigger';
import { CodeAction } from 'vs/editor/common/modes';
import { applyCodeAction } from 'vs/editor/contrib/codeAction/codeActionCommands';
import { getCodeActions } from 'vs/editor/contrib/codeAction/codeAction';
import { ICodeActionsOnSaveOptions } from 'vs/editor/common/config/editorOptions';

export interface ISaveParticipantParticipant extends ISaveParticipant {
// progressMessage: string;
Expand Down Expand Up @@ -259,6 +267,59 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant {
}
}

class CodeActionOnParticipant implements ISaveParticipant {

constructor(
@ITextModelService private readonly _textModelService: ITextModelService,
@IFileService private readonly _fileService: IFileService,
@ICommandService private readonly _commandService: ICommandService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IConfigurationService private readonly _configurationService: IConfigurationService
) { }

async participate(editorModel: ITextFileEditorModel, env: { reason: SaveReason }): Promise<void> {
if (env.reason === SaveReason.AUTO) {
return undefined;
}

const model = editorModel.textEditorModel;
const editor = findEditor(model, this._codeEditorService);
if (!editor) {
return undefined;
}

const settingsOverrides = { overrideIdentifier: model.getLanguageIdentifier().language, resource: editorModel.getResource() };
const setting = this._configurationService.getValue<ICodeActionsOnSaveOptions>('editor.codeActionsOnSave', settingsOverrides);
if (!setting) {
return undefined;
}

const codeActionsOnSave = Object.keys(setting).filter(x => setting[x]).map(x => new CodeActionKind(x));
if (!codeActionsOnSave.length) {
return undefined;
}

const timeout = this._configurationService.getValue<number>('editor.codeActionsOnSaveTimeout', settingsOverrides);

return new Promise<CodeAction[]>((resolve, reject) => {
setTimeout(() => reject(localize('codeActionsOnSave.didTimeout', "Aborted codeActionsOnSave after {0}ms", timeout)), timeout);
this.getActionsToRun(model, codeActionsOnSave).then(resolve);
}).then(actionsToRun => this.applyCodeActions(actionsToRun, editor));
}

private async applyCodeActions(actionsToRun: CodeAction[], editor: ICodeEditor) {
for (const action of actionsToRun) {
await applyCodeAction(action, this._textModelService, this._fileService, this._commandService, editor);
}
}

private async getActionsToRun(model: ITextModel, codeActionsOnSave: CodeActionKind[]) {
const actions = await getCodeActions(model, model.getFullModelRange(), { kind: CodeActionKind.Source, includeSourceActions: true });
const actionsToRun = actions.filter(returnedAction => returnedAction.kind && codeActionsOnSave.some(onSaveKind => onSaveKind.contains(returnedAction.kind)));
return actionsToRun;
}
}

class ExtHostSaveParticipant implements ISaveParticipantParticipant {

private _proxy: ExtHostDocumentSaveParticipantShape;
Expand Down Expand Up @@ -303,6 +364,7 @@ export class SaveParticipant implements ISaveParticipant {
) {
this._saveParticipants = [
instantiationService.createInstance(TrimWhitespaceParticipant),
instantiationService.createInstance(CodeActionOnParticipant),
instantiationService.createInstance(FormatOnSaveParticipant),
instantiationService.createInstance(FinalNewLineParticipant),
instantiationService.createInstance(TrimFinalNewLinesParticipant),
Expand Down

0 comments on commit c29f432

Please sign in to comment.