From dc31fa213be87c0c937324ec608e42157dbb4418 Mon Sep 17 00:00:00 2001 From: Johannes Rieken Date: Fri, 19 Jan 2018 17:54:54 +0100 Subject: [PATCH] support interleaving file and text changes, basically rewrite bulk edit... --- src/vs/editor/browser/services/bulkEdit.ts | 393 ++++++++---------- src/vs/editor/common/modes.ts | 38 +- .../contrib/quickFix/quickFixCommands.ts | 9 +- .../contrib/quickFix/test/quickFix.test.ts | 4 +- src/vs/editor/contrib/rename/rename.ts | 8 +- src/vs/monaco.d.ts | 24 +- src/vs/platform/progress/common/progress.ts | 6 + src/vs/vscode.d.ts | 56 ++- .../api/electron-browser/mainThreadEditors.ts | 53 +-- .../mainThreadLanguageFeatures.ts | 18 +- src/vs/workbench/api/node/extHost.protocol.ts | 53 +-- .../workbench/api/node/extHostApiCommands.ts | 6 +- .../node/extHostDocumentSaveParticipant.ts | 14 +- .../api/node/extHostLanguageFeatures.ts | 8 +- .../workbench/api/node/extHostTextEditors.ts | 43 +- .../api/node/extHostTypeConverters.ts | 36 +- src/vs/workbench/api/node/extHostTypes.ts | 102 ++--- .../parts/search/browser/replaceService.ts | 18 +- .../extHostDocumentSaveParticipant.test.ts | 30 +- .../api/extHostLanguageFeatures.test.ts | 5 +- .../api/extHostTextEditors.test.ts | 15 +- .../electron-browser/api/extHostTypes.test.ts | 50 ++- .../api/mainThreadEditors.test.ts | 58 ++- 23 files changed, 481 insertions(+), 566 deletions(-) diff --git a/src/vs/editor/browser/services/bulkEdit.ts b/src/vs/editor/browser/services/bulkEdit.ts index d5052d8c706..2be87d824bf 100644 --- a/src/vs/editor/browser/services/bulkEdit.ts +++ b/src/vs/editor/browser/services/bulkEdit.ts @@ -5,75 +5,46 @@ 'use strict'; import * as nls from 'vs/nls'; -import { flatten } from 'vs/base/common/arrays'; -import { IStringDictionary, forEach, values, groupBy, size } from 'vs/base/common/collections'; import { IDisposable, dispose, IReference } 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, IFileChange } from 'vs/platform/files/common/files'; +import { IFileService } from 'vs/platform/files/common/files'; import { EditOperation } from 'vs/editor/common/core/editOperation'; -import { Range, IRange } from 'vs/editor/common/core/range'; -import { Selection, ISelection } from 'vs/editor/common/core/selection'; +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 } from 'vs/platform/progress/common/progress'; +import { IProgressRunner, emptyProgressRunner, IProgress } from 'vs/platform/progress/common/progress'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IResourceRename, IResourceCreate } from 'vs/editor/common/modes'; +import { optional } from 'vs/platform/instantiation/common/instantiation'; +import { ResourceTextEdit, ResourceFileEdit, isResourceFileEdit } from 'vs/editor/common/modes'; +import { getPathLabel } from 'vs/base/common/labels'; -export interface IResourceFileEdit { - readonly renamedResources: { from: URI, to }[]; - readonly createdResources: { uri: URI, contents: string }[]; - readonly deletedResources: URI[]; -} - -export interface IResourceEdit { - resource: URI; - range?: IRange; - newText: string; - newEol?: EndOfLineSequence; -} -interface IRecording { - stop(): void; - hasChanged(resource: URI): boolean; - allChanges(): IFileChange[]; -} - -class ChangeRecorder { +abstract class IRecording { - private _fileService: IFileService; - - constructor(fileService?: IFileService) { - this._fileService = fileService; - } - - public start(): IRecording { - - const changes: IStringDictionary = Object.create(null); + static start(fileService: IFileService): IRecording { + const _changes = new Set(); let stop: IDisposable; - if (this._fileService) { - stop = this._fileService.onFileChanges((event) => { - event.changes.forEach(change => { - - const key = String(change.resource); - let array = changes[key]; - - if (!array) { - changes[key] = array = []; - } - array.push(change); - }); + if (fileService) { + // watch only when there is a fileservice available + stop = fileService.onFileChanges(event => { + for (const change of event.changes) { + _changes.add(change.resource.toString()); + } }); } return { - stop: () => { return stop && stop.dispose(); }, - hasChanged: (resource: URI) => !!changes[resource.toString()], - allChanges: () => flatten(values(changes)) + stop() { return dispose(stop); }, + hasChanged(resource) { return _changes.has(resource.toString()); } }; } + + abstract stop(): void; + abstract hasChanged(resource: URI): boolean; } class EditTask implements IDisposable { @@ -91,26 +62,34 @@ class EditTask implements IDisposable { this._edits = []; } - public addEdit(edit: IResourceEdit): void { - - if (typeof edit.newEol === 'number') { - // honor eol-change - this._newEol = edit.newEol; + dispose() { + if (this._model) { + this._modelReference.dispose(); + this._modelReference = null; } + } - if (edit.range || edit.newText) { - // create edit operation - let range: Range; - if (!edit.range) { - range = this._model.getFullModelRange(); - } else { - range = Range.lift(edit.range); + addEdit(resourceEdit: ResourceTextEdit): void { + + for (const edit of resourceEdit.edits) { + if (typeof edit.eol === 'number') { + // honor eol-change + this._newEol = edit.eol; + } + if (edit.range || edit.text) { + // create edit operation + let range: Range; + if (!edit.range) { + range = this._model.getFullModelRange(); + } else { + range = Range.lift(edit.range); + } + this._edits.push(EditOperation.replaceMove(range, edit.text)); } - this._edits.push(EditOperation.replaceMove(range, edit.newText)); } } - public apply(): void { + apply(): void { if (this._edits.length > 0) { this._edits = this._edits.map((value, index) => ({ value, index })).sort((a, b) => { @@ -167,16 +146,10 @@ class EditTask implements IDisposable { return [this._endCursorSelection]; } - public getEndCursorSelection(): Selection { + getEndCursorSelection(): Selection { return this._endCursorSelection; } - dispose() { - if (this._model) { - this._modelReference.dispose(); - this._modelReference = null; - } - } } class SourceModelEditTask extends EditTask { @@ -196,46 +169,42 @@ class SourceModelEditTask extends EditTask { class BulkEditModel implements IDisposable { private _textModelResolverService: ITextModelService; - private _numberOfResourcesToModify: number = 0; - private _edits: IStringDictionary = Object.create(null); + private _edits = new Map(); private _tasks: EditTask[]; private _sourceModel: URI; private _sourceSelections: Selection[]; private _sourceModelTask: SourceModelEditTask; + private _progress: IProgress; constructor( textModelResolverService: ITextModelService, - sourceModel: URI, - sourceSelections: Selection[], - edits: IResourceEdit[], - private progress: IProgressRunner, - private renames: IResourceRename[], - private creates: IResourceCreate[], - private deletes: URI[], - private fileService: IFileService + editor: ICodeEditor, + edits: ResourceTextEdit[], + progress: IProgress ) { this._textModelResolverService = textModelResolverService; - this._sourceModel = sourceModel; - this._sourceSelections = sourceSelections; - this._sourceModelTask = null; + this._sourceModel = editor ? editor.getModel().uri : undefined; + this._sourceSelections = editor ? editor.getSelections() : undefined; + this._sourceModelTask = undefined; + this._progress = progress; - this._numberOfResourcesToModify += this.renames.length + this.deletes.length + this.creates.length; + edits.forEach(this.addEdit, this); + } - for (let edit of edits) { - this._addEdit(edit); - } + dispose(): void { + this._tasks = dispose(this._tasks); } - private _addEdit(edit: IResourceEdit): void { - let array = this._edits[edit.resource.toString()]; + addEdit(edit: ResourceTextEdit): void { + let array = this._edits.get(edit.resource.toString()); if (!array) { - this._edits[edit.resource.toString()] = array = []; - this._numberOfResourcesToModify += 1; + array = []; + this._edits.set(edit.resource.toString(), array); } array.push(edit); } - public async prepare(): TPromise { + async prepare(): TPromise { if (this._tasks) { throw new Error('illegal state - already prepared'); @@ -244,42 +213,25 @@ class BulkEditModel implements IDisposable { this._tasks = []; const promises: TPromise[] = []; - if (this.progress) { - this.progress.total(this._numberOfResourcesToModify * 2); - } - - await TPromise.join(this.renames.map(rename => - this.fileService.moveFile(rename.from, rename.to))); - - await TPromise.join(this.creates.map(create => - this.fileService.createFile(create.uri, create.contents))); - - await TPromise.join(this.deletes.map(uri => - this.fileService.del(uri))); - - forEach(this._edits, entry => { - const promise = this._textModelResolverService.createModelReference(URI.parse(entry.key)).then(ref => { + this._edits.forEach((value, key) => { + const promise = this._textModelResolverService.createModelReference(URI.parse(key)).then(ref => { const model = ref.object; if (!model || !model.textEditorModel) { - throw new Error(`Cannot load file ${entry.key}`); + throw new Error(`Cannot load file ${key}`); } - const textEditorModel = model.textEditorModel; let task: EditTask; - - if (this._sourceModel && textEditorModel.uri.toString() === this._sourceModel.toString()) { + if (this._sourceModel && model.textEditorModel.uri.toString() === this._sourceModel.toString()) { this._sourceModelTask = new SourceModelEditTask(ref, this._sourceSelections); task = this._sourceModelTask; } else { task = new EditTask(ref); } - entry.value.forEach(edit => task.addEdit(edit)); + value.forEach(edit => task.addEdit(edit)); this._tasks.push(task); - if (this.progress) { - this.progress.worked(1); - } + this._progress.report(undefined); }); promises.push(promise); }); @@ -289,147 +241,136 @@ class BulkEditModel implements IDisposable { return this; } - public apply(): Selection { - this._tasks.forEach(task => this.applyTask(task)); - let r: Selection = null; - if (this._sourceModelTask) { - r = this._sourceModelTask.getEndCursorSelection(); - } - return r; - } - - private applyTask(task: EditTask): void { - task.apply(); - if (this.progress) { - this.progress.worked(1); + apply(): Selection { + for (const task of this._tasks) { + task.apply(); + this._progress.report(undefined); } - } - - dispose(): void { - this._tasks = dispose(this._tasks); + return this._sourceModelTask + ? this._sourceModelTask.getEndCursorSelection() + : undefined; } } -export interface BulkEdit { - progress(progress: IProgressRunner): void; - add(edit: IResourceEdit[]): void; - addRename(edit: IResourceRename[]): void; - addCreate(edit: IResourceCreate[]): void; - addDelete(edit: URI[]): void; - finish(): TPromise; - ariaMessage(): string; -} +export type Edit = ResourceFileEdit | ResourceTextEdit; -export function bulkEdit(textModelResolverService: ITextModelService, editor: ICodeEditor, edits: IResourceEdit[], fileService: IFileService, resourceFileEdits?: IResourceFileEdit): TPromise { - let bulk = createBulkEdit(textModelResolverService, editor, fileService); - bulk.add(edits); - bulk.addRename(resourceFileEdits.renamedResources); - bulk.addCreate(resourceFileEdits.createdResources); - bulk.addDelete(resourceFileEdits.deletedResources); - bulk.progress(null); - return bulk.finish(); -} +export class BulkEdit { -export function createBulkEdit(textModelResolverService: ITextModelService, editor?: ICodeEditor, fileService?: IFileService): 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(); + } - let all: IResourceEdit[] = []; - const renames: IResourceRename[] = []; - const creates: IResourceCreate[] = []; - const deletes: URI[] = []; - let recording = new ChangeRecorder(fileService).start(); - let progressRunner: IProgressRunner; + private _edits: Edit[] = []; + private _editor: ICodeEditor; + private _progress: IProgressRunner; - function progress(progress: IProgressRunner) { - progressRunner = progress; + constructor( + editor: ICodeEditor, + progress: IProgressRunner, + @ITextModelService private _textModelService: ITextModelService, + @optional(IFileService) private _fileService: IFileService + ) { + this._editor = editor; + this._progress = progress || emptyProgressRunner; } - function add(edits: IResourceEdit[]): void { - all.push(...edits); + add(edits: Edit[] | Edit): void { + if (Array.isArray(edits)) { + this._edits.push(...edits); + } else { + this._edits.push(edits); + } } - function addRename(edits: IResourceRename[]): void { - renames.push(...edits); + ariaMessage(): string { + 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"); + } else if (editCount > 1 && resourceCount > 1) { + return nls.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); + } } - function addCreate(edits: IResourceCreate[]): void { - creates.push(...edits); - } + async perform(): TPromise { - function addDelete(edits: URI[]): void { - deletes.push(...edits); - } + let seen = new Set(); + let total = 0; - function getConcurrentEdits() { - let names: string[]; - for (let edit of all) { - if (recording.hasChanged(edit.resource)) { - if (!names) { - names = []; - } - names.push(edit.resource.fsPath); + const groups: Edit[][] = []; + let group: Edit[]; + for (const edit of this._edits) { + if (!group || isResourceFileEdit(group[0]) === isResourceFileEdit(edit)) { + group = []; + groups.push(group); + } + group.push(edit); + + if (isResourceFileEdit(edit)) { + total += 1; + } else if (!seen.has(edit.resource.toString())) { + seen.add(edit.resource.toString()); + total += 2; } } - if (names) { - return nls.localize('conflict', "These files have changed in the meantime: {0}", names.join(', ')); + + // define total work and progress callback + // for child operations + this._progress.total(total); + let progress: IProgress = { report: _ => this._progress.worked(1) }; + + // do it. return the last selection computed + // by a text change (can be undefined then) + let res: Selection = undefined; + for (const group of groups) { + if (isResourceFileEdit(group[0])) { + await this._performFileEdits(group, progress); + } else { + res = await this._performTextEdits(group, progress) || res; + } } - return undefined; + return res; } - function finish(): TPromise { + private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress) { + for (const edit of edits) { - if (all.length === 0 && renames.length === 0 && creates.length === 0 && deletes.length === 0) { - return TPromise.as(undefined); - } + progress.report(undefined); - let concurrentEdits = getConcurrentEdits(); - if (concurrentEdits) { - return TPromise.wrapError(new Error(concurrentEdits)); + if (edit.newUri && edit.oldUri) { + await this._fileService.moveFile(edit.oldUri, edit.newUri, false); + } else if (!edit.newUri && edit.oldUri) { + await this._fileService.del(edit.oldUri, true); + } else if (edit.newUri && !edit.oldUri) { + await this._fileService.createFile(edit.newUri, undefined, { overwrite: false }); + } } + } - let uri: URI; - let selections: Selection[]; + private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress): TPromise { - if (editor && editor.getModel()) { - uri = editor.getModel().uri; - selections = editor.getSelections(); - } + const recording = IRecording.start(this._fileService); + const model = new BulkEditModel(this._textModelService, this._editor, edits, progress); - const model = new BulkEditModel(textModelResolverService, uri, selections, all, progressRunner, renames, creates, deletes, fileService); + await model.prepare(); - return model.prepare().then(async _ => { + const conflicts = edits + .filter(edit => recording.hasChanged(edit.resource)) + .map(edit => getPathLabel(edit.resource)); - let concurrentEdits = getConcurrentEdits(); - if (concurrentEdits) { - throw new Error(concurrentEdits); - } + recording.stop(); - recording.stop(); - - const result = await model.apply(); + if (conflicts.length > 0) { model.dispose(); - return result; - }); - } - - function ariaMessage(): string { - let editCount = all.length; - let resourceCount = size(groupBy(all, edit => edit.resource.toString())); - if (editCount === 0) { - return nls.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); - } else { - return nls.localize('summary.n0', "Made {0} text edits in one file", editCount, resourceCount); + throw new Error(nls.localize('conflict', "These files have changed in the meantime: {0}", conflicts.join(', '))); } - } - return { - progress, - add, - addRename, - addCreate, - addDelete, - finish, - ariaMessage - }; + const selection = await model.apply(); + model.dispose(); + return selection; + } } diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 9aa2e6ae1ba..e822789ab01 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -17,6 +17,7 @@ import { TokenizationRegistryImpl } from 'vs/editor/common/modes/tokenizationReg import { Color } from 'vs/base/common/color'; import { IMarkerData } from 'vs/platform/markers/common/markers'; import * as model from 'vs/editor/common/model'; +import { isObject } from 'vs/base/common/types'; /** * Open ended enum at runtime @@ -816,29 +817,36 @@ export interface DocumentColorProvider { provideColorPresentations(model: model.ITextModel, colorInfo: IColorInformation, token: CancellationToken): IColorPresentation[] | Thenable; } -export interface IResourceEdit { - resource: URI; - range: IRange; - newText: string; +/** + * @internal + */ +export function isResourceFileEdit(thing: any): thing is ResourceFileEdit { + return isObject(thing) && (Boolean((thing).newUri) || Boolean((thing).oldUri)); +} + +/** + * @internal + */ +export function isResourceTextEdit(thing: any): thing is ResourceTextEdit { + return isObject(thing) && (thing).resource && Array.isArray((thing).edits); } -export interface IResourceRename { - readonly from: URI; - readonly to: URI; +export interface ResourceFileEdit { + oldUri: URI; + newUri: URI; } -export interface IResourceCreate { - readonly uri: URI; - readonly contents: string; +export interface ResourceTextEdit { + resource: URI; + modelVersionId?: number; + edits: TextEdit[]; } export interface WorkspaceEdit { - edits: IResourceEdit[]; - renamedResources?: IResourceRename[]; - createdResources?: IResourceCreate[]; - deletedResources?: URI[]; - rejectReason?: string; + edits: Array; + rejectReason?: string; // TODO@joh, move to rename } + export interface RenameProvider { provideRenameEdits(model: model.ITextModel, position: Position, newName: string, token: CancellationToken): WorkspaceEdit | Thenable; } diff --git a/src/vs/editor/contrib/quickFix/quickFixCommands.ts b/src/vs/editor/contrib/quickFix/quickFixCommands.ts index f012b11a4ca..1345f72935b 100644 --- a/src/vs/editor/contrib/quickFix/quickFixCommands.ts +++ b/src/vs/editor/contrib/quickFix/quickFixCommands.ts @@ -22,10 +22,9 @@ import { LightBulbWidget } from './lightBulbWidget'; import { QuickFixModel, QuickFixComputeEvent } from './quickFixModel'; import { TPromise } from 'vs/base/common/winjs.base'; import { CodeAction } from 'vs/editor/common/modes'; -import { bulkEdit } from 'vs/editor/browser/services/bulkEdit'; +import { BulkEdit } from 'vs/editor/browser/services/bulkEdit'; import { IFileService } from 'vs/platform/files/common/files'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; -import URI from 'vs/base/common/uri'; export class QuickFixController implements IEditorContribution { @@ -113,11 +112,7 @@ export class QuickFixController implements IEditorContribution { private async _onApplyCodeAction(action: CodeAction): TPromise { if (action.edit) { - await bulkEdit(this._textModelService, this._editor, action.edit.edits, this._fileService, { - createdResources: action.edit.createdResources.map(create => ({ uri: URI.revive(create.uri), contents: create.contents })), - renamedResources: action.edit.renamedResources.map(rename => ({ from: URI.revive(rename.from), to: URI.revive(rename.to) })), - deletedResources: action.edit.deletedResources.map(URI.revive) - }); + await BulkEdit.perform(action.edit.edits, this._textModelService, this._fileService, this._editor); } if (action.command) { diff --git a/src/vs/editor/contrib/quickFix/test/quickFix.test.ts b/src/vs/editor/contrib/quickFix/test/quickFix.test.ts index 8e1fdc79473..0a598114488 100644 --- a/src/vs/editor/contrib/quickFix/test/quickFix.test.ts +++ b/src/vs/editor/contrib/quickFix/test/quickFix.test.ts @@ -8,7 +8,7 @@ import * as assert from 'assert'; import URI from 'vs/base/common/uri'; import Severity from 'vs/base/common/severity'; import { TextModel } from 'vs/editor/common/model/textModel'; -import { CodeActionProviderRegistry, LanguageIdentifier, CodeActionProvider, Command, WorkspaceEdit, IResourceEdit } from 'vs/editor/common/modes'; +import { CodeActionProviderRegistry, LanguageIdentifier, CodeActionProvider, Command, WorkspaceEdit, ResourceTextEdit } from 'vs/editor/common/modes'; import { IDisposable, dispose } from 'vs/base/common/lifecycle'; import { Range } from 'vs/editor/common/core/range'; import { getCodeActions } from 'vs/editor/contrib/quickFix/quickFix'; @@ -57,7 +57,7 @@ suite('QuickFix', () => { bcd: { diagnostics: [], edit: new class implements WorkspaceEdit { - edits: IResourceEdit[]; + edits: ResourceTextEdit[]; }, title: 'abc' } diff --git a/src/vs/editor/contrib/rename/rename.ts b/src/vs/editor/contrib/rename/rename.ts index 0fbe5db81f9..b25b9b3f74b 100644 --- a/src/vs/editor/contrib/rename/rename.ts +++ b/src/vs/editor/contrib/rename/rename.ts @@ -18,7 +18,7 @@ import { registerEditorAction, registerEditorContribution, ServicesAccessor, Edi import { IEditorContribution } from 'vs/editor/common/editorCommon'; import { ITextModel } from 'vs/editor/common/model'; import { EditorContextKeys } from 'vs/editor/common/editorContextKeys'; -import { createBulkEdit } from 'vs/editor/browser/services/bulkEdit'; +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'; @@ -147,9 +147,7 @@ class RenameController implements IEditorContribution { this._renameInputVisible.reset(); this.editor.focus(); - // start recording of file changes so that we can figure out if a file that - // is to be renamed conflicts with another (concurrent) modification - const edit = createBulkEdit(this._textModelResolverService, this.editor, this._fileService); + 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 = rename(this.editor.getModel(), this.editor.getPosition(), newName).then(result => { @@ -163,7 +161,7 @@ class RenameController implements IEditorContribution { } edit.add(result.edits); - return edit.finish().then(selection => { + return edit.perform().then(selection => { if (selection) { this.editor.setSelection(selection); } diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 58eb60dd225..ea436add39c 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -4907,27 +4907,19 @@ declare module monaco.languages { provideColorPresentations(model: editor.ITextModel, colorInfo: IColorInformation, token: CancellationToken): IColorPresentation[] | Thenable; } - export interface IResourceEdit { - resource: Uri; - range: IRange; - newText: string; - } - - export interface IResourceRename { - readonly from: Uri; - readonly to: Uri; + export interface ResourceFileEdit { + oldUri: Uri; + newUri: Uri; } - export interface IResourceCreate { - readonly uri: Uri; - readonly contents: string; + export interface ResourceTextEdit { + resource: Uri; + modelVersionId?: number; + edits: TextEdit[]; } export interface WorkspaceEdit { - edits: IResourceEdit[]; - renamedResources?: IResourceRename[]; - createdResources?: IResourceCreate[]; - deletedResources?: Uri[]; + edits: Array; rejectReason?: string; } diff --git a/src/vs/platform/progress/common/progress.ts b/src/vs/platform/progress/common/progress.ts index 75addbff74d..97fc957c02b 100644 --- a/src/vs/platform/progress/common/progress.ts +++ b/src/vs/platform/progress/common/progress.ts @@ -31,6 +31,12 @@ export interface IProgressRunner { done(): void; } +export const emptyProgressRunner: IProgressRunner = Object.freeze({ + total() { }, + worked() { }, + done() { } +}); + export interface IProgress { report(item: T): void; } diff --git a/src/vs/vscode.d.ts b/src/vs/vscode.d.ts index ad5cb36dc8c..1529b850f59 100644 --- a/src/vs/vscode.d.ts +++ b/src/vs/vscode.d.ts @@ -2430,29 +2430,6 @@ declare module 'vscode' { */ readonly size: number; - /** - * Renames a given resource in the workspace. - * - * @param from Uri of current resource. - * @param to Uri of renamed resource. - */ - renameResource(from: Uri, to: Uri): void; - - /** - * Create a new resource in the workspace. - * - * @param uri Uri of resource to create. - * @param contents New file contents. - */ - createResource(uri: Uri, contents: String): void; - - /** - * Delete a given resource in the workspace. - * - * @param uri Uri of resource to delete. - */ - deleteResource(uri: Uri): void; - /** * Replace the given range with given text for the given resource. * @@ -2510,19 +2487,40 @@ declare module 'vscode' { entries(): [Uri, TextEdit[]][]; /** - * Get all resource rename edits. + * Renames a given resource in the workspace. + * + * @param from Uri of current resource. + * @param to Uri of renamed resource. + */ + renameResource(from: Uri, to: Uri): void; + + /** + * Create a new resource in the workspace. + * + * @param uri Uri of resource to create. + */ + createResource(uri: Uri): void; + + /** + * Delete a given resource in the workspace. + * + * @param uri Uri of resource to delete. */ - readonly renamedResources: { from: Uri, to: Uri }[]; + deleteResource(uri: Uri): void; /** - * Get all resource create edits. + * Get the resource edits for this workspace edit. + * + * @returns A array of uri-tuples in which a rename-edit + * is represented as `[from, to]`, a delete-operation as `[from, null]`, + * and a create-operation as `[null, to]`; */ - readonly createdResources: { uri: Uri, contents: string }[]; + resourceEdits(): [Uri, Uri][]; /** - * Get all resource delete edits. + * */ - readonly deletedResources: Uri[]; + allEntries(): ([Uri, TextEdit[]] | [Uri, Uri])[]; } /** diff --git a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts index 1af196d2491..409e9df963d 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadEditors.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadEditors.ts @@ -15,17 +15,18 @@ import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/edi 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, IResourceFileEdit } from 'vs/workbench/api/node/extHost.protocol'; +import { ITextEditorConfigurationUpdate, TextEditorRevealType, IApplyEditsOptions, IUndoStopOptions, WorkspaceEditDto, reviveWorkspaceEditDto } from 'vs/workbench/api/node/extHost.protocol'; import { MainThreadDocumentsAndEditors } from './mainThreadDocumentsAndEditors'; import { equals as objectEquals } from 'vs/base/common/objects'; -import { ExtHostContext, MainThreadEditorsShape, ExtHostEditorsShape, ITextDocumentShowOptions, ITextEditorPositionData, IExtHostContext, IWorkspaceResourceEdit } from '../node/extHost.protocol'; +import { ExtHostContext, MainThreadEditorsShape, 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, IResourceEdit } from 'vs/editor/browser/services/bulkEdit'; +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'; export class MainThreadEditors implements MainThreadEditorsShape { @@ -210,40 +211,22 @@ export class MainThreadEditors implements MainThreadEditorsShape { return TPromise.as(this._documentsAndEditors.getEditor(id).applyEdits(modelVersionId, edits, opts)); } - $tryApplyWorkspaceEdit(workspaceResourceEdits: IWorkspaceResourceEdit[], resourceFileEdits?: IResourceFileEdit): TPromise { + $tryApplyWorkspaceEdit(dto: WorkspaceEditDto): TPromise { + + const { edits } = reviveWorkspaceEditDto(dto); // First check if loaded models were not changed in the meantime - for (let i = 0, len = workspaceResourceEdits.length; i < len; i++) { - const workspaceResourceEdit = workspaceResourceEdits[i]; - if (workspaceResourceEdit.modelVersionId) { - const uri = URI.revive(workspaceResourceEdit.resource); - let model = this._modelService.getModel(uri); - if (model && model.getVersionId() !== workspaceResourceEdit.modelVersionId) { + 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); } } } - // Convert to shape expected by bulkEdit below - let resourceEdits: IResourceEdit[] = []; - for (let i = 0, len = workspaceResourceEdits.length; i < len; i++) { - const workspaceResourceEdit = workspaceResourceEdits[i]; - const uri = URI.revive(workspaceResourceEdit.resource); - const edits = workspaceResourceEdit.edits; - - for (let j = 0, lenJ = edits.length; j < lenJ; j++) { - const edit = edits[j]; - - resourceEdits.push({ - resource: uri, - newText: edit.newText, - newEol: edit.newEol, - range: edit.range - }); - } - } - let codeEditor: ICodeEditor; let editor = this._workbenchEditorService.getActiveEditor(); if (editor) { @@ -253,17 +236,7 @@ export class MainThreadEditors implements MainThreadEditorsShape { } } - return bulkEdit( - this._textModelResolverService, - codeEditor, - resourceEdits, - this._fileService, - resourceFileEdits ? { - renamedResources: resourceFileEdits.renamedResources.map(entry => ({ from: URI.revive(entry.from), to: URI.revive(entry.to) })), - createdResources: resourceFileEdits.createdResources.map(entry => ({ uri: URI.revive(entry.uri), contents: entry.contents })), - deletedResources: resourceFileEdits.deletedResources.map(URI.revive) - } : undefined - ).then(() => true); + return BulkEdit.perform(edits, this._textModelResolverService, this._fileService, codeEditor).then(() => true); } $tryInsertSnippet(id: string, template: string, ranges: IRange[], opts: IUndoStopOptions): TPromise { diff --git a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts index d60034d69a2..77ff222bd12 100644 --- a/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts +++ b/src/vs/workbench/api/electron-browser/mainThreadLanguageFeatures.ts @@ -15,7 +15,7 @@ import { wireCancellationToken } from 'vs/base/common/async'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Position as EditorPosition } from 'vs/editor/common/core/position'; import { Range as EditorRange } from 'vs/editor/common/core/range'; -import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, SymbolInformationDto, WorkspaceEditDto, ResourceEditDto, CodeActionDto } from '../node/extHost.protocol'; +import { ExtHostContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, MainContext, IExtHostContext, ISerializedLanguageConfiguration, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, LocationDto, SymbolInformationDto, CodeActionDto, reviveWorkspaceEditDto } from '../node/extHost.protocol'; import { LanguageConfigurationRegistry } from 'vs/editor/common/modes/languageConfigurationRegistry'; import { LanguageConfiguration, IndentationRule, OnEnterRule } from 'vs/editor/common/modes/languageConfiguration'; import { IHeapService } from './mainThreadHeapService'; @@ -86,21 +86,9 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha } } - private static _reviveResourceEditDto(data: ResourceEditDto): modes.IResourceEdit { - data.resource = URI.revive(data.resource); - return data; - } - - private static _reviveWorkspaceEditDto(data: WorkspaceEditDto): modes.WorkspaceEdit { - if (data && data.edits) { - data.edits.forEach(MainThreadLanguageFeatures._reviveResourceEditDto); - } - return data; - } - private static _reviveCodeActionDto(data: CodeActionDto[]): modes.CodeAction[] { if (data) { - data.forEach(code => MainThreadLanguageFeatures._reviveWorkspaceEditDto(code.edit)); + data.forEach(code => reviveWorkspaceEditDto(code.edit)); } return data; } @@ -266,7 +254,7 @@ export class MainThreadLanguageFeatures implements MainThreadLanguageFeaturesSha $registerRenameSupport(handle: number, selector: vscode.DocumentSelector): void { this._registrations[handle] = modes.RenameProviderRegistry.register(toLanguageSelector(selector), { provideRenameEdits: (model: ITextModel, position: EditorPosition, newName: string, token: CancellationToken): Thenable => { - return wireCancellationToken(token, this._proxy.$provideRenameEdits(handle, model.uri, position, newName)).then(MainThreadLanguageFeatures._reviveWorkspaceEditDto); + return wireCancellationToken(token, this._proxy.$provideRenameEdits(handle, model.uri, position, newName)).then(reviveWorkspaceEditDto); } }); } diff --git a/src/vs/workbench/api/node/extHost.protocol.ts b/src/vs/workbench/api/node/extHost.protocol.ts index 0d3ad1d8df1..dad2c493041 100644 --- a/src/vs/workbench/api/node/extHost.protocol.ts +++ b/src/vs/workbench/api/node/extHost.protocol.ts @@ -14,7 +14,7 @@ import { import * as vscode from 'vscode'; -import { UriComponents } from 'vs/base/common/uri'; +import URI, { UriComponents } from 'vs/base/common/uri'; import Severity from 'vs/base/common/severity'; import { TPromise } from 'vs/base/common/winjs.base'; @@ -52,7 +52,7 @@ import { IStat, IFileChange } from 'vs/platform/files/common/files'; import { ConfigurationScope } from 'vs/platform/configuration/common/configurationRegistry'; import { ParsedArgs } from 'vs/platform/environment/common/environment'; import { CommentRule, CharacterPair, EnterAction } from 'vs/editor/common/modes/languageConfiguration'; -import { EndOfLineSequence, ISingleEditOperation } from 'vs/editor/common/model'; +import { ISingleEditOperation } from 'vs/editor/common/model'; export interface IEnvironment { isExtensionDevelopmentDebug: boolean; @@ -191,8 +191,6 @@ export interface IApplyEditsOptions extends IUndoStopOptions { setEndOfLine: EndOfLine; } - - export interface ITextDocumentShowOptions { position?: EditorPosition; preserveFocus?: boolean; @@ -200,22 +198,6 @@ export interface ITextDocumentShowOptions { selection?: IRange; } -export interface IWorkspaceResourceEdit { - resource: UriComponents; - modelVersionId?: number; - edits: { - range?: IRange; - newText: string; - newEol?: EndOfLineSequence; - }[]; -} - -export interface IResourceFileEdit { - renamedResources: { from: UriComponents, to: UriComponents }[]; - createdResources: { uri: UriComponents, contents: string }[]; - deletedResources: UriComponents[]; -} - export interface MainThreadEditorsShape extends IDisposable { $tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): TPromise; $registerTextEditorDecorationType(key: string, options: editorCommon.IDecorationRenderOptions): void; @@ -228,7 +210,7 @@ export interface MainThreadEditorsShape extends IDisposable { $tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): TPromise; $trySetSelections(id: string, selections: ISelection[]): TPromise; $tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): TPromise; - $tryApplyWorkspaceEdit(workspaceResourceEdits: IWorkspaceResourceEdit[], resourceFileEdits?: IResourceFileEdit): TPromise; + $tryApplyWorkspaceEdit(workspaceEditDto: WorkspaceEditDto): TPromise; $tryInsertSnippet(id: string, template: string, selections: IRange[], opts: IUndoStopOptions): TPromise; $getDiffInformation(id: string): TPromise; } @@ -622,17 +604,38 @@ export interface WorkspaceSymbolsDto extends IdObject { symbols: SymbolInformationDto[]; } -export interface ResourceEditDto { +export interface ResourceFileEditDto { + oldUri: UriComponents; + newUri: UriComponents; +} + +export interface ResourceTextEditDto { resource: UriComponents; - range: IRange; - newText: string; + modelVersionId?: number; + edits: modes.TextEdit[]; } export interface WorkspaceEditDto { - edits: ResourceEditDto[]; + edits: (ResourceFileEditDto | ResourceTextEditDto)[]; + + // todo@joh reject should go into rename rejectReason?: string; } +export function reviveWorkspaceEditDto(data: WorkspaceEditDto): modes.WorkspaceEdit { + if (data && data.edits) { + for (const edit of data.edits) { + if (typeof (edit).resource === 'object') { + (edit).resource = URI.revive((edit).resource); + } else { + (edit).newUri = URI.revive((edit).newUri); + (edit).oldUri = URI.revive((edit).oldUri); + } + } + } + return data; +} + export interface CodeActionDto { title: string; edit?: WorkspaceEditDto; diff --git a/src/vs/workbench/api/node/extHostApiCommands.ts b/src/vs/workbench/api/node/extHostApiCommands.ts index 62652183f85..d29bab7425a 100644 --- a/src/vs/workbench/api/node/extHostApiCommands.ts +++ b/src/vs/workbench/api/node/extHostApiCommands.ts @@ -344,11 +344,7 @@ export class ExtHostApiCommands { if (value.rejectReason) { return TPromise.wrapError(new Error(value.rejectReason)); } - let workspaceEdit = new types.WorkspaceEdit(); - for (let edit of value.edits) { - workspaceEdit.replace(edit.resource, typeConverters.toRange(edit.range), edit.newText); - } - return workspaceEdit; + return typeConverters.WorkspaceEdit.to(value); }); } diff --git a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts index 3733a20adbc..d6edc95eb20 100644 --- a/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts +++ b/src/vs/workbench/api/node/extHostDocumentSaveParticipant.ts @@ -8,7 +8,7 @@ import Event from 'vs/base/common/event'; import URI, { UriComponents } from 'vs/base/common/uri'; import { sequence, always } from 'vs/base/common/async'; import { illegalState } from 'vs/base/common/errors'; -import { ExtHostDocumentSaveParticipantShape, MainThreadEditorsShape, IWorkspaceResourceEdit } from 'vs/workbench/api/node/extHost.protocol'; +import { ExtHostDocumentSaveParticipantShape, MainThreadEditorsShape, ResourceTextEditDto } from 'vs/workbench/api/node/extHost.protocol'; import { TextEdit } from 'vs/workbench/api/node/extHostTypes'; import { fromRange, TextDocumentSaveReason, EndOfLine } from 'vs/workbench/api/node/extHostTypeConverters'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; @@ -142,7 +142,7 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic }).then(values => { - let workspaceResourceEdit: IWorkspaceResourceEdit = { + const resourceEdit: ResourceTextEditDto = { resource: document.uri, edits: [] }; @@ -150,10 +150,10 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic for (const value of values) { if (Array.isArray(value) && (value).every(e => e instanceof TextEdit)) { for (const { newText, newEol, range } of value) { - workspaceResourceEdit.edits.push({ + resourceEdit.edits.push({ range: range && fromRange(range), - newText, - newEol: EndOfLine.from(newEol) + text: newText, + eol: EndOfLine.from(newEol) }); } } @@ -161,12 +161,12 @@ export class ExtHostDocumentSaveParticipant implements ExtHostDocumentSavePartic // apply edits if any and if document // didn't change somehow in the meantime - if (workspaceResourceEdit.edits.length === 0) { + if (resourceEdit.edits.length === 0) { return undefined; } if (version === document.version) { - return this._mainThreadEditors.$tryApplyWorkspaceEdit([workspaceResourceEdit]); + return this._mainThreadEditors.$tryApplyWorkspaceEdit({ edits: [resourceEdit] }); } // TODO@joh bubble this to listener? diff --git a/src/vs/workbench/api/node/extHostLanguageFeatures.ts b/src/vs/workbench/api/node/extHostLanguageFeatures.ts index 37ac8015683..731fccd8492 100644 --- a/src/vs/workbench/api/node/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/node/extHostLanguageFeatures.ts @@ -17,7 +17,7 @@ import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostCommands, CommandsConverter } from 'vs/workbench/api/node/extHostCommands'; import { ExtHostDiagnostics, DiagnosticCollection } from 'vs/workbench/api/node/extHostDiagnostics'; import { asWinJsPromise } from 'vs/base/common/async'; -import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, SymbolInformationDto, SuggestResultDto, WorkspaceSymbolsDto, SuggestionDto } from './extHost.protocol'; +import { MainContext, MainThreadLanguageFeaturesShape, ExtHostLanguageFeaturesShape, ObjectIdentifier, IRawColorInfo, IMainContext, IdObject, ISerializedRegExp, ISerializedIndentationRule, ISerializedOnEnterRule, ISerializedLanguageConfiguration, SymbolInformationDto, SuggestResultDto, WorkspaceSymbolsDto, SuggestionDto, CodeActionDto } from './extHost.protocol'; import { regExpLeadsToEndlessLoop } from 'vs/base/common/strings'; import { IPosition } from 'vs/editor/common/core/position'; import { IRange } from 'vs/editor/common/core/range'; @@ -255,7 +255,7 @@ class ReferenceAdapter { } } -export interface CustomCodeAction extends modes.CodeAction { +export interface CustomCodeAction extends CodeActionDto { _isSynthetic?: boolean; } @@ -273,7 +273,7 @@ class CodeActionAdapter { this._provider = provider; } - provideCodeActions(resource: URI, range: IRange): TPromise { + provideCodeActions(resource: URI, range: IRange): TPromise { const doc = this._documents.getDocumentData(resource).document; const ran = TypeConverters.toRange(range); @@ -943,7 +943,7 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape { return this._createDisposable(handle); } - $provideCodeActions(handle: number, resource: UriComponents, range: IRange): TPromise { + $provideCodeActions(handle: number, resource: UriComponents, range: IRange): TPromise { return this._withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeActions(URI.revive(resource), range)); } diff --git a/src/vs/workbench/api/node/extHostTextEditors.ts b/src/vs/workbench/api/node/extHostTextEditors.ts index 2b3738a27f3..ea973d68b22 100644 --- a/src/vs/workbench/api/node/extHostTextEditors.ts +++ b/src/vs/workbench/api/node/extHostTextEditors.ts @@ -12,7 +12,7 @@ import * as TypeConverters from './extHostTypeConverters'; import { TextEditorDecorationType, ExtHostTextEditor } from './extHostTextEditor'; import { ExtHostDocumentsAndEditors } from './extHostDocumentsAndEditors'; import { Position as EditorPosition } from 'vs/platform/editor/common/editor'; -import { MainContext, MainThreadEditorsShape, ExtHostEditorsShape, ITextDocumentShowOptions, ITextEditorPositionData, IResolvedTextEditorConfiguration, ISelectionChangeEvent, IMainContext, IWorkspaceResourceEdit } from './extHost.protocol'; +import { MainContext, MainThreadEditorsShape, ExtHostEditorsShape, ITextDocumentShowOptions, ITextEditorPositionData, IResolvedTextEditorConfiguration, ISelectionChangeEvent, IMainContext, WorkspaceEditDto } from './extHost.protocol'; import * as vscode from 'vscode'; export class ExtHostEditors implements ExtHostEditorsShape { @@ -92,40 +92,23 @@ export class ExtHostEditors implements ExtHostEditorsShape { applyWorkspaceEdit(edit: vscode.WorkspaceEdit): TPromise { - let workspaceResourceEdits: IWorkspaceResourceEdit[] = []; + const dto: WorkspaceEditDto = { edits: [] }; - let entries = edit.entries(); - for (let entry of entries) { - let [uri, edits] = entry; - - let doc = this._extHostDocumentsAndEditors.getDocument(uri.toString()); - let docVersion: number = undefined; - if (doc) { - docVersion = doc.version; - } - - let workspaceResourceEdit: IWorkspaceResourceEdit = { - resource: uri, - modelVersionId: docVersion, - edits: [] - }; - - for (let edit of edits) { - workspaceResourceEdit.edits.push({ - newText: edit.newText, - newEol: TypeConverters.EndOfLine.from(edit.newEol), - range: edit.range && TypeConverters.fromRange(edit.range) + for (let entry of edit.allEntries()) { + let [uri, uriOrEdits] = entry; + if (Array.isArray(uriOrEdits)) { + let doc = this._extHostDocumentsAndEditors.getDocument(uri.toString()); + dto.edits.push({ + resource: uri, + modelVersionId: doc && doc.version, + edits: uriOrEdits.map(TypeConverters.TextEdit.from) }); + } else { + dto.edits.push({ oldUri: uri, newUri: uriOrEdits }); } - - workspaceResourceEdits.push(workspaceResourceEdit); } - return this._proxy.$tryApplyWorkspaceEdit(workspaceResourceEdits, { - createdResources: edit.createdResources, - renamedResources: edit.renamedResources, - deletedResources: edit.deletedResources - }); + return this._proxy.$tryApplyWorkspaceEdit(dto); } // --- called from main thread diff --git a/src/vs/workbench/api/node/extHostTypeConverters.ts b/src/vs/workbench/api/node/extHostTypeConverters.ts index f1379708459..af3ba6607cd 100644 --- a/src/vs/workbench/api/node/extHostTypeConverters.ts +++ b/src/vs/workbench/api/node/extHostTypeConverters.ts @@ -20,6 +20,7 @@ import { ISelection } from 'vs/editor/common/core/selection'; import * as htmlContent from 'vs/base/common/htmlContent'; import { IRelativePattern } from 'vs/base/common/glob'; import { LanguageSelector, LanguageFilter } from 'vs/editor/common/modes/languageSelector'; +import { WorkspaceEditDto, ResourceTextEditDto, ResourceFileEditDto } from 'vs/workbench/api/node/extHost.protocol'; export interface PositionLike { line: number; @@ -229,28 +230,35 @@ export const TextEdit = { export namespace WorkspaceEdit { export function from(value: vscode.WorkspaceEdit): modes.WorkspaceEdit { const result: modes.WorkspaceEdit = { - edits: [], - renamedResources: value.renamedResources, - createdResources: value.createdResources, - deletedResources: value.deletedResources + edits: [] }; - for (let entry of value.entries()) { - let [uri, textEdits] = entry; - for (let textEdit of textEdits) { - result.edits.push({ - resource: uri, - newText: textEdit.newText, - range: fromRange(textEdit.range) - }); + for (const entry of value.allEntries()) { + const [uri, uriOrEdits] = entry; + if (Array.isArray(uriOrEdits)) { + // text edits + result.edits.push({ resource: uri, edits: uriOrEdits.map(TextEdit.from) }); + } else { + // resource edits + result.edits.push({ oldUri: uri, newUri: uriOrEdits }); } } return result; } - export function to(value: modes.WorkspaceEdit) { + export function to(value: WorkspaceEditDto) { const result = new types.WorkspaceEdit(); for (const edit of value.edits) { - result.replace(edit.resource, toRange(edit.range), edit.newText); + if (Array.isArray((edit).edits)) { + result.set( + URI.revive((edit).resource), + (edit).edits.map(TextEdit.to) + ); + } else { + result.renameResource( + URI.revive((edit).oldUri), + URI.revive((edit).newUri) + ); + } } return result; } diff --git a/src/vs/workbench/api/node/extHostTypes.ts b/src/vs/workbench/api/node/extHostTypes.ts index a7b250b6d69..b7351530c0c 100644 --- a/src/vs/workbench/api/node/extHostTypes.ts +++ b/src/vs/workbench/api/node/extHostTypes.ts @@ -491,44 +491,28 @@ export class TextEdit { } } -export class WorkspaceEdit { +export class WorkspaceEdit implements vscode.WorkspaceEdit { - private _values: [URI, TextEdit[]][] = []; - private readonly _resourcesCreated: { uri: URI, contents: string }[] = []; - private readonly _resourcesDeleted: URI[] = []; - private readonly _resourcesRenamed: { from: URI, to: URI }[] = []; - private _index = new Map(); + private _clock: number = 0; - private _validResources = new Set(); - private _invalidResources = new Set(); + private _resourceEdits: [number/*time*/, URI, URI][] = []; + private _textEdits: [URI, TextEdit[]][] = []; + private _textEditsIndex = new Map(); + createResource(uri: vscode.Uri): void { + this.renameResource(undefined, uri); + } - createResource(uri: URI, contents: string): void { - if (this._invalidResources.has(uri)) { - throw illegalArgument('Cannot create already deleted resource'); - } - this._resourcesCreated.push({ uri: uri, contents: contents }); - this._validResources.add(uri); + deleteResource(uri: vscode.Uri): void { + this.renameResource(uri, undefined); } - deleteResource(uri: URI): void { - if (this._validResources.has(uri)) { - throw illegalArgument('Cannot delete newly created resource'); - } - this._resourcesDeleted.push(uri); - this._invalidResources.add(uri); + renameResource(from: vscode.Uri, to: vscode.Uri): void { + this._resourceEdits.push([this._clock++, from, to]); } - renameResource(uri: URI, newUri: URI): void { - if (this._validResources.has(uri)) { - throw illegalArgument('Cannot delete newly created resource'); - } - if (this._invalidResources.has(newUri)) { - throw illegalArgument('Cannot create already deleted resource'); - } - this._resourcesRenamed.push({ from: uri, to: newUri }); - this._invalidResources.add(uri); - this._validResources.add(newUri); + resourceEdits(): [vscode.Uri, vscode.Uri][] { + return this._resourceEdits.map(([, oldUri, newUri]) => (<[vscode.Uri, vscode.Uri]>[oldUri, newUri])); } replace(uri: URI, range: Range, newText: string): void { @@ -550,50 +534,54 @@ export class WorkspaceEdit { } has(uri: URI): boolean { - return this._index.has(uri.toString()); + return this._textEditsIndex.has(uri.toString()); } set(uri: URI, edits: TextEdit[]): void { - if (this._invalidResources.has(uri)) { - throw illegalArgument('Cannot modify already deleted resource'); - } - this._validResources.add(uri); - const idx = this._index.get(uri.toString()); - if (typeof idx === 'undefined') { - let newLen = this._values.push([uri, edits]); - this._index.set(uri.toString(), newLen - 1); + if (!this._textEditsIndex.has(uri.toString())) { + let newLen = this._textEdits.push([uri, edits]); + this._textEditsIndex.set(uri.toString(), [newLen - 1, this._clock++]); } else { - this._values[idx][1] = edits; + const [idx] = this._textEditsIndex.get(uri.toString()); + this._textEdits[idx][1] = edits; } } get(uri: URI): TextEdit[] { - let idx = this._index.get(uri.toString()); - return typeof idx !== 'undefined' && this._values[idx][1]; + if (!this._textEditsIndex.has(uri.toString())) { + return undefined; + } + const [idx] = this._textEditsIndex.get(uri.toString()); + return this._textEdits[idx][1]; } entries(): [URI, TextEdit[]][] { - return this._values; - } - - get createdResources(): { uri: URI, contents: string }[] { - return this._resourcesCreated; - } - - get deletedResources(): URI[] { - return this._resourcesDeleted; - } - - get renamedResources(): { from: URI, to: URI }[] { - return this._resourcesRenamed; + // todo@joh - make this immutable + return this._textEdits; + } + + allEntries(): ([URI, TextEdit[]] | [URI, URI])[] { + // use the 'time' the we have assigned when inserting + // the operation and use that order in the resulting + // array + const res: ([URI, TextEdit[]] | [URI, URI])[] = []; + this._textEditsIndex.forEach(value => { + const [index, time] = value; + res[time] = this._textEdits[index]; + }); + this._resourceEdits.forEach(value => { + const [time, oldUri, newUri] = value; + res[time] = [oldUri, newUri]; + }); + return res; } get size(): number { - return this._values.length + this._resourcesCreated.length + this._resourcesRenamed.length + this._resourcesDeleted.length; + return this._textEdits.length + this._resourceEdits.length; } toJSON(): any { - return this._values; + return this._textEdits; } } diff --git a/src/vs/workbench/parts/search/browser/replaceService.ts b/src/vs/workbench/parts/search/browser/replaceService.ts index c93549e85dd..82bbc09d363 100644 --- a/src/vs/workbench/parts/search/browser/replaceService.ts +++ b/src/vs/workbench/parts/search/browser/replaceService.ts @@ -15,7 +15,7 @@ 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, IResourceEdit, createBulkEdit } from 'vs/editor/browser/services/bulkEdit'; +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'; @@ -24,6 +24,7 @@ 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'; const REPLACE_PREVIEW = 'replacePreview'; @@ -103,8 +104,7 @@ export class ReplaceService implements IReplaceService { public replace(match: FileMatchOrMatch, progress?: IProgressRunner, resource?: URI): TPromise; public replace(arg: any, progress: IProgressRunner = null, resource: URI = null): TPromise { - let bulkEdit: BulkEdit = createBulkEdit(this.textModelResolverService, null, this.fileService); - bulkEdit.progress(progress); + let bulkEdit = new BulkEdit(null, progress, this.textModelResolverService, this.fileService); if (arg instanceof Match) { let match = arg; @@ -126,7 +126,7 @@ export class ReplaceService implements IReplaceService { }); } - return bulkEdit.finish(); + return bulkEdit.perform(); } public openReplacePreview(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise { @@ -174,12 +174,14 @@ export class ReplaceService implements IReplaceService { }); } - private createEdit(match: Match, text: string, resource: URI = null): IResourceEdit { + private createEdit(match: Match, text: string, resource: URI = null): ResourceTextEdit { let fileMatch: FileMatch = match.parent(); - let resourceEdit: IResourceEdit = { + let resourceEdit: ResourceTextEdit = { resource: resource !== null ? resource : fileMatch.resource(), - range: match.range(), - newText: text + edits: [{ + range: match.range(), + text: text + }] }; return resourceEdit; } diff --git a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts index a4c0366b6e4..8ec74bfb073 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostDocumentSaveParticipant.test.ts @@ -10,7 +10,7 @@ import { TPromise } from 'vs/base/common/winjs.base'; import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { TextDocumentSaveReason, TextEdit, Position, EndOfLine } from 'vs/workbench/api/node/extHostTypes'; -import { MainThreadEditorsShape, IWorkspaceResourceEdit } from 'vs/workbench/api/node/extHost.protocol'; +import { MainThreadEditorsShape, WorkspaceEditDto } from 'vs/workbench/api/node/extHost.protocol'; import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/node/extHostDocumentSaveParticipant'; import { SingleProxyRPCProtocol } from './testRPCProtocol'; import { SaveReason } from 'vs/workbench/services/textfile/common/textfiles'; @@ -18,6 +18,7 @@ import * as vscode from 'vscode'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { NullLogService } from 'vs/platform/log/common/log'; +import { isResourceTextEdit, ResourceTextEdit } from 'vs/editor/common/modes'; suite('ExtHostDocumentSaveParticipant', () => { @@ -262,10 +263,10 @@ suite('ExtHostDocumentSaveParticipant', () => { test('event delivery, pushEdits sync', () => { - let edits: IWorkspaceResourceEdit[]; + let dto: WorkspaceEditDto; const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock() { - $tryApplyWorkspaceEdit(_edits: IWorkspaceResourceEdit[]) { - edits = _edits; + $tryApplyWorkspaceEdit(_edits: WorkspaceEditDto) { + dto = _edits; return TPromise.as(true); } }); @@ -278,16 +279,17 @@ suite('ExtHostDocumentSaveParticipant', () => { return participant.$participateInSave(resource, SaveReason.EXPLICIT).then(() => { sub.dispose(); - assert.equal(edits.length, 1); - assert.equal(edits[0].edits.length, 2); + assert.equal(dto.edits.length, 1); + assert.ok(isResourceTextEdit(dto.edits[0])); + assert.equal((dto.edits[0]).edits.length, 2); }); }); test('event delivery, concurrent change', () => { - let edits: IWorkspaceResourceEdit[]; + let edits: WorkspaceEditDto; const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock() { - $tryApplyWorkspaceEdit(_edits: IWorkspaceResourceEdit[]) { + $tryApplyWorkspaceEdit(_edits: WorkspaceEditDto) { edits = _edits; return TPromise.as(true); } @@ -321,16 +323,20 @@ suite('ExtHostDocumentSaveParticipant', () => { test('event delivery, two listeners -> two document states', () => { const participant = new ExtHostDocumentSaveParticipant(nullLogService, documents, new class extends mock() { - $tryApplyWorkspaceEdit(_edits: IWorkspaceResourceEdit[]) { + $tryApplyWorkspaceEdit(dto: WorkspaceEditDto) { - for (const { resource, edits } of _edits) { + for (const edit of dto.edits) { + if (!isResourceTextEdit(edit)) { + continue; + } + const { resource, edits } = edit; const uri = URI.revive(resource); - for (const { newText, range } of edits) { + for (const { text, range } of edits) { documents.$acceptModelChanged(uri.toString(), { changes: [{ range, + text, rangeLength: undefined, - text: newText }], eol: undefined, versionId: documents.getDocumentData(uri).version + 1 diff --git a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts index 256ebb6879f..f85ee97b9cb 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostLanguageFeatures.test.ts @@ -24,7 +24,7 @@ import { IHeapService } from 'vs/workbench/api/electron-browser/mainThreadHeapSe import { ExtHostDocuments } from 'vs/workbench/api/node/extHostDocuments'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { getDocumentSymbols } from 'vs/editor/contrib/quickOpen/quickOpen'; -import { DocumentSymbolProviderRegistry, DocumentHighlightKind, Hover } from 'vs/editor/common/modes'; +import { DocumentSymbolProviderRegistry, DocumentHighlightKind, Hover, ResourceTextEdit } from 'vs/editor/common/modes'; import { getCodeLensData } from 'vs/editor/contrib/codelens/codelens'; import { getDefinitionsAtPosition, getImplementationsAtPosition, getTypeDefinitionsAtPosition } from 'vs/editor/contrib/goToDeclaration/goToDeclaration'; import { getHover } from 'vs/editor/contrib/hover/getHover'; @@ -812,7 +812,8 @@ suite('ExtHostLanguageFeatures', function () { return rpcProtocol.sync().then(() => { return rename(model, new EditorPosition(1, 1), 'newName').then(value => { - assert.equal(value.edits.length, 2); // least relevant renamer + assert.equal(value.edits.length, 1); // least relevant renamer + assert.equal((value.edits)[0].edits.length, 2); // least relevant renamer }); }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts index b5ec3f55274..41663d277ef 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTextEditors.test.ts @@ -7,25 +7,26 @@ import * as assert from 'assert'; import { TPromise } from 'vs/base/common/winjs.base'; import * as extHostTypes from 'vs/workbench/api/node/extHostTypes'; -import { MainContext, MainThreadEditorsShape, IWorkspaceResourceEdit } from 'vs/workbench/api/node/extHost.protocol'; +import { MainContext, MainThreadEditorsShape, WorkspaceEditDto } from 'vs/workbench/api/node/extHost.protocol'; import URI from 'vs/base/common/uri'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { ExtHostDocumentsAndEditors } from 'vs/workbench/api/node/extHostDocumentsAndEditors'; import { SingleProxyRPCProtocol, TestRPCProtocol } from 'vs/workbench/test/electron-browser/api/testRPCProtocol'; import { ExtHostEditors } from 'vs/workbench/api/node/extHostTextEditors'; +import { ResourceTextEdit } from 'vs/editor/common/modes'; suite('ExtHostTextEditors.applyWorkspaceEdit', () => { const resource = URI.parse('foo:bar'); let editors: ExtHostEditors; - let workspaceResourceEdits: IWorkspaceResourceEdit[]; + let workspaceResourceEdits: WorkspaceEditDto; setup(() => { workspaceResourceEdits = null; let rpcProtocol = new TestRPCProtocol(); rpcProtocol.set(MainContext.MainThreadEditors, new class extends mock() { - $tryApplyWorkspaceEdit(_workspaceResourceEdits: IWorkspaceResourceEdit[]): TPromise { + $tryApplyWorkspaceEdit(_workspaceResourceEdits: WorkspaceEditDto): TPromise { workspaceResourceEdits = _workspaceResourceEdits; return TPromise.as(true); } @@ -48,8 +49,8 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { let edit = new extHostTypes.WorkspaceEdit(); edit.replace(resource, new extHostTypes.Range(0, 0, 0, 0), 'hello'); return editors.applyWorkspaceEdit(edit).then((result) => { - assert.equal(workspaceResourceEdits.length, 1); - assert.equal(workspaceResourceEdits[0].modelVersionId, 1337); + assert.equal(workspaceResourceEdits.edits.length, 1); + assert.equal((workspaceResourceEdits.edits[0]).modelVersionId, 1337); }); }); @@ -57,8 +58,8 @@ suite('ExtHostTextEditors.applyWorkspaceEdit', () => { let edit = new extHostTypes.WorkspaceEdit(); edit.replace(URI.parse('foo:bar2'), new extHostTypes.Range(0, 0, 0, 0), 'hello'); return editors.applyWorkspaceEdit(edit).then((result) => { - assert.equal(workspaceResourceEdits.length, 1); - assert.ok(typeof workspaceResourceEdits[0].modelVersionId === 'undefined'); + assert.equal(workspaceResourceEdits.edits.length, 1); + assert.ok(typeof (workspaceResourceEdits.edits[0]).modelVersionId === 'undefined'); }); }); diff --git a/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts b/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts index 3e99b175fc7..478b506e60f 100644 --- a/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts +++ b/src/vs/workbench/test/electron-browser/api/extHostTypes.test.ts @@ -362,17 +362,51 @@ suite('ExtHostTypes', function () { }); - test('WorkspaceEdit should fail when editing deleted resource', () => { - const resource = URI.parse('file:///a.ts'); + // test('WorkspaceEdit should fail when editing deleted resource', () => { + // const resource = URI.parse('file:///a.ts'); + + // const edit = new types.WorkspaceEdit(); + // edit.deleteResource(resource); + // try { + // edit.insert(resource, new types.Position(0, 0), ''); + // assert.fail(false, 'Should disallow edit of deleted resource'); + // } catch { + // // expected + // } + // }); + + test('WorkspaceEdit - keep order of text and file changes', function () { const edit = new types.WorkspaceEdit(); - edit.deleteResource(resource); - try { - edit.insert(resource, new types.Position(0, 0), ''); - assert.fail(false, 'Should disallow edit of deleted resource'); - } catch { - // expected + edit.replace(URI.parse('foo:a'), new types.Range(1, 1, 1, 1), 'foo'); + edit.renameResource(URI.parse('foo:a'), URI.parse('foo:b')); + edit.replace(URI.parse('foo:a'), new types.Range(2, 1, 2, 1), 'bar'); + edit.replace(URI.parse('foo:b'), new types.Range(3, 1, 3, 1), 'bazz'); + + const all = edit.allEntries(); + assert.equal(all.length, 3); + + function isFileChange(thing: [URI, types.TextEdit[]] | [URI, URI]): thing is [URI, URI] { + const [f, s] = thing; + return URI.isUri(f) && URI.isUri(s); } + + function isTextChange(thing: [URI, types.TextEdit[]] | [URI, URI]): thing is [URI, types.TextEdit[]] { + const [f, s] = thing; + return URI.isUri(f) && Array.isArray(s); + } + + const [first, second, third] = all; + assert.equal(first[0].toString(), 'foo:a'); + assert.ok(!isFileChange(first)); + assert.ok(isTextChange(first) && first[1].length === 2); + + assert.equal(second[0].toString(), 'foo:a'); + assert.ok(isFileChange(second)); + + assert.equal(third[0].toString(), 'foo:b'); + assert.ok(!isFileChange(third)); + assert.ok(isTextChange(third) && third[1].length === 1); }); test('DocumentLink', function () { 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 b725c6163bd..da8f3fb2498 100644 --- a/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts +++ b/src/vs/workbench/test/electron-browser/api/mainThreadEditors.test.ts @@ -13,7 +13,7 @@ import { ModelServiceImpl } from 'vs/editor/common/services/modelServiceImpl'; import { TestCodeEditorService } from 'vs/editor/test/browser/testCodeEditorService'; import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; -import { ExtHostDocumentsAndEditorsShape, IWorkspaceResourceEdit, ExtHostContext, ExtHostDocumentsShape } from 'vs/workbench/api/node/extHost.protocol'; +import { ExtHostDocumentsAndEditorsShape, ExtHostContext, ExtHostDocumentsShape } from 'vs/workbench/api/node/extHost.protocol'; import { mock } from 'vs/workbench/test/electron-browser/api/mock'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import Event from 'vs/base/common/event'; @@ -26,6 +26,7 @@ import { EditOperation } from 'vs/editor/common/core/editOperation'; 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'; suite('MainThreadEditors', () => { @@ -35,7 +36,7 @@ suite('MainThreadEditors', () => { let editors: MainThreadEditors; const movedResources = new Map(); - const createdResources = new Map(); + const createdResources = new Set(); const deletedResources = new Set(); setup(() => { @@ -46,23 +47,22 @@ suite('MainThreadEditors', () => { movedResources.clear(); createdResources.clear(); deletedResources.clear(); - const fileService = new TestFileService(); - fileService.moveFile = async (from, target): TPromise => { - assert(!movedResources.has(from)); - movedResources.set(from, target); - return createMockFileStat(target); - }; - fileService.createFile = async (uri, contents): TPromise => { - assert(!createdResources.has(uri)); - createdResources.set(uri, contents); - return createMockFileStat(uri); - }; - fileService.del = async (uri): TPromise => { - assert(!deletedResources.has(uri)); - deletedResources.add(uri); + const fileService = new class extends TestFileService { + async moveFile(from, target): TPromise { + movedResources.set(from, target); + return createMockFileStat(target); + } + async createFile(uri): TPromise { + createdResources.add(uri); + return createMockFileStat(uri); + } + async del(uri): TPromise { + deletedResources.add(uri); + } }; + const textFileService = new class extends mock() { isDirty() { return false; } models = { @@ -119,11 +119,11 @@ suite('MainThreadEditors', () => { let model = modelService.createModel('something', null, resource); - let workspaceResourceEdit: IWorkspaceResourceEdit = { + let workspaceResourceEdit: ResourceTextEdit = { resource: resource, modelVersionId: model.getVersionId(), edits: [{ - newText: 'asdfg', + text: 'asdfg', range: new Range(1, 1, 1, 1) }] }; @@ -131,28 +131,22 @@ suite('MainThreadEditors', () => { // Act as if the user edited the model model.applyEdits([EditOperation.insert(new Position(0, 0), 'something')]); - return editors.$tryApplyWorkspaceEdit([workspaceResourceEdit]).then((result) => { + return editors.$tryApplyWorkspaceEdit({ edits: [workspaceResourceEdit] }).then((result) => { assert.equal(result, false); }); }); test(`applyWorkspaceEdit with only resource edit`, () => { - let model = modelService.createModel('something', null, resource); - - let workspaceResourceEdit: IWorkspaceResourceEdit = { - resource: resource, - modelVersionId: model.getVersionId(), - edits: [] - }; - - return editors.$tryApplyWorkspaceEdit([workspaceResourceEdit], { - renamedResources: [{ from: resource, to: resource }], - createdResources: [{ uri: resource, contents: 'foo' }], - deletedResources: [resource] + return editors.$tryApplyWorkspaceEdit({ + edits: [ + { oldUri: resource, newUri: resource }, + { oldUri: undefined, newUri: resource }, + { oldUri: resource, newUri: undefined } + ] }).then((result) => { assert.equal(result, true); assert.equal(movedResources.get(resource), resource); - assert.equal(createdResources.get(resource), 'foo'); + assert.equal(createdResources.has(resource), true); assert.equal(deletedResources.has(resource), true); }); }); -- GitLab