diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index 4cf0bc4cf6a45160556ca83f8d68638edc786008..0c22b17f5efb9a3bb160d3f6939e98f83113505b 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -150,6 +150,10 @@ "name": "vs/workbench/services/actions", "project": "vscode-workbench" }, + { + "name": "vs/workbench/services/bulkEdit", + "project": "vscode-workbench" + }, { "name": "vs/workbench/services/configuration", "project": "vscode-workbench" @@ -219,4 +223,4 @@ "project": "vscode-preferences" } ] -} \ No newline at end of file +} diff --git a/src/vs/editor/browser/services/bulkEditService.ts b/src/vs/editor/browser/services/bulkEditService.ts new file mode 100644 index 0000000000000000000000000000000000000000..8bbde13a40fad6dbb3e24791575aec0ef132948a --- /dev/null +++ b/src/vs/editor/browser/services/bulkEditService.ts @@ -0,0 +1,32 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; +import { WorkspaceEdit } from 'vs/editor/common/modes'; +import { TPromise } from 'vs/base/common/winjs.base'; +import { ICodeEditor } from '../editorBrowser'; +import { Selection } from 'vs/editor/common/core/selection'; +import { IProgressRunner } from 'vs/platform/progress/common/progress'; + +export const IBulkEditService = createDecorator('IWorkspaceEditService'); + + +export interface IBulkEditOptions { + editor?: ICodeEditor; + progress?: IProgressRunner; +} + +export interface IBulkEditResult { + selection: Selection; + ariaSummary: string; +} + +export interface IBulkEditService { + _serviceBrand: any; + + apply(edit: WorkspaceEdit, options: IBulkEditOptions): TPromise; +} + diff --git a/src/vs/editor/contrib/codeAction/codeActionCommands.ts b/src/vs/editor/contrib/codeAction/codeActionCommands.ts index d356d916ed81906482bbca4b9b62c008a275afcb..3a14450abf19eef7de42b9245415612320da7e90 100644 --- a/src/vs/editor/contrib/codeAction/codeActionCommands.ts +++ b/src/vs/editor/contrib/codeAction/codeActionCommands.ts @@ -8,18 +8,14 @@ import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { TPromise } from 'vs/base/common/winjs.base'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import { EditorAction, EditorCommand, ServicesAccessor } from 'vs/editor/browser/editorExtensions'; -import { BulkEdit } from 'vs/editor/browser/services/bulkEdit'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; import { CodeAction } from 'vs/editor/common/modes'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { MessageController } from 'vs/editor/contrib/message/messageController'; import * as nls from 'vs/nls'; import { ICommandService } from 'vs/platform/commands/common/commands'; import { ContextKeyExpr, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IContextMenuService } from 'vs/platform/contextview/browser/contextView'; -import { IFileService } from 'vs/platform/files/common/files'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; import { IMarkerService } from 'vs/platform/markers/common/markers'; import { CodeActionModel, CodeActionsComputeEvent, SUPPORTED_CODE_ACTIONS } from './codeActionModel'; @@ -27,6 +23,7 @@ import { CodeActionAutoApply, CodeActionFilter, CodeActionKind } from './codeAct import { CodeActionContextMenu } from './codeActionWidget'; import { LightBulbWidget } from './lightBulbWidget'; import { escapeRegExpCharacters } from 'vs/base/common/strings'; +import { IBulkEditService } from '../../browser/services/bulkEditService'; function contextKeyForSupportedActions(kind: CodeActionKind) { return ContextKeyExpr.regex( @@ -54,8 +51,7 @@ export class QuickFixController implements IEditorContribution { @ICommandService private readonly _commandService: ICommandService, @IContextMenuService contextMenuService: IContextMenuService, @IKeybindingService private readonly _keybindingService: IKeybindingService, - @ITextModelService private readonly _textModelService: ITextModelService, - @optional(IFileService) private _fileService: IFileService + @IBulkEditService private readonly _bulkEditService: IBulkEditService ) { this._editor = editor; this._model = new CodeActionModel(this._editor, markerService, contextKeyService); @@ -131,19 +127,18 @@ export class QuickFixController implements IEditorContribution { } private async _onApplyCodeAction(action: CodeAction): TPromise { - await applyCodeAction(action, this._textModelService, this._fileService, this._commandService, this._editor); + await applyCodeAction(action, this._bulkEditService, this._commandService, this._editor); } } export async function applyCodeAction( action: CodeAction, - textModelService: ITextModelService, - fileService: IFileService, + bulkEditService: IBulkEditService, commandService: ICommandService, editor: ICodeEditor, ) { if (action.edit) { - await BulkEdit.perform(action.edit.edits, textModelService, fileService, editor); + await bulkEditService.apply(action.edit, { editor }); } if (action.command) { await commandService.executeCommand(action.command.id, ...action.command.arguments); @@ -332,4 +327,4 @@ export class OrganizeImportsAction extends EditorAction { { kind: CodeActionKind.SourceOrganizeImports, includeSourceActions: true }, CodeActionAutoApply.IfSingle); } -} \ No newline at end of file +} diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 803d4ac0b8c9e832b3a3199a022a86b9414fc087..380edcb04c94797b688dfc5abfa10eb3c413e41b 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -9,18 +9,14 @@ import * as nls from 'vs/nls'; import { illegalArgument } from 'vs/base/common/errors'; import { KeyMod, KeyCode } from 'vs/base/common/keyCodes'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IFileService } from 'vs/platform/files/common/files'; import { RawContextKey, IContextKey, IContextKeyService, ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey'; import { IProgressService } from 'vs/platform/progress/common/progress'; import { registerEditorAction, registerEditorContribution, ServicesAccessor, EditorAction, EditorCommand, registerEditorCommand, registerDefaultLanguageCommand } from 'vs/editor/browser/editorExtensions'; import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { BulkEdit } from 'vs/editor/browser/services/bulkEdit'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; import RenameInputField from './renameInputField'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { asWinJsPromise } from 'vs/base/common/async'; import { WorkspaceEdit, RenameProviderRegistry, RenameProvider, RenameLocation } from 'vs/editor/common/modes'; @@ -31,6 +27,7 @@ import { MessageController } from 'vs/editor/contrib/message/messageController'; import { EditorState, CodeEditorStateFlag } from 'vs/editor/browser/core/editorState'; import { KeybindingsRegistry } from 'vs/platform/keybinding/common/keybindingsRegistry'; import { INotificationService } from 'vs/platform/notification/common/notification'; +import { IBulkEditService } from '../../browser/services/bulkEditService'; class RenameSkeleton { @@ -111,11 +108,10 @@ class RenameController implements IEditorContribution { constructor( private editor: ICodeEditor, @INotificationService private readonly _notificationService: INotificationService, - @ITextModelService private readonly _textModelResolverService: ITextModelService, + @IBulkEditService private readonly _bulkEditService: IBulkEditService, @IProgressService private readonly _progressService: IProgressService, @IContextKeyService contextKeyService: IContextKeyService, @IThemeService themeService: IThemeService, - @optional(IFileService) private _fileService: IFileService ) { this._renameInputField = new RenameInputField(editor, themeService); this._renameInputVisible = CONTEXT_RENAME_INPUT_VISIBLE.bindTo(contextKeyService); @@ -168,7 +164,6 @@ class RenameController implements IEditorContribution { this.editor.focus(); - const edit = new BulkEdit(this.editor, null, this._textModelResolverService, this._fileService); const state = new EditorState(this.editor, CodeEditorStateFlag.Position | CodeEditorStateFlag.Value | CodeEditorStateFlag.Selection | CodeEditorStateFlag.Scroll); const renameOperation = skeleton.provideRenameEdits(newNameOrFocusFlag, 0, [], Range.lift(loc.range).getStartPosition()).then(result => { @@ -180,14 +175,15 @@ class RenameController implements IEditorContribution { } return undefined; } - edit.add(result.edits); - return edit.perform().then(selection => { - if (selection) { - this.editor.setSelection(selection); + return this._bulkEditService.apply(result, { editor: this.editor }).then(result => { + if (result.selection) { + this.editor.setSelection(result.selection); } // alert - alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc.text, newNameOrFocusFlag, edit.ariaMessage())); + if (result.ariaSummary) { + alert(nls.localize('aria', "Successfully renamed '{0}' to '{1}'. Summary: {2}", loc.text, newNameOrFocusFlag, result.ariaSummary)); + } }); }, err => { diff --git a/src/vs/editor/standalone/browser/simpleServices.ts b/src/vs/editor/standalone/browser/simpleServices.ts index ed7728c315d1088bbd0575bb7ed48b529c78bf66..f1e4783dda84ab2879c4784978e635a2ce34845c 100644 --- a/src/vs/editor/standalone/browser/simpleServices.ts +++ b/src/vs/editor/standalone/browser/simpleServices.ts @@ -36,12 +36,17 @@ import { ITelemetryService, ITelemetryInfo } from 'vs/platform/telemetry/common/ import { ResolvedKeybinding, Keybinding, createKeybinding, SimpleKeybinding } from 'vs/base/common/keyCodes'; import { ResolvedKeybindingItem } from 'vs/platform/keybinding/common/resolvedKeybindingItem'; import { OS } from 'vs/base/common/platform'; -import { IRange } from 'vs/editor/common/core/range'; +import { IRange, Range } from 'vs/editor/common/core/range'; import { ITextModel } from 'vs/editor/common/model'; import { INotificationService, INotification, INotificationHandle, NoOpNotification, IPromptChoice } from 'vs/platform/notification/common/notification'; import { IConfirmation, IConfirmationResult, IDialogService, IDialogOptions } from 'vs/platform/dialogs/common/dialogs'; import { IPosition, Position as Pos } from 'vs/editor/common/core/position'; import { isEditorConfigurationKey, isDiffEditorConfigurationKey } from 'vs/editor/common/config/commonEditorConfig'; +import { IBulkEditService, IBulkEditOptions, IBulkEditResult } from 'vs/editor/browser/services/bulkEditService'; +import { WorkspaceEdit, isResourceTextEdit, TextEdit } from 'vs/editor/common/modes'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { EditOperation } from 'vs/editor/common/core/editOperation'; +import { localize } from 'vs/nls'; export class SimpleEditor implements IEditor { @@ -643,3 +648,44 @@ export function applyConfigurationValues(configurationService: IConfigurationSer } }); } + +export class SimpleBulkEditService implements IBulkEditService { + _serviceBrand: any; + + constructor(private readonly _modelService: IModelService) { + // + } + + apply(workspaceEdit: WorkspaceEdit, options: IBulkEditOptions): TPromise { + + let edits = new Map(); + + for (let edit of workspaceEdit.edits) { + if (!isResourceTextEdit(edit)) { + return TPromise.wrapError(new Error('bad edit - only text edits are supported')); + } + let model = this._modelService.getModel(edit.resource); + if (!model) { + return TPromise.wrapError(new Error('bad edit - model not found')); + } + let array = edits.get(model); + if (!array) { + array = []; + } + edits.set(model, array.concat(edit.edits)); + } + + let totalEdits = 0; + let totalFiles = 0; + edits.forEach((edits, model) => { + model.applyEdits(edits.map(edit => EditOperation.replaceMove(Range.lift(edit.range), edit.text))); + totalFiles += 1; + totalEdits += edits.length; + }); + + return TPromise.as({ + selection: undefined, + ariaSummary: localize('summary', 'Made {0} edits in {1} files', totalEdits, totalFiles) + }); + } +} diff --git a/src/vs/editor/standalone/browser/standaloneServices.ts b/src/vs/editor/standalone/browser/standaloneServices.ts index b1086e4283396ce39300e7a8f536f00cce3c5bf0..b59efb0e7ec9c70d85a1e26c9b97edbca5e4552b 100644 --- a/src/vs/editor/standalone/browser/standaloneServices.ts +++ b/src/vs/editor/standalone/browser/standaloneServices.ts @@ -33,7 +33,7 @@ import { CodeEditorServiceImpl } from 'vs/editor/browser/services/codeEditorServ import { SimpleConfigurationService, SimpleResourceConfigurationService, SimpleMenuService, SimpleProgressService, StandaloneCommandService, StandaloneKeybindingService, SimpleNotificationService, - StandaloneTelemetryService, SimpleWorkspaceContextService, SimpleDialogService + StandaloneTelemetryService, SimpleWorkspaceContextService, SimpleDialogService, SimpleBulkEditService } from 'vs/editor/standalone/browser/simpleServices'; import { ContextKeyService } from 'vs/platform/contextkey/browser/contextKeyService'; import { IMenuService } from 'vs/platform/actions/common/actions'; @@ -43,6 +43,7 @@ import { ILogService, NullLogService } from 'vs/platform/log/common/log'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { IDialogService } from 'vs/platform/dialogs/common/dialogs'; import { IListService, ListService } from 'vs/platform/list/browser/listService'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; export interface IEditorContextViewService extends IContextViewService { dispose(): void; @@ -192,6 +193,8 @@ export class DynamicStandaloneServices extends Disposable { ensure(IContextMenuService, () => this._register(new ContextMenuService(domElement, telemetryService, notificationService, contextViewService))); ensure(IMenuService, () => new SimpleMenuService(commandService)); + + ensure(IBulkEditService, () => new SimpleBulkEditService(StaticServices.modelService.get(IModelService))); } public get(serviceId: ServiceIdentifier): T { diff --git a/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts b/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts index 5a121fa26d7aecc23ab96e408fab7219c8a4460a..167dac86d2b45500072b56e794a06218662a20d0 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadDocumentsAndEditors.ts @@ -24,6 +24,7 @@ import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/un import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { isCodeEditor, isDiffEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; import URI from 'vs/base/common/uri'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; namespace mapset { @@ -310,14 +311,16 @@ export class MainThreadDocumentsAndEditors { @IFileService fileService: IFileService, @ITextModelService textModelResolverService: ITextModelService, @IUntitledEditorService untitledEditorService: IUntitledEditorService, - @IEditorGroupService editorGroupService: IEditorGroupService + @IEditorGroupService editorGroupService: IEditorGroupService, + @IBulkEditService bulkEditService: IBulkEditService, + ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostDocumentsAndEditors); const mainThreadDocuments = new MainThreadDocuments(this, extHostContext, this._modelService, modeService, this._textFileService, fileService, textModelResolverService, untitledEditorService); extHostContext.set(MainContext.MainThreadDocuments, mainThreadDocuments); - const mainThreadTextEditors = new MainThreadTextEditors(this, extHostContext, codeEditorService, this._workbenchEditorService, editorGroupService, textModelResolverService, fileService, this._modelService); + const mainThreadTextEditors = new MainThreadTextEditors(this, extHostContext, codeEditorService, bulkEditService, this._workbenchEditorService, editorGroupService); extHostContext.set(MainContext.MainThreadTextEditors, mainThreadTextEditors); // It is expected that the ctor of the state computer calls our `_onDelta`. diff --git a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts index a47b82e3c7b13342548bb5b9aad7ae061eebad56..df45221c509c6909cbb0516f834f9127bca3b9a9 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts @@ -4,29 +4,24 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import URI, { UriComponents } from 'vs/base/common/uri'; -import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { disposed } from 'vs/base/common/errors'; +import { IDisposable, dispose } from 'vs/base/common/lifecycle'; +import { equals as objectEquals } from 'vs/base/common/objects'; +import URI, { UriComponents } from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { IDecorationRenderOptions, IDecorationOptions, ILineChange } from 'vs/editor/common/editorCommon'; -import { ISingleEditOperation } from 'vs/editor/common/model'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; +import { IRange } from 'vs/editor/common/core/range'; +import { ISelection } from 'vs/editor/common/core/selection'; +import { IDecorationOptions, IDecorationRenderOptions, ILineChange } from 'vs/editor/common/editorCommon'; +import { ISingleEditOperation } from 'vs/editor/common/model'; +import { ITextEditorOptions, Position as EditorPosition } from 'vs/platform/editor/common/editor'; +import { IApplyEditsOptions, ITextEditorConfigurationUpdate, IUndoStopOptions, TextEditorRevealType, WorkspaceEditDto, reviveWorkspaceEditDto } from 'vs/workbench/api/node/extHost.protocol'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; -import { Position as EditorPosition, ITextEditorOptions } from 'vs/platform/editor/common/editor'; -import { MainThreadTextEditor } from './mainThreadEditor'; -import { ITextEditorConfigurationUpdate, TextEditorRevealType, IApplyEditsOptions, IUndoStopOptions, WorkspaceEditDto, reviveWorkspaceEditDto } from 'vs/workbench/api/node/extHost.protocol'; +import { ExtHostContext, ExtHostEditorsShape, IExtHostContext, ITextDocumentShowOptions, ITextEditorPositionData, MainThreadTextEditorsShape } from '../node/extHost.protocol'; import { MainThreadDocumentsAndEditors } from './mainThreadDocumentsAndEditors'; -import { equals as objectEquals } from 'vs/base/common/objects'; -import { ExtHostContext, MainThreadTextEditorsShape, ExtHostEditorsShape, ITextDocumentShowOptions, ITextEditorPositionData, IExtHostContext } from '../node/extHost.protocol'; -import { IRange } from 'vs/editor/common/core/range'; -import { ISelection } from 'vs/editor/common/core/selection'; -import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import { IFileService } from 'vs/platform/files/common/files'; -import { BulkEdit } from 'vs/editor/browser/services/bulkEdit'; -import { IModelService } from 'vs/editor/common/services/modelService'; -import { isCodeEditor, ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { isResourceFileEdit } from 'vs/editor/common/modes'; +import { MainThreadTextEditor } from './mainThreadEditor'; export class MainThreadTextEditors implements MainThreadTextEditorsShape { @@ -42,11 +37,9 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { documentsAndEditors: MainThreadDocumentsAndEditors, extHostContext: IExtHostContext, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, + @IBulkEditService private readonly _bulkEditService: IBulkEditService, @IWorkbenchEditorService workbenchEditorService: IWorkbenchEditorService, @IEditorGroupService editorGroupService: IEditorGroupService, - @ITextModelService private readonly _textModelResolverService: ITextModelService, - @IFileService private readonly _fileService: IFileService, - @IModelService private readonly _modelService: IModelService, ) { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostEditors); this._documentsAndEditors = documentsAndEditors; @@ -209,31 +202,8 @@ export class MainThreadTextEditors implements MainThreadTextEditorsShape { } $tryApplyWorkspaceEdit(dto: WorkspaceEditDto): TPromise { - const { edits } = reviveWorkspaceEditDto(dto); - - // First check if loaded models were not changed in the meantime - for (let i = 0, len = edits.length; i < len; i++) { - const edit = edits[i]; - if (!isResourceFileEdit(edit) && edit.modelVersionId) { - let model = this._modelService.getModel(edit.resource); - if (model && model.getVersionId() !== edit.modelVersionId) { - // model changed in the meantime - return TPromise.as(false); - } - } - } - - let codeEditor: ICodeEditor; - let editor = this._workbenchEditorService.getActiveEditor(); - if (editor) { - let candidate = editor.getControl(); - if (isCodeEditor(candidate)) { - codeEditor = candidate; - } - } - - return BulkEdit.perform(edits, this._textModelResolverService, this._fileService, codeEditor).then(() => true); + return this._bulkEditService.apply({ edits }, undefined).then(() => true, err => false); } $tryInsertSnippet(id: string, template: string, ranges: IRange[], opts: IUndoStopOptions): TPromise { diff --git a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts index ddeba980ac2b0f9aae2ef04eb5739abf2aa44f56..12f214ffe96a38b9e37af3c9e1355e1131decbe9 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadSaveParticipant.ts @@ -30,14 +30,13 @@ 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'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; export interface ISaveParticipantParticipant extends ISaveParticipant { // progressMessage: string; @@ -270,8 +269,7 @@ class FormatOnSaveParticipant implements ISaveParticipantParticipant { class CodeActionOnParticipant implements ISaveParticipant { constructor( - @ITextModelService private readonly _textModelService: ITextModelService, - @IFileService private readonly _fileService: IFileService, + @IBulkEditService private readonly _bulkEditService: IBulkEditService, @ICommandService private readonly _commandService: ICommandService, @ICodeEditorService private readonly _codeEditorService: ICodeEditorService, @IConfigurationService private readonly _configurationService: IConfigurationService @@ -309,7 +307,7 @@ class CodeActionOnParticipant implements ISaveParticipant { private async applyCodeActions(actionsToRun: CodeAction[], editor: ICodeEditor) { for (const action of actionsToRun) { - await applyCodeAction(action, this._textModelService, this._fileService, this._commandService, editor); + await applyCodeAction(action, this._bulkEditService, this._commandService, editor); } } diff --git a/src/vs/workbench/parts/search/browser/replaceService.ts b/src/vs/workbench/parts/search/browser/replaceService.ts index 1d904f9fff1b6eb5a1c9fd908540672bf8a889ba..56e2cf51b591b3ddcd730250fc29a30ed23504eb 100644 --- a/src/vs/workbench/parts/search/browser/replaceService.ts +++ b/src/vs/workbench/parts/search/browser/replaceService.ts @@ -15,7 +15,6 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi import { IModelService } from 'vs/editor/common/services/modelService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { Match, FileMatch, FileMatchOrMatch, ISearchWorkbenchService } from 'vs/workbench/parts/search/common/searchModel'; -import { BulkEdit } from 'vs/editor/browser/services/bulkEdit'; import { IProgressRunner } from 'vs/platform/progress/common/progress'; import { IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { ITextModelService, ITextModelContentProvider } from 'vs/editor/common/services/resolverService'; @@ -23,10 +22,10 @@ import { IWorkbenchContribution } from 'vs/workbench/common/contributions'; import { ScrollType } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { IFileService } from 'vs/platform/files/common/files'; import { ResourceTextEdit } from 'vs/editor/common/modes'; import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; +import { IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; const REPLACE_PREVIEW = 'replacePreview'; @@ -95,10 +94,10 @@ export class ReplaceService implements IReplaceService { public _serviceBrand: any; constructor( - @IFileService private fileService: IFileService, @ITextFileService private textFileService: ITextFileService, @IEditorService private editorService: IWorkbenchEditorService, - @ITextModelService private textModelResolverService: ITextModelService + @ITextModelService private textModelResolverService: ITextModelService, + @IBulkEditService private bulkEditorService: IBulkEditService ) { } public replace(match: Match): TPromise; @@ -108,8 +107,6 @@ export class ReplaceService implements IReplaceService { const edits: ResourceTextEdit[] = []; - let bulkEdit = new BulkEdit(null, progress, this.textModelResolverService, this.fileService); - if (arg instanceof Match) { let match = arg; edits.push(this.createEdit(match, match.replaceString, resource)); @@ -128,8 +125,8 @@ export class ReplaceService implements IReplaceService { }); } - bulkEdit.add(edits); - return bulkEdit.perform().then(() => this.textFileService.saveAll(edits.map(e => e.resource))); + return this.bulkEditorService.apply({ edits }, { progress }).then(() => this.textFileService.saveAll(edits.map(e => e.resource))); + } public openReplacePreview(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise { diff --git a/src/vs/editor/browser/services/bulkEdit.ts b/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts similarity index 73% rename from src/vs/editor/browser/services/bulkEdit.ts rename to src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts index 6753f7bb6e1727d055c5adbfd519ab6380de58ab..291e49fbb734b24350af90da7a4128e03f4e541c 100644 --- a/src/vs/editor/browser/services/bulkEdit.ts +++ b/src/vs/workbench/services/bulkEdit/electron-browser/bulkEditService.ts @@ -4,40 +4,40 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; -import * as nls from 'vs/nls'; -import { IDisposable, dispose, IReference } from 'vs/base/common/lifecycle'; + +import { getPathLabel } from 'vs/base/common/labels'; +import { IDisposable, IReference, dispose } from 'vs/base/common/lifecycle'; import URI from 'vs/base/common/uri'; import { TPromise } from 'vs/base/common/winjs.base'; -import { ITextModelService, ITextEditorModel } from 'vs/editor/common/services/resolverService'; -import { IFileService, FileChangeType } from 'vs/platform/files/common/files'; +import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IBulkEditOptions, IBulkEditResult, IBulkEditService } from 'vs/editor/browser/services/bulkEditService'; import { EditOperation } from 'vs/editor/common/core/editOperation'; import { Range } from 'vs/editor/common/core/range'; import { Selection } from 'vs/editor/common/core/selection'; -import { IIdentifiedSingleEditOperation, ITextModel, EndOfLineSequence } from 'vs/editor/common/model'; -import { IProgressRunner, emptyProgressRunner, IProgress } from 'vs/platform/progress/common/progress'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { optional } from 'vs/platform/instantiation/common/instantiation'; -import { ResourceTextEdit, ResourceFileEdit, isResourceFileEdit, isResourceTextEdit } from 'vs/editor/common/modes'; -import { getPathLabel } from 'vs/base/common/labels'; +import { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model'; +import { ResourceFileEdit, ResourceTextEdit, WorkspaceEdit, isResourceFileEdit, isResourceTextEdit } from 'vs/editor/common/modes'; +import { IModelService } from 'vs/editor/common/services/modelService'; +import { ITextEditorModel, ITextModelService } from 'vs/editor/common/services/resolverService'; +import { localize } from 'vs/nls'; +import { FileChangeType, IFileService } from 'vs/platform/files/common/files'; +import { registerSingleton } from 'vs/platform/instantiation/common/extensions'; +import { IProgress, IProgressRunner, emptyProgressRunner } from 'vs/platform/progress/common/progress'; +import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; +abstract class Recording { -abstract class IRecording { + static start(fileService: IFileService): Recording { - static start(fileService: IFileService): IRecording { - - const _changes = new Set(); + let _changes = new Set(); let stop: IDisposable; - if (fileService) { - // watch only when there is a fileservice available - stop = fileService.onFileChanges(event => { - for (const change of event.changes) { - if (change.type === FileChangeType.UPDATED) { - _changes.add(change.resource.toString()); - } + stop = fileService.onFileChanges(event => { + for (const change of event.changes) { + if (change.type === FileChangeType.UPDATED) { + _changes.add(change.resource.toString()); } - }); - } + } + }); return { stop() { return dispose(stop); }, @@ -258,12 +258,6 @@ export type Edit = ResourceFileEdit | ResourceTextEdit; export class BulkEdit { - static perform(edits: Edit[], textModelService: ITextModelService, fileService: IFileService, editor: ICodeEditor): TPromise { - const edit = new BulkEdit(editor, null, textModelService, fileService); - edit.add(edits); - return edit.perform(); - } - private _edits: Edit[] = []; private _editor: ICodeEditor; private _progress: IProgressRunner; @@ -272,7 +266,7 @@ export class BulkEdit { editor: ICodeEditor, progress: IProgressRunner, @ITextModelService private readonly _textModelService: ITextModelService, - @optional(IFileService) private _fileService: IFileService + @IFileService private readonly _fileService: IFileService ) { this._editor = editor; this._progress = progress || emptyProgressRunner; @@ -290,11 +284,11 @@ export class BulkEdit { const editCount = this._edits.reduce((prev, cur) => isResourceFileEdit(cur) ? prev : prev + cur.edits.length, 0); const resourceCount = this._edits.length; if (editCount === 0) { - return nls.localize('summary.0', "Made no edits"); + return localize('summary.0', "Made no edits"); } else if (editCount > 1 && resourceCount > 1) { - return nls.localize('summary.nm', "Made {0} text edits in {1} files", editCount, resourceCount); + return localize('summary.nm', "Made {0} text edits in {1} files", editCount, resourceCount); } else { - return nls.localize('summary.n0', "Made {0} text edits in one file", editCount, resourceCount); + return localize('summary.n0', "Made {0} text edits in one file", editCount, resourceCount); } } @@ -358,7 +352,7 @@ export class BulkEdit { private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress): TPromise { - const recording = IRecording.start(this._fileService); + const recording = Recording.start(this._fileService); const model = new BulkEditModel(this._textModelService, this._editor, edits, progress); await model.prepare(); @@ -371,7 +365,7 @@ export class BulkEdit { if (conflicts.length > 0) { model.dispose(); - throw new Error(nls.localize('conflict', "These files have changed in the meantime: {0}", conflicts.join(', '))); + throw new Error(localize('conflict', "These files have changed in the meantime: {0}", conflicts.join(', '))); } const selection = await model.apply(); @@ -379,3 +373,59 @@ export class BulkEdit { return selection; } } + +export class BulkEditService implements IBulkEditService { + + _serviceBrand: any; + + constructor( + @IModelService private readonly _modelService: IModelService, + @IWorkbenchEditorService private readonly _workbenchEditorService: IWorkbenchEditorService, + @ITextModelService private readonly _textModelService: ITextModelService, + @IFileService private readonly _fileService: IFileService + ) { + + } + + apply(edit: WorkspaceEdit, options: IBulkEditOptions = {}): TPromise { + + let { edits } = edit; + let codeEditor = options.editor; + + // First check if loaded models were not changed in the meantime + for (let i = 0, len = edits.length; i < len; i++) { + const edit = edits[i]; + if (!isResourceFileEdit(edit) && typeof edit.modelVersionId === 'number') { + let model = this._modelService.getModel(edit.resource); + if (model && model.getVersionId() !== edit.modelVersionId) { + // model changed in the meantime + return TPromise.wrapError(new Error(`${model.uri.toString()} has changed in the meantime`)); + } + } + } + + // try to find code editor + // todo@joh, prefer edit that gets edited + if (!codeEditor) { + let editor = this._workbenchEditorService.getActiveEditor(); + if (editor) { + let candidate = editor.getControl(); + if (isCodeEditor(candidate)) { + codeEditor = candidate; + } + } + } + + const bulkEdit = new BulkEdit(options.editor, options.progress, this._textModelService, this._fileService); + bulkEdit.add(edits); + return bulkEdit.perform().then(selection => { + return { + selection, + ariaSummary: bulkEdit.ariaMessage() + }; + }); + } +} + + +registerSingleton(IBulkEditService, BulkEditService); diff --git a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts index 60ef0f54790fe7229e01cd387806d46c5528f210..2a5fbc98574c2d751010898558ec46d6eb922aab 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts @@ -27,6 +27,7 @@ import { TestFileService } from 'vs/workbench/test/workbenchTestServices'; import { TPromise } from 'vs/base/common/winjs.base'; import { IFileStat } from 'vs/platform/files/common/files'; import { ResourceTextEdit } from 'vs/editor/common/modes'; +import { BulkEditService } from '../../../services/bulkEdit/electron-browser/bulkEditService'; suite('MainThreadEditors', () => { @@ -80,6 +81,8 @@ suite('MainThreadEditors', () => { onEditorGroupMoved = Event.None; }; + const bulkEditService = new BulkEditService(modelService, workbenchEditorService, null, fileService); + const rpcProtocol = new TestRPCProtocol(); rpcProtocol.set(ExtHostContext.ExtHostDocuments, new class extends mock() { $acceptModelChanged(): void { @@ -101,17 +104,16 @@ suite('MainThreadEditors', () => { null, null, editorGroupService, + bulkEditService, ); editors = new MainThreadTextEditors( documentAndEditor, SingleProxyRPCProtocol(null), codeEditorService, + bulkEditService, workbenchEditorService, editorGroupService, - null, - fileService, - modelService ); }); diff --git a/src/vs/workbench/workbench.main.ts b/src/vs/workbench/workbench.main.ts index e5cbc7ad2bf0f06d71fb20f7a7852ad2574d0ec3..78ae4f9f1eadef8190b5c183989a9e4168bf0d9d 100644 --- a/src/vs/workbench/workbench.main.ts +++ b/src/vs/workbench/workbench.main.ts @@ -131,3 +131,6 @@ import 'vs/workbench/parts/themes/test/electron-browser/themes.test.contribution import 'vs/workbench/parts/watermark/electron-browser/watermark'; import 'vs/workbench/parts/welcome/overlay/browser/welcomeOverlay'; + +// services +import 'vs/workbench/services/bulkEdit/electron-browser/bulkEditService';