Skip to content

Commit

Permalink
bulk edit provider listens on operation events only, add more test (p…
Browse files Browse the repository at this point in the history
…artity disabled due to #52212), #10659
  • Loading branch information
jrieken committed Jun 19, 2018
1 parent 48d38a1 commit a979c73
Show file tree
Hide file tree
Showing 7 changed files with 177 additions and 74 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@

import * as assert from 'assert';
import * as vscode from 'vscode';
import { createRandomFile, deleteFile, closeAllEditors, pathEquals } from '../utils';
import { join, basename } from 'path';
import { createRandomFile, deleteFile, closeAllEditors, pathEquals, rndName } from '../utils';
import { join, basename, dirname } from 'path';
import * as fs from 'fs';
import * as os from 'os';

suite('workspace-namespace', () => {

Expand Down Expand Up @@ -519,7 +520,6 @@ suite('workspace-namespace', () => {
});
});


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

Expand Down Expand Up @@ -564,7 +564,98 @@ suite('workspace-namespace', () => {
let success = await vscode.workspace.applyEdit(edit);
assert.equal(success, true);

// todo@ben https://github.com/Microsoft/vscode/issues/52212
// let doc = await vscode.workspace.openTextDocument(newUri);
// assert.equal(doc.getText(), 'AFTERBEFORE');
});

function nameWithUnderscore(uri: vscode.Uri) {
return uri.with({ path: join(dirname(uri.path), `_${basename(uri.path)}`) });
}

test('WorkspaceEdit: applying edits before and after rename duplicates resource #42633', async function () {
let docUri = await createRandomFile();
let newUri = nameWithUnderscore(docUri);

let we = new vscode.WorkspaceEdit();
we.insert(docUri, new vscode.Position(0, 0), 'Hello');
we.insert(docUri, new vscode.Position(0, 0), 'Foo');
we.renameFile(docUri, newUri);
we.insert(newUri, new vscode.Position(0, 0), 'Bar');

assert.ok(await vscode.workspace.applyEdit(we));
// todo@ben https://github.com/Microsoft/vscode/issues/52212
// let doc = await vscode.workspace.openTextDocument(newUri);
// assert.equal(doc.getText(), 'BarFooHello');
});

test('WorkspaceEdit: Problem recreating a renamed resource #42634', async function () {
let docUri = await createRandomFile();
let newUri = nameWithUnderscore(docUri);

let we = new vscode.WorkspaceEdit();
we.insert(docUri, new vscode.Position(0, 0), 'Hello');
we.insert(docUri, new vscode.Position(0, 0), 'Foo');
we.renameFile(docUri, newUri);

we.createFile(docUri);
we.insert(docUri, new vscode.Position(0, 0), 'Bar');

assert.ok(await vscode.workspace.applyEdit(we));

// todo@ben https://github.com/Microsoft/vscode/issues/52212
// let newDoc = await vscode.workspace.openTextDocument(newUri);
// assert.equal(newDoc.getText(), 'FooHello');

// todo@ben https://github.com/Microsoft/vscode/issues/52212
// let doc = await vscode.workspace.openTextDocument(docUri);
// assert.equal(doc.getText(), 'Bar');
});

test('WorkspaceEdit api - after saving a deleted file, it still shows up as deleted. #42667', async function () {
let docUri = await createRandomFile();
let we = new vscode.WorkspaceEdit();
we.deleteFile(docUri);
we.insert(docUri, new vscode.Position(0, 0), 'InsertText');

assert.ok(!(await vscode.workspace.applyEdit(we)));
try {
await vscode.workspace.openTextDocument(docUri);
assert.ok(false);
} catch (e) {
assert.ok(true);
}
});

test('WorkspaceEdit: edit and rename parent folder duplicates resource #42641', async function () {

let dir = join(os.tmpdir(), 'before-' + rndName());
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir);
}

let docUri = await createRandomFile('', dir);
let docParent = docUri.with({ path: dirname(docUri.path) });
let newParent = nameWithUnderscore(docParent);

let we = new vscode.WorkspaceEdit();
we.insert(docUri, new vscode.Position(0, 0), 'Hello');
we.renameFile(docParent, newParent);

assert.ok(await vscode.workspace.applyEdit(we));

try {
await vscode.workspace.openTextDocument(docUri);
assert.ok(false);
} catch (e) {
assert.ok(true);
}

let newUri = newParent.with({ path: join(newParent.path, basename(docUri.path)) });
let doc = await vscode.workspace.openTextDocument(newUri);
assert.ok(doc);

// todo@ben https://github.com/Microsoft/vscode/issues/52212
// assert.equal(doc.getText(), 'Hello');
});
});
6 changes: 3 additions & 3 deletions extensions/vscode-api-tests/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import * as fs from 'fs';
import * as os from 'os';
import { join } from 'path';

function rndName() {
export function rndName() {
return Math.random().toString(36).replace(/[^a-z]+/g, '').substr(0, 10);
}

export function createRandomFile(contents = ''): Thenable<vscode.Uri> {
export function createRandomFile(contents = '', dir: string = os.tmpdir()): Thenable<vscode.Uri> {
return new Promise((resolve, reject) => {
const tmpFile = join(os.tmpdir(), rndName());
const tmpFile = join(dir, rndName());
fs.writeFile(tmpFile, contents, (error) => {
if (error) {
return reject(error);
Expand Down
100 changes: 56 additions & 44 deletions src/vs/workbench/api/node/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { isMarkdownString } from 'vs/base/common/htmlContent';
import { IRelativePattern } from 'vs/base/common/glob';
import { relative } from 'path';
import { startsWith } from 'vs/base/common/strings';
import { values } from 'vs/base/common/map';
import { coalesce } from 'vs/base/common/arrays';

export class Disposable {

Expand Down Expand Up @@ -492,12 +494,11 @@ export class TextEdit {
}
}

export class WorkspaceEdit implements vscode.WorkspaceEdit {

private _seqPool: number = 0;

private _resourceEdits: { seq: number, from: URI, to: URI }[] = [];
private _textEdits = new Map<string, { seq: number, uri: URI, edits: TextEdit[] }>();
export class WorkspaceEdit implements vscode.WorkspaceEdit {

private _edits = new Array<{ _type: 1, from: URI, to: URI } | { _type: 2, uri: URI, edit: TextEdit }>();

createFile(uri: vscode.Uri): void {
this.renameFile(undefined, uri);
Expand All @@ -508,22 +509,11 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit {
}

renameFile(from: vscode.Uri, to: vscode.Uri): void {
this._resourceEdits.push({ seq: this._seqPool++, from, to });
this._edits.push({ _type: 1, from, to });
}

// resourceEdits(): [vscode.Uri, vscode.Uri][] {
// return this._resourceEdits.map(({ from, to }) => (<[vscode.Uri, vscode.Uri]>[from, to]));
// }

replace(uri: URI, range: Range, newText: string): void {
let edit = new TextEdit(range, newText);
let array = this.get(uri);
if (array) {
array.push(edit);
} else {
array = [edit];
}
this.set(uri, array);
this._edits.push({ _type: 2, uri, edit: new TextEdit(range, newText) });
}

insert(resource: URI, position: Position, newText: string): void {
Expand All @@ -535,54 +525,76 @@ export class WorkspaceEdit implements vscode.WorkspaceEdit {
}

has(uri: URI): boolean {
return this._textEdits.has(uri.toString());
for (const edit of this._edits) {
if (edit._type === 2 && edit.uri.toString() === uri.toString()) {
return true;
}
}
return false;
}

set(uri: URI, edits: TextEdit[]): void {
let data = this._textEdits.get(uri.toString());
if (!data) {
data = { seq: this._seqPool++, uri, edits: [] };
this._textEdits.set(uri.toString(), data);
}
if (!edits) {
data.edits = undefined;
// remove all text edits for `uri`
for (let i = 0; i < this._edits.length; i++) {
const element = this._edits[i];
if (element._type === 2 && element.uri.toString() === uri.toString()) {
this._edits[i] = undefined;
}
}
this._edits = coalesce(this._edits);
} else {
data.edits = edits.slice(0);
// append edit to the end
for (const edit of edits) {
if (edit) {
this._edits.push({ _type: 2, uri, edit });
}
}
}
}

get(uri: URI): TextEdit[] {
if (!this._textEdits.has(uri.toString())) {
let res: TextEdit[] = [];
for (let candidate of this._edits) {
if (candidate._type === 2 && candidate.uri.toString() === uri.toString()) {
res.push(candidate.edit);
}
}
if (res.length === 0) {
return undefined;
}
const { edits } = this._textEdits.get(uri.toString());
return edits ? edits.slice() : undefined;
return res;
}

entries(): [URI, TextEdit[]][] {
const res: [URI, TextEdit[]][] = [];
this._textEdits.forEach(value => res.push([value.uri, value.edits]));
return res.slice();
let textEdits = new Map<string, [URI, TextEdit[]]>();
for (let candidate of this._edits) {
if (candidate._type === 2) {
let textEdit = textEdits.get(candidate.uri.toString());
if (!textEdit) {
textEdit = [candidate.uri, []];
textEdits.set(candidate.uri.toString(), textEdit);
}
textEdit[1].push(candidate.edit);
}
}
return values(textEdits);
}

allEntries(): ([URI, TextEdit[]] | [URI, URI])[] {
// use the 'seq' the we have assigned when inserting
// the operation and use that order in the resulting
// array
const res: ([URI, TextEdit[]] | [URI, URI])[] = [];
this._textEdits.forEach(value => {
const { seq, uri, edits } = value;
res[seq] = [uri, edits];
});
this._resourceEdits.forEach(value => {
const { seq, from, to } = value;
res[seq] = [from, to];
});
let res: ([URI, TextEdit[]] | [URI, URI])[] = [];
for (let edit of this._edits) {
if (edit._type === 1) {
res.push([edit.from, edit.to]);
} else {
res.push([edit.uri, [edit.edit]]);
}
}
return res;
}

get size(): number {
return this._textEdits.size + this._resourceEdits.length;
return this.entries().length;
}

toJSON(): any {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@


import { getPathLabel } from 'vs/base/common/labels';
import { IDisposable, IReference, dispose } from 'vs/base/common/lifecycle';
import { dispose, IDisposable, IReference } from 'vs/base/common/lifecycle';
import URI from 'vs/base/common/uri';
import { TPromise } from 'vs/base/common/winjs.base';
import { ICodeEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser';
Expand All @@ -15,36 +15,30 @@ 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 { EndOfLineSequence, IIdentifiedSingleEditOperation, ITextModel } from 'vs/editor/common/model';
import { ResourceFileEdit, ResourceTextEdit, WorkspaceEdit, isResourceFileEdit, isResourceTextEdit } from 'vs/editor/common/modes';
import { isResourceFileEdit, isResourceTextEdit, ResourceFileEdit, ResourceTextEdit, WorkspaceEdit } 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 { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { IFileService } from 'vs/platform/files/common/files';
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { emptyProgressRunner, IProgress, IProgressRunner } from 'vs/platform/progress/common/progress';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IEditorService } from 'vs/workbench/services/editor/common/editorService';
import { ITextFileService } from 'vs/workbench/services/textfile/common/textfiles';
import { ILogService } from 'vs/platform/log/common/log';

abstract class Recording {

static start(fileService: IFileService): Recording {

let _changes = new Set<string>();
let stop: IDisposable;

stop = fileService.onFileChanges(event => {
for (const change of event.changes) {
if (change.type === FileChangeType.UPDATED) {
_changes.add(change.resource.toString());
}
}
let subscription = fileService.onAfterOperation(e => {
_changes.add(e.resource.toString());
});

return {
stop() { return dispose(stop); },
stop() { return subscription.dispose(); },
hasChanged(resource) { return _changes.has(resource.toString()); }
};
}
Expand Down Expand Up @@ -269,6 +263,7 @@ export class BulkEdit {
constructor(
editor: ICodeEditor,
progress: IProgressRunner,
@ILogService private readonly _logService: ILogService,
@ITextModelService private readonly _textModelService: ITextModelService,
@IFileService private readonly _fileService: IFileService,
@ITextFileService private readonly _textFileService: ITextFileService,
Expand Down Expand Up @@ -343,8 +338,8 @@ export class BulkEdit {
}

private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress<void>) {
this._logService.debug('_performFileEdits', JSON.stringify(edits));
for (const edit of edits) {

progress.report(undefined);

if (edit.newUri && edit.oldUri) {
Expand All @@ -358,6 +353,7 @@ export class BulkEdit {
}

private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress<void>): TPromise<Selection> {
this._logService.debug('_performTextEdits', JSON.stringify(edits));

const recording = Recording.start(this._fileService);
const model = new BulkEditModel(this._textModelService, this._editor, edits, progress);
Expand Down Expand Up @@ -424,14 +420,16 @@ export class BulkEditService implements IBulkEditService {
}
}

const bulkEdit = new BulkEdit(options.editor, options.progress, this._textModelService, this._fileService, this._textFileService, this._environmentService, this._contextService);
const bulkEdit = new BulkEdit(options.editor, options.progress, this._logService, this._textModelService, this._fileService, this._textFileService, this._environmentService, this._contextService);
bulkEdit.add(edits);

return bulkEdit.perform().then(selection => {
return { selection, ariaSummary: bulkEdit.ariaMessage() };
}, err => {
// console.log('apply FAILED');
// console.log(err);
this._logService.error(err);
return TPromise.wrapError(err);
throw err;
});
}
}
Expand Down

0 comments on commit a979c73

Please sign in to comment.