未验证 提交 57a072f8 编写于 作者: J Johannes Rieken 提交者: GitHub

Merge pull request #41552 from Microsoft/rename-workspaceedit-proto

Allow renaming/creating/deleting files in a workspaced edit
......@@ -10,6 +10,7 @@ import * as vscode from 'vscode';
import { createRandomFile, deleteFile, closeAllEditors, pathEquals } from './utils';
import { join, basename } from 'path';
import * as fs from 'fs';
import { Uri } from 'vscode';
suite('workspace-namespace', () => {
......@@ -505,4 +506,44 @@ suite('workspace-namespace', () => {
return vscode.workspace.applyEdit(edit);
});
});
test('applyEdit should fail when editing deleted resource', async () => {
const resource = await createRandomFile();
const 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
}
});
test('applyEdit should fail when renaming deleted resource', async () => {
const resource = await createRandomFile();
const edit = new vscode.WorkspaceEdit();
edit.deleteResource(resource);
try {
edit.renameResource(resource, resource);
assert.fail(false, 'Should disallow rename of deleted resource');
} catch {
// noop
}
});
test('applyEdit should fail when editing renamed from resource', async () => {
const resource = await createRandomFile();
const newResource = Uri.parse(resource.fsPath + '.1');
const edit = new vscode.WorkspaceEdit();
edit.renameResource(resource, newResource);
try {
edit.insert(resource, new vscode.Position(0, 0), '');
assert.fail(false, 'Should disallow editing renamed file');
} catch {
// noop
}
});
});
......@@ -5,68 +5,48 @@
'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, FileChangeType } 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 { optional } from 'vs/platform/instantiation/common/instantiation';
import { ResourceTextEdit, ResourceFileEdit, isResourceFileEdit, isResourceTextEdit } from 'vs/editor/common/modes';
import { getPathLabel } from 'vs/base/common/labels';
export interface IResourceEdit {
resource: URI;
range?: IRange;
newText: string;
newEol?: EndOfLineSequence;
}
interface IRecording {
stop(): void;
hasChanged(resource: URI): boolean;
allChanges(): IFileChange[];
}
class ChangeRecorder {
private _fileService: IFileService;
constructor(fileService?: IFileService) {
this._fileService = fileService;
}
abstract class IRecording {
public start(): IRecording {
const changes: IStringDictionary<IFileChange[]> = Object.create(null);
static start(fileService: IFileService): IRecording {
const _changes = new Set<string>();
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 = [];
if (fileService) {
// watch only when there is a fileservice available
stop = fileService.onFileChanges(event => {
for (const change of event.changes) {
if (change.type === FileChangeType.UPDATED) {
_changes.add(change.resource.toString());
}
array.push(change);
});
}
});
}
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 {
......@@ -84,26 +64,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) => {
......@@ -160,16 +148,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 {
......@@ -189,34 +171,42 @@ class SourceModelEditTask extends EditTask {
class BulkEditModel implements IDisposable {
private _textModelResolverService: ITextModelService;
private _numberOfResourcesToModify: number = 0;
private _edits: IStringDictionary<IResourceEdit[]> = Object.create(null);
private _edits = new Map<string, ResourceTextEdit[]>();
private _tasks: EditTask[];
private _sourceModel: URI;
private _sourceSelections: Selection[];
private _sourceModelTask: SourceModelEditTask;
constructor(textModelResolverService: ITextModelService, sourceModel: URI, sourceSelections: Selection[], edits: IResourceEdit[], private progress: IProgressRunner = null) {
private _progress: IProgress<void>;
constructor(
textModelResolverService: ITextModelService,
editor: ICodeEditor,
edits: ResourceTextEdit[],
progress: IProgress<void>
) {
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;
for (let edit of edits) {
this._addEdit(edit);
}
edits.forEach(this.addEdit, this);
}
private _addEdit(edit: IResourceEdit): void {
let array = this._edits[edit.resource.toString()];
dispose(): void {
this._tasks = dispose(this._tasks);
}
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 prepare(): TPromise<BulkEditModel> {
async prepare(): TPromise<BulkEditModel> {
if (this._tasks) {
throw new Error('illegal state - already prepared');
......@@ -225,158 +215,167 @@ class BulkEditModel implements IDisposable {
this._tasks = [];
const promises: TPromise<any>[] = [];
if (this.progress) {
this.progress.total(this._numberOfResourcesToModify * 2);
}
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);
});
await TPromise.join(promises);
return TPromise.join(promises).then(_ => this);
return this;
}
public apply(): Selection {
this._tasks.forEach(task => this.applyTask(task));
let r: Selection = null;
if (this._sourceModelTask) {
r = this._sourceModelTask.getEndCursorSelection();
apply(): Selection {
for (const task of this._tasks) {
task.apply();
this._progress.report(undefined);
}
return r;
}
private applyTask(task: EditTask): void {
task.apply();
if (this.progress) {
this.progress.worked(1);
}
}
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;
finish(): TPromise<ISelection>;
ariaMessage(): string;
}
export function bulkEdit(textModelResolverService: ITextModelService, editor: ICodeEditor, edits: IResourceEdit[], fileService?: IFileService, progress: IProgressRunner = null): TPromise<any> {
let bulk = createBulkEdit(textModelResolverService, editor, fileService);
bulk.add(edits);
bulk.progress(progress);
return bulk.finish();
}
export type Edit = ResourceFileEdit | ResourceTextEdit;
export function createBulkEdit(textModelResolverService: ITextModelService, editor?: ICodeEditor, fileService?: IFileService): BulkEdit {
export class BulkEdit {
let all: IResourceEdit[] = [];
let recording = new ChangeRecorder(fileService).start();
let progressRunner: IProgressRunner;
function progress(progress: IProgressRunner) {
progressRunner = progress;
static perform(edits: Edit[], textModelService: ITextModelService, fileService: IFileService, editor: ICodeEditor): TPromise<any> {
const edit = new BulkEdit(editor, null, textModelService, fileService);
edit.add(edits);
return edit.perform();
}
function add(edits: IResourceEdit[]): void {
all.push(...edits);
private _edits: Edit[] = [];
private _editor: ICodeEditor;
private _progress: IProgressRunner;
constructor(
editor: ICodeEditor,
progress: IProgressRunner,
@ITextModelService private _textModelService: ITextModelService,
@optional(IFileService) private _fileService: IFileService
) {
this._editor = editor;
this._progress = progress || emptyProgressRunner;
}
function getConcurrentEdits() {
let names: string[];
for (let edit of all) {
if (recording.hasChanged(edit.resource)) {
if (!names) {
names = [];
}
names.push(edit.resource.fsPath);
}
add(edits: Edit[] | Edit): void {
if (Array.isArray(edits)) {
this._edits.push(...edits);
} else {
this._edits.push(edits);
}
if (names) {
return nls.localize('conflict', "These files have changed in the meantime: {0}", names.join(', '));
}
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);
}
return undefined;
}
function finish(): TPromise<ISelection> {
async perform(): TPromise<Selection> {
let seen = new Set<string>();
let total = 0;
const groups: Edit[][] = [];
let group: Edit[];
for (const edit of this._edits) {
if (!group
|| (isResourceFileEdit(group[0]) && !isResourceFileEdit(edit))
|| (isResourceTextEdit(group[0]) && !isResourceTextEdit(edit))
) {
group = [];
groups.push(group);
}
group.push(edit);
if (all.length === 0) {
return TPromise.as(undefined);
if (isResourceFileEdit(edit)) {
total += 1;
} else if (!seen.has(edit.resource.toString())) {
seen.add(edit.resource.toString());
total += 2;
}
}
let concurrentEdits = getConcurrentEdits();
if (concurrentEdits) {
return TPromise.wrapError<ISelection>(new Error(concurrentEdits));
// define total work and progress callback
// for child operations
this._progress.total(total);
let progress: IProgress<void> = { 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(<ResourceFileEdit[]>group, progress);
} else {
res = await this._performTextEdits(<ResourceTextEdit[]>group, progress) || res;
}
}
return res;
}
private async _performFileEdits(edits: ResourceFileEdit[], progress: IProgress<void>) {
for (const edit of edits) {
let uri: URI;
let selections: Selection[];
progress.report(undefined);
if (editor && editor.getModel()) {
uri = editor.getModel().uri;
selections = editor.getSelections();
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 });
}
}
}
const model = new BulkEditModel(textModelResolverService, uri, selections, all, progressRunner);
private async _performTextEdits(edits: ResourceTextEdit[], progress: IProgress<void>): TPromise<Selection> {
return model.prepare().then(_ => {
const recording = IRecording.start(this._fileService);
const model = new BulkEditModel(this._textModelService, this._editor, edits, progress);
let concurrentEdits = getConcurrentEdits();
if (concurrentEdits) {
throw new Error(concurrentEdits);
}
await model.prepare();
recording.stop();
const conflicts = edits
.filter(edit => recording.hasChanged(edit.resource))
.map(edit => getPathLabel(edit.resource));
const result = model.apply();
model.dispose();
return result;
});
}
recording.stop();
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);
if (conflicts.length > 0) {
model.dispose();
throw new Error(nls.localize('conflict', "These files have changed in the meantime: {0}", conflicts.join(', ')));
}
}
return {
progress,
add,
finish,
ariaMessage
};
const selection = await model.apply();
model.dispose();
return selection;
}
}
......@@ -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
......@@ -825,15 +826,36 @@ export interface DocumentColorProvider {
provideColorPresentations(model: model.ITextModel, colorInfo: IColorInformation, token: CancellationToken): IColorPresentation[] | Thenable<IColorPresentation[]>;
}
export interface IResourceEdit {
/**
* @internal
*/
export function isResourceFileEdit(thing: any): thing is ResourceFileEdit {
return isObject(thing) && (Boolean((<ResourceFileEdit>thing).newUri) || Boolean((<ResourceFileEdit>thing).oldUri));
}
/**
* @internal
*/
export function isResourceTextEdit(thing: any): thing is ResourceTextEdit {
return isObject(thing) && (<ResourceTextEdit>thing).resource && Array.isArray((<ResourceTextEdit>thing).edits);
}
export interface ResourceFileEdit {
oldUri: URI;
newUri: URI;
}
export interface ResourceTextEdit {
resource: URI;
range: IRange;
newText: string;
modelVersionId?: number;
edits: TextEdit[];
}
export interface WorkspaceEdit {
edits: IResourceEdit[];
rejectReason?: string;
edits: Array<ResourceTextEdit | ResourceFileEdit>;
rejectReason?: string; // TODO@joh, move to rename
}
export interface RenameProvider {
provideRenameEdits(model: model.ITextModel, position: Position, newName: string, token: CancellationToken): WorkspaceEdit | Thenable<WorkspaceEdit>;
}
......
......@@ -23,7 +23,7 @@ import { QuickFixModel, QuickFixComputeEvent } from './quickFixModel';
import { CodeActionKind, CodeActionAutoApply } from './codeActionTrigger';
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';
......@@ -129,9 +129,7 @@ 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.perform(action.edit.edits, this._textModelService, this._fileService, this._editor);
}
if (action.command) {
......
......@@ -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, CodeAction, CodeActionContext } from 'vs/editor/common/modes';
import { CodeActionProviderRegistry, LanguageIdentifier, CodeActionProvider, Command, WorkspaceEdit, ResourceTextEdit, CodeAction, CodeActionContext } 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';
......@@ -58,7 +58,7 @@ suite('QuickFix', () => {
bcd: {
diagnostics: [],
edit: new class implements WorkspaceEdit {
edits: IResourceEdit[];
edits: ResourceTextEdit[];
},
title: 'abc'
}
......
......@@ -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, <ICodeEditor>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);
}
......
......@@ -4913,14 +4913,19 @@ declare module monaco.languages {
provideColorPresentations(model: editor.ITextModel, colorInfo: IColorInformation, token: CancellationToken): IColorPresentation[] | Thenable<IColorPresentation[]>;
}
export interface IResourceEdit {
export interface ResourceFileEdit {
oldUri: Uri;
newUri: Uri;
}
export interface ResourceTextEdit {
resource: Uri;
range: IRange;
newText: string;
modelVersionId?: number;
edits: TextEdit[];
}
export interface WorkspaceEdit {
edits: IResourceEdit[];
edits: Array<ResourceTextEdit | ResourceFileEdit>;
rejectReason?: string;
}
......
......@@ -31,6 +31,12 @@ export interface IProgressRunner {
done(): void;
}
export const emptyProgressRunner: IProgressRunner = Object.freeze({
total() { },
worked() { },
done() { }
});
export interface IProgress<T> {
report(item: T): void;
}
......
......@@ -2559,6 +2559,42 @@ declare module 'vscode' {
* @return An array of `[Uri, TextEdit[]]`-tuples.
*/
entries(): [Uri, TextEdit[]][];
/**
* 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.
*/
deleteResource(uri: Uri): void;
/**
* 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]`;
*/
resourceEdits(): [Uri, Uri][];
/**
*
*/
allEntries(): ([Uri, TextEdit[]] | [Uri, Uri])[];
}
/**
......
......@@ -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 } 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[]): TPromise<boolean> {
$tryApplyWorkspaceEdit(dto: WorkspaceEditDto): TPromise<boolean> {
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,8 +236,7 @@ export class MainThreadEditors implements MainThreadEditorsShape {
}
}
return bulkEdit(this._textModelResolverService, codeEditor, resourceEdits, this._fileService)
.then(() => true);
return BulkEdit.perform(edits, this._textModelResolverService, this._fileService, codeEditor).then(() => true);
}
$tryInsertSnippet(id: string, template: string, ranges: IRange[], opts: IUndoStopOptions): TPromise<boolean> {
......
......@@ -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 <modes.IResourceEdit>data;
}
private static _reviveWorkspaceEditDto(data: WorkspaceEditDto): modes.WorkspaceEdit {
if (data && data.edits) {
data.edits.forEach(MainThreadLanguageFeatures._reviveResourceEditDto);
}
return <modes.WorkspaceEdit>data;
}
private static _reviveCodeActionDto(data: CodeActionDto[]): modes.CodeAction[] {
if (data) {
data.forEach(code => MainThreadLanguageFeatures._reviveWorkspaceEditDto(code.edit));
data.forEach(code => reviveWorkspaceEditDto(code.edit));
}
return <modes.CodeAction[]>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), <modes.RenameProvider>{
provideRenameEdits: (model: ITextModel, position: EditorPosition, newName: string, token: CancellationToken): Thenable<modes.WorkspaceEdit> => {
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);
}
});
}
......
......@@ -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, FileChangeType } 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';
import { ILineMatch, IPatternInfo } from 'vs/platform/search/common/search';
import { LogLevel } from 'vs/platform/log/common/log';
......@@ -194,8 +194,6 @@ export interface IApplyEditsOptions extends IUndoStopOptions {
setEndOfLine: EndOfLine;
}
export interface ITextDocumentShowOptions {
position?: EditorPosition;
preserveFocus?: boolean;
......@@ -203,16 +201,6 @@ export interface ITextDocumentShowOptions {
selection?: IRange;
}
export interface IWorkspaceResourceEdit {
resource: UriComponents;
modelVersionId?: number;
edits: {
range?: IRange;
newText: string;
newEol?: EndOfLineSequence;
}[];
}
export interface MainThreadEditorsShape extends IDisposable {
$tryShowTextDocument(resource: UriComponents, options: ITextDocumentShowOptions): TPromise<string>;
$registerTextEditorDecorationType(key: string, options: editorCommon.IDecorationRenderOptions): void;
......@@ -225,7 +213,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(workspaceEditDto: WorkspaceEditDto): TPromise<boolean>;
$tryInsertSnippet(id: string, template: string, selections: IRange[], opts: IUndoStopOptions): TPromise<boolean>;
$getDiffInformation(id: string): TPromise<editorCommon.ILineChange[]>;
}
......@@ -625,23 +613,44 @@ 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 (<ResourceTextEditDto>edit).resource === 'object') {
(<ResourceTextEditDto>edit).resource = URI.revive((<ResourceTextEditDto>edit).resource);
} else {
(<ResourceFileEditDto>edit).newUri = URI.revive((<ResourceFileEditDto>edit).newUri);
(<ResourceFileEditDto>edit).oldUri = URI.revive((<ResourceFileEditDto>edit).oldUri);
}
}
}
return <modes.WorkspaceEdit>data;
}
export interface CodeActionDto {
title: string;
edit?: WorkspaceEditDto;
diagnostics?: IMarkerData[];
command?: modes.Command;
scope?: string;
kind?: string;
}
export interface ExtHostLanguageFeaturesShape {
......
......@@ -353,11 +353,7 @@ export class ExtHostApiCommands {
if (value.rejectReason) {
return TPromise.wrapError<types.WorkspaceEdit>(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);
});
}
......
......@@ -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) && (<vscode.TextEdit[]>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?
......
......@@ -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,8 @@ class CodeActionAdapter {
this._provider = provider;
}
provideCodeActions(resource: URI, range: IRange, context: modes.CodeActionContext): TPromise<modes.CodeAction[]> {
provideCodeActions(resource: URI, range: IRange, context: modes.CodeActionContext): TPromise<CodeActionDto[]> {
const doc = this._documents.getDocumentData(resource).document;
const ran = <vscode.Range>TypeConverters.toRange(range);
......@@ -948,7 +949,8 @@ export class ExtHostLanguageFeatures implements ExtHostLanguageFeaturesShape {
return this._createDisposable(handle);
}
$provideCodeActions(handle: number, resource: UriComponents, range: IRange, context: modes.CodeActionContext): TPromise<modes.CodeAction[]> {
$provideCodeActions(handle: number, resource: UriComponents, range: IRange, context: modes.CodeActionContext): TPromise<CodeActionDto[]> {
return this._withAdapter(handle, CodeActionAdapter, adapter => adapter.provideCodeActions(URI.revive(resource), range, context));
}
......
......@@ -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,36 +92,23 @@ export class ExtHostEditors implements ExtHostEditorsShape {
applyWorkspaceEdit(edit: vscode.WorkspaceEdit): TPromise<boolean> {
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);
return this._proxy.$tryApplyWorkspaceEdit(dto);
}
// --- called from main thread
......
......@@ -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;
......@@ -228,24 +229,36 @@ export const TextEdit = {
export namespace WorkspaceEdit {
export function from(value: vscode.WorkspaceEdit): modes.WorkspaceEdit {
const result: modes.WorkspaceEdit = { 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)
});
const result: modes.WorkspaceEdit = {
edits: []
};
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((<ResourceTextEditDto>edit).edits)) {
result.set(
URI.revive((<ResourceTextEditDto>edit).resource),
<types.TextEdit[]>(<ResourceTextEditDto>edit).edits.map(TextEdit.to)
);
} else {
result.renameResource(
URI.revive((<ResourceFileEditDto>edit).oldUri),
URI.revive((<ResourceFileEditDto>edit).newUri)
);
}
}
return result;
}
......
......@@ -492,10 +492,28 @@ export class TextEdit {
}
}
export class WorkspaceEdit {
export class WorkspaceEdit implements vscode.WorkspaceEdit {
private _values: [URI, TextEdit[]][] = [];
private _index = new Map<string, number>();
private _seqPool: number = 0;
private _resourceEdits: { seq: number, from: URI, to: URI }[] = [];
private _textEdits = new Map<string, { seq: number, uri: URI, edits: TextEdit[] }>();
createResource(uri: vscode.Uri): void {
this.renameResource(undefined, uri);
}
deleteResource(uri: vscode.Uri): void {
this.renameResource(uri, undefined);
}
renameResource(from: vscode.Uri, to: vscode.Uri): void {
this._resourceEdits.push({ seq: this._seqPool++, 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);
......@@ -503,8 +521,9 @@ export class WorkspaceEdit {
if (array) {
array.push(edit);
} else {
this.set(uri, [edit]);
array = [edit];
}
this.set(uri, array);
}
insert(resource: URI, position: Position, newText: string): void {
......@@ -516,34 +535,58 @@ export class WorkspaceEdit {
}
has(uri: URI): boolean {
return this._index.has(uri.toString());
return this._textEdits.has(uri.toString());
}
set(uri: URI, edits: TextEdit[]): void {
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);
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;
} else {
this._values[idx][1] = edits;
data.edits = edits.slice(0);
}
}
get(uri: URI): TextEdit[] {
let idx = this._index.get(uri.toString());
return typeof idx !== 'undefined' && this._values[idx][1];
if (!this._textEdits.has(uri.toString())) {
return undefined;
}
const { edits } = this._textEdits.get(uri.toString());
return edits ? edits.slice() : undefined;
}
entries(): [URI, TextEdit[]][] {
return this._values;
const res: [URI, TextEdit[]][] = [];
this._textEdits.forEach(value => res.push([value.uri, value.edits]));
return res.slice();
}
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];
});
return res;
}
get size(): number {
return this._values.length;
return this._textEdits.size + this._resourceEdits.length;
}
toJSON(): any {
return this._values;
return this.entries();
}
}
......
......@@ -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';
import { createTextBufferFactoryFromSnapshot } from 'vs/editor/common/model/textModel';
const REPLACE_PREVIEW = 'replacePreview';
......@@ -104,8 +105,7 @@ export class ReplaceService implements IReplaceService {
public replace(match: FileMatchOrMatch, progress?: IProgressRunner, resource?: URI): TPromise<any>;
public replace(arg: any, progress: IProgressRunner = null, resource: URI = null): TPromise<any> {
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 = <Match>arg;
......@@ -127,7 +127,7 @@ export class ReplaceService implements IReplaceService {
});
}
return bulkEdit.finish();
return bulkEdit.perform();
}
public openReplacePreview(element: FileMatchOrMatch, preserveFocus?: boolean, sideBySide?: boolean, pinned?: boolean): TPromise<any> {
......@@ -175,12 +175,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;
}
......
......@@ -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<MainThreadEditorsShape>() {
$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((<ResourceTextEdit>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<MainThreadEditorsShape>() {
$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<MainThreadEditorsShape>() {
$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
......
......@@ -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';
......@@ -840,7 +840,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((<ResourceTextEdit[]>value.edits)[0].edits.length, 2); // least relevant renamer
});
});
});
......
......@@ -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<MainThreadEditorsShape>() {
$tryApplyWorkspaceEdit(_workspaceResourceEdits: IWorkspaceResourceEdit[]): TPromise<boolean> {
$tryApplyWorkspaceEdit(_workspaceResourceEdits: WorkspaceEditDto): TPromise<boolean> {
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((<ResourceTextEdit>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 (<ResourceTextEdit>workspaceResourceEdits.edits[0]).modelVersionId === 'undefined');
});
});
......
......@@ -362,6 +362,53 @@ suite('ExtHostTypes', function () {
});
// 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.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 () {
assert.throws(() => new types.DocumentLink(null, null));
assert.throws(() => new types.DocumentLink(new types.Range(1, 1, 1, 1), null));
......
......@@ -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';
......@@ -23,6 +23,10 @@ import { Range } from 'vs/editor/common/core/range';
import { Position } from 'vs/editor/common/core/position';
import { IModelService } from 'vs/editor/common/services/modelService';
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', () => {
......@@ -31,10 +35,34 @@ suite('MainThreadEditors', () => {
let modelService: IModelService;
let editors: MainThreadEditors;
const movedResources = new Map<URI, URI>();
const createdResources = new Set<URI>();
const deletedResources = new Set<URI>();
setup(() => {
const configService = new TestConfigurationService();
modelService = new ModelServiceImpl(null, configService);
const codeEditorService = new TestCodeEditorService();
movedResources.clear();
createdResources.clear();
deletedResources.clear();
const fileService = new class extends TestFileService {
async moveFile(from, target): TPromise<IFileStat> {
movedResources.set(from, target);
return createMockFileStat(target);
}
async createFile(uri): TPromise<IFileStat> {
createdResources.add(uri);
return createMockFileStat(uri);
}
async del(uri): TPromise<any> {
deletedResources.add(uri);
}
};
const textFileService = new class extends mock<ITextFileService>() {
isDirty() { return false; }
models = <any>{
......@@ -69,7 +97,7 @@ suite('MainThreadEditors', () => {
workbenchEditorService,
codeEditorService,
null,
null,
fileService,
null,
null,
editorGroupService,
......@@ -82,7 +110,7 @@ suite('MainThreadEditors', () => {
workbenchEditorService,
editorGroupService,
null,
null,
fileService,
modelService
);
});
......@@ -91,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)
}]
};
......@@ -103,8 +131,35 @@ 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`, () => {
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.has(resource), true);
assert.equal(deletedResources.has(resource), true);
});
});
});
function createMockFileStat(target: URI): IFileStat {
return {
etag: '',
isDirectory: false,
name: target.path,
mtime: 0,
resource: target
};
}
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册