diff --git a/src/vs/editor/common/config/commonEditorConfig.ts b/src/vs/editor/common/config/commonEditorConfig.ts index 62ebc3a17d975..035f47aa2069d 100644 --- a/src/vs/editor/common/config/commonEditorConfig.ts +++ b/src/vs/editor/common/config/commonEditorConfig.ts @@ -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, diff --git a/src/vs/editor/common/config/editorOptions.ts b/src/vs/editor/common/config/editorOptions.ts index e4aa5cf65af21..61037c9f7d07f 100644 --- a/src/vs/editor/common/config/editorOptions.ts +++ b/src/vs/editor/common/config/editorOptions.ts @@ -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 @@ -136,6 +137,13 @@ export interface IEditorLightbulbOptions { enabled?: boolean; } +/** + * Configuration map for codeActionsOnSave + */ +export interface ICodeActionsOnSaveOptions { + [kind: string]: boolean; +} + /** * Configuration options for the editor. */ @@ -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. @@ -850,6 +866,8 @@ export interface EditorContribOptions { readonly find: InternalEditorFindOptions; readonly colorDecorators: boolean; readonly lightbulbEnabled: boolean; + readonly codeActionsOnSave: ICodeActionsOnSaveOptions; + readonly codeActionsOnSaveTimeout: number; } /** @@ -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 ); } @@ -1391,6 +1411,21 @@ function _boolean(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; @@ -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) }; } } @@ -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 } }; } @@ -2305,6 +2344,8 @@ export const EDITOR_DEFAULTS: IValidatedEditorOptions = { globalFindClipboard: false }, colorDecorators: true, - lightbulbEnabled: true + lightbulbEnabled: true, + codeActionsOnSave: {}, + codeActionsOnSaveTimeout: 750 }, }; diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index 3083b047c8d3b..d356d916ed819 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -131,13 +131,22 @@ export class QuickFixController implements IEditorContribution { } private async _onApplyCodeAction(action: CodeAction): TPromise { - 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); } } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 268571ebc7dd4..6c8b58194f0d0 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -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. */ @@ -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. @@ -3118,6 +3133,8 @@ declare namespace monaco.editor { readonly find: InternalEditorFindOptions; readonly colorDecorators: boolean; readonly lightbulbEnabled: boolean; + readonly codeActionsOnSave: ICodeActionsOnSaveOptions; + readonly codeActionsOnSaveTimeout: number; } /** diff --git a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts index c5c267edb8d70..ddeba980ac2b0 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts @@ -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; @@ -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 { + 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('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('editor.codeActionsOnSaveTimeout', settingsOverrides); + + return new Promise((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; @@ -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),