Skip to content

Commit

Permalink
Add rename/create/delete file to workspaced edit
Browse files Browse the repository at this point in the history
Fixes #10659

Allows workspace edits to also change files in the workspace
  • Loading branch information
mjbvz committed Jan 12, 2018
1 parent 05b667d commit e932b8a
Show file tree
Hide file tree
Showing 13 changed files with 304 additions and 23 deletions.
14 changes: 14 additions & 0 deletions extensions/vscode-api-tests/src/workspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,4 +505,18 @@ suite('workspace-namespace', () => {
return vscode.workspace.applyEdit(edit);
});
});


test('applyEdit should fail when editing deleted resource', async () => {
const resource = await createRandomFile();

let edit = new vscode.WorkspaceEdit();
edit.deleteResource(resource);
try {
edit.insert(resource, new vscode.Position(0, 0), '');
assert.fail(false, 'Should disallow edit of deleted resource');
} catch {
// noop
}
});
});
71 changes: 62 additions & 9 deletions src/vs/editor/browser/services/bulkEdit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,13 @@ import { Selection, ISelection } from 'vs/editor/common/core/selection';
import { IIdentifiedSingleEditOperation, ITextModel, EndOfLineSequence } from 'vs/editor/common/model';
import { IProgressRunner } from 'vs/platform/progress/common/progress';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { IResourceRename, IResourceCreate } from 'vs/editor/common/modes';

export interface IResourceFileEdit {
readonly renamedResources: { from: URI, to }[];
readonly createdResources: { uri: URI, contents: string }[];
readonly deletedResources: URI[];
}

export interface IResourceEdit {
resource: URI;
Expand Down Expand Up @@ -196,12 +203,24 @@ class BulkEditModel implements IDisposable {
private _sourceSelections: Selection[];
private _sourceModelTask: SourceModelEditTask;

constructor(textModelResolverService: ITextModelService, sourceModel: URI, sourceSelections: Selection[], edits: IResourceEdit[], private progress: IProgressRunner = null) {
constructor(
textModelResolverService: ITextModelService,
sourceModel: URI,
sourceSelections: Selection[],
edits: IResourceEdit[],
private progress: IProgressRunner,
private renames: IResourceRename[],
private creates: IResourceCreate[],
private deletes: URI[],
private fileService: IFileService
) {
this._textModelResolverService = textModelResolverService;
this._sourceModel = sourceModel;
this._sourceSelections = sourceSelections;
this._sourceModelTask = null;

this._numberOfResourcesToModify += this.renames.length + this.deletes.length + this.creates.length;

for (let edit of edits) {
this._addEdit(edit);
}
Expand All @@ -216,7 +235,7 @@ class BulkEditModel implements IDisposable {
array.push(edit);
}

public prepare(): TPromise<BulkEditModel> {
public async prepare(): TPromise<BulkEditModel> {

if (this._tasks) {
throw new Error('illegal state - already prepared');
Expand All @@ -229,6 +248,15 @@ class BulkEditModel implements IDisposable {
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 => {
const model = ref.object;
Expand Down Expand Up @@ -256,8 +284,9 @@ class BulkEditModel implements IDisposable {
promises.push(promise);
});

await TPromise.join(promises);

return TPromise.join(promises).then(_ => this);
return this;
}

public apply(): Selection {
Expand All @@ -284,20 +313,29 @@ class BulkEditModel implements IDisposable {
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<ISelection>;
ariaMessage(): string;
}

export function bulkEdit(textModelResolverService: ITextModelService, editor: ICodeEditor, edits: IResourceEdit[], fileService?: IFileService, progress: IProgressRunner = null): TPromise<any> {
export function bulkEdit(textModelResolverService: ITextModelService, editor: ICodeEditor, edits: IResourceEdit[], fileService: IFileService, resourceFileEdits?: IResourceFileEdit): TPromise<any> {
let bulk = createBulkEdit(textModelResolverService, editor, fileService);
bulk.add(edits);
bulk.progress(progress);
bulk.addRename(resourceFileEdits.renamedResources);
bulk.addCreate(resourceFileEdits.createdResources);
bulk.addDelete(resourceFileEdits.deletedResources);
bulk.progress(null);
return bulk.finish();
}

export function createBulkEdit(textModelResolverService: ITextModelService, editor?: ICodeEditor, fileService?: IFileService): BulkEdit {

let all: IResourceEdit[] = [];
const renames: IResourceRename[] = [];
const creates: IResourceCreate[] = [];
const deletes: URI[] = [];
let recording = new ChangeRecorder(fileService).start();
let progressRunner: IProgressRunner;

Expand All @@ -309,6 +347,18 @@ export function createBulkEdit(textModelResolverService: ITextModelService, edit
all.push(...edits);
}

function addRename(edits: IResourceRename[]): void {
renames.push(...edits);
}

function addCreate(edits: IResourceCreate[]): void {
creates.push(...edits);
}

function addDelete(edits: URI[]): void {
deletes.push(...edits);
}

function getConcurrentEdits() {
let names: string[];
for (let edit of all) {
Expand All @@ -327,7 +377,7 @@ export function createBulkEdit(textModelResolverService: ITextModelService, edit

function finish(): TPromise<ISelection> {

if (all.length === 0) {
if (all.length === 0 && renames.length === 0 && creates.length === 0 && deletes.length === 0) {
return TPromise.as(undefined);
}

Expand All @@ -344,9 +394,9 @@ export function createBulkEdit(textModelResolverService: ITextModelService, edit
selections = editor.getSelections();
}

const model = new BulkEditModel(textModelResolverService, uri, selections, all, progressRunner);
const model = new BulkEditModel(textModelResolverService, uri, selections, all, progressRunner, renames, creates, deletes, fileService);

return model.prepare().then(_ => {
return model.prepare().then(async _ => {

let concurrentEdits = getConcurrentEdits();
if (concurrentEdits) {
Expand All @@ -355,7 +405,7 @@ export function createBulkEdit(textModelResolverService: ITextModelService, edit

recording.stop();

const result = model.apply();
const result = await model.apply();
model.dispose();
return result;
});
Expand All @@ -376,6 +426,9 @@ export function createBulkEdit(textModelResolverService: ITextModelService, edit
return {
progress,
add,
addRename,
addCreate,
addDelete,
finish,
ariaMessage
};
Expand Down
14 changes: 14 additions & 0 deletions src/vs/editor/common/modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -821,8 +821,22 @@ export interface IResourceEdit {
range: IRange;
newText: string;
}

export interface IResourceRename {
readonly from: URI;
readonly to: URI;
}

export interface IResourceCreate {
readonly uri: URI;
readonly contents: string;
}

export interface WorkspaceEdit {
edits: IResourceEdit[];
renamedResources?: IResourceRename[];
createdResources?: IResourceCreate[];
deletedResources?: URI[];
rejectReason?: string;
}
export interface RenameProvider {
Expand Down
11 changes: 7 additions & 4 deletions src/vs/editor/contrib/quickFix/quickFixCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ 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 { createBulkEdit } 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 {

Expand Down Expand Up @@ -112,9 +113,11 @@ export class QuickFixController implements IEditorContribution {

private async _onApplyCodeAction(action: CodeAction): TPromise<void> {
if (action.edit) {
const edit = createBulkEdit(this._textModelService, this._editor, this._fileService);
edit.add(action.edit.edits);
await edit.finish();
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)
});
}

if (action.command) {
Expand Down
13 changes: 13 additions & 0 deletions src/vs/monaco.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4913,8 +4913,21 @@ declare module monaco.languages {
newText: string;
}

export interface IResourceRename {
readonly from: Uri;
readonly to: Uri;
}

export interface IResourceCreate {
readonly uri: Uri;
readonly contents: string;
}

export interface WorkspaceEdit {
edits: IResourceEdit[];
renamedResources?: IResourceRename[];
createdResources?: IResourceCreate[];
deletedResources?: Uri[];
rejectReason?: string;
}

Expand Down
38 changes: 38 additions & 0 deletions src/vs/vscode.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2430,6 +2430,29 @@ 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.
*
Expand Down Expand Up @@ -2485,6 +2508,21 @@ declare module 'vscode' {
* @return An array of `[Uri, TextEdit[]]`-tuples.
*/
entries(): [Uri, TextEdit[]][];

/**
* Get all resource rename edits.
*/
readonly renamedResources: { from: Uri, to: Uri }[];

/**
* Get all resource create edits.
*/
readonly createdResources: { uri: Uri, contents: string }[];

/**
* Get all resource delete edits.
*/
readonly deletedResources: Uri[];
}

/**
Expand Down
17 changes: 13 additions & 4 deletions src/vs/workbench/api/electron-browser/mainThreadEditors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ 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 } from 'vs/workbench/api/node/extHost.protocol';
import { ITextEditorConfigurationUpdate, TextEditorRevealType, IApplyEditsOptions, IUndoStopOptions, IResourceFileEdit } 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';
Expand Down Expand Up @@ -210,7 +210,7 @@ export class MainThreadEditors implements MainThreadEditorsShape {
return TPromise.as(this._documentsAndEditors.getEditor(id).applyEdits(modelVersionId, edits, opts));
}

$tryApplyWorkspaceEdit(workspaceResourceEdits: IWorkspaceResourceEdit[]): TPromise<boolean> {
$tryApplyWorkspaceEdit(workspaceResourceEdits: IWorkspaceResourceEdit[], resourceFileEdits?: IResourceFileEdit): TPromise<boolean> {

// First check if loaded models were not changed in the meantime
for (let i = 0, len = workspaceResourceEdits.length; i < len; i++) {
Expand Down Expand Up @@ -253,8 +253,17 @@ export class MainThreadEditors implements MainThreadEditorsShape {
}
}

return bulkEdit(this._textModelResolverService, codeEditor, resourceEdits, this._fileService)
.then(() => true);
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);
}

$tryInsertSnippet(id: string, template: string, ranges: IRange[], opts: IUndoStopOptions): TPromise<boolean> {
Expand Down
8 changes: 7 additions & 1 deletion src/vs/workbench/api/node/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,12 @@ export interface IWorkspaceResourceEdit {
}[];
}

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<string>;
$registerTextEditorDecorationType(key: string, options: editorCommon.IDecorationRenderOptions): void;
Expand All @@ -222,7 +228,7 @@ export interface MainThreadEditorsShape extends IDisposable {
$tryRevealRange(id: string, range: IRange, revealType: TextEditorRevealType): TPromise<void>;
$trySetSelections(id: string, selections: ISelection[]): TPromise<void>;
$tryApplyEdits(id: string, modelVersionId: number, edits: ISingleEditOperation[], opts: IApplyEditsOptions): TPromise<boolean>;
$tryApplyWorkspaceEdit(workspaceResourceEdits: IWorkspaceResourceEdit[]): TPromise<boolean>;
$tryApplyWorkspaceEdit(workspaceResourceEdits: IWorkspaceResourceEdit[], resourceFileEdits?: IResourceFileEdit): TPromise<boolean>;
$tryInsertSnippet(id: string, template: string, selections: IRange[], opts: IUndoStopOptions): TPromise<boolean>;
$getDiffInformation(id: string): TPromise<editorCommon.ILineChange[]>;
}
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/node/extHostTextEditors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ export class ExtHostEditors implements ExtHostEditorsShape {
workspaceResourceEdits.push(workspaceResourceEdit);
}

return this._proxy.$tryApplyWorkspaceEdit(workspaceResourceEdits);
return this._proxy.$tryApplyWorkspaceEdit(workspaceResourceEdits, edit);
}

// --- called from main thread
Expand Down
7 changes: 6 additions & 1 deletion src/vs/workbench/api/node/extHostTypeConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,12 @@ export const TextEdit = {

export namespace WorkspaceEdit {
export function from(value: vscode.WorkspaceEdit): modes.WorkspaceEdit {
const result: modes.WorkspaceEdit = { edits: [] };
const result: modes.WorkspaceEdit = {
edits: [],
renamedResources: value.renamedResources,
createdResources: value.createdResources,
deletedResources: value.deletedResources
};
for (let entry of value.entries()) {
let [uri, textEdits] = entry;
for (let textEdit of textEdits) {
Expand Down
Loading

0 comments on commit e932b8a

Please sign in to comment.