diff --git a/src/vs/editor/contrib/rename/browser/rename2.ts b/src/vs/editor/contrib/rename/browser/rename2.ts index 95be4ac6cb4..e6a3abc2e6e 100644 --- a/src/vs/editor/contrib/rename/browser/rename2.ts +++ b/src/vs/editor/contrib/rename/browser/rename2.ts @@ -25,7 +25,7 @@ import {IKeybindingService, IKeybindingContextKey} from 'vs/platform/keybinding/ import {IEventService} from 'vs/platform/event/common/event'; import {IEditorService} from 'vs/platform/editor/common/editor'; import {KeyMod, KeyCode} from 'vs/base/common/keyCodes'; -import {RenameRegistry} from '../common/rename'; +import {RenameRegistry, rename} from '../common/rename'; export class RenameAction extends EditorAction { @@ -145,42 +145,12 @@ export class RenameAction extends EditorAction { // start recording of file changes so that we can figure out if a file that // is to be renamed conflicts with another (concurrent) modification - let sourceModel = this.editor.getModel().getAssociatedResource(); - let sourceSelections = this.editor.getSelections(); + let edit = createBulkEdit(this._eventService, this._editorService, this.editor); - let supports = RenameRegistry.ordered(this.editor.getModel()); - let hasResult = false; - let rejects: string[] = []; - - let factory = supports.map(support => { - return () => { - if (!hasResult) { - return support.rename(sourceModel, this.editor.getPosition(), newName).then(result => { - if (!result) { - // ignore - } else if (!result.rejectReason) { - hasResult = true; - return result; - } else { - rejects.push(result.rejectReason); - } - }); - } - }; - }); - - let edit = createBulkEdit(this._eventService, this._editorService, - this.editor); - - return sequence(factory).then(values => { - - let result = values[0]; - if (rejects.length > 0) { - return TPromise.wrapError(rejects.join('\n')); - } else if (!result) { - return TPromise.wrapError(nls.localize('no result', "No result.")); + return rename(this.editor.getModel(), this.editor.getPosition(), newName).then(result => { + if (result.rejectReason) { + return TPromise.wrapError(result.rejectReason); } - edit.add(result.edits); return edit; }); diff --git a/src/vs/editor/contrib/rename/common/rename.ts b/src/vs/editor/contrib/rename/common/rename.ts index a511e319128..a7e122a87a9 100644 --- a/src/vs/editor/contrib/rename/common/rename.ts +++ b/src/vs/editor/contrib/rename/common/rename.ts @@ -5,7 +5,56 @@ 'use strict'; -import {IRenameSupport} from 'vs/editor/common/modes'; +import {IRenameSupport, IRenameResult} from 'vs/editor/common/modes'; +import {IModel, IPosition} from 'vs/editor/common/editorCommon'; +import {TPromise} from 'vs/base/common/winjs.base'; +import {localize} from 'vs/nls'; +import {sequence} from 'vs/base/common/async'; +import {onUnexpectedError} from 'vs/base/common/errors'; import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; export const RenameRegistry = new LanguageFeatureRegistry('renameSupport'); + +export function rename(model: IModel, position: IPosition, newName: string): TPromise { + + const supports = RenameRegistry.ordered(model); + const resource = model.getAssociatedResource(); + const rejects: string[] = []; + let hasResult = false; + + const factory = supports.map(support => { + return () => { + if (!hasResult) { + return support.rename(resource, position, newName).then(result => { + if (!result) { + // ignore + } else if (!result.rejectReason) { + hasResult = true; + return result; + } else { + rejects.push(result.rejectReason); + } + }); + } + }; + }); + + return sequence(factory).then(values => { + let result = values[0]; + if (rejects.length > 0) { + return { + currentName: undefined, + edits: undefined, + rejectReason: rejects.join('\n') + }; + } else if (!result) { + return { + currentName: undefined, + edits: undefined, + rejectReason: localize('no result', "No result.") + }; + } else { + return result; + } + }); +} \ No newline at end of file diff --git a/src/vs/workbench/api/browser/pluginHost.api.impl.ts b/src/vs/workbench/api/browser/pluginHost.api.impl.ts index e466e1dd0fa..8de01d9e74a 100644 --- a/src/vs/workbench/api/browser/pluginHost.api.impl.ts +++ b/src/vs/workbench/api/browser/pluginHost.api.impl.ts @@ -283,7 +283,7 @@ export class PluginHostAPIImplementation { return languageFeatures.registerReferenceProvider(selector, provider); }, registerRenameProvider(selector: vscode.DocumentSelector, provider: vscode.RenameProvider): vscode.Disposable { - return features.rename.register(selector, provider); + return languageFeatures.registerRenameProvider(selector, provider); }, registerDocumentSymbolProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentSymbolProvider): vscode.Disposable { return languageFeatures.registerDocumentSymbolProvider(selector, provider); diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 72618bb2842..e49617773f1 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -454,8 +454,58 @@ class NavigateTypeAdapter implements INavigateTypesSupport { } } +class RenameAdapter implements modes.IRenameSupport { + + private _documents: PluginHostModelService; + private _provider: vscode.RenameProvider; + + constructor(documents: PluginHostModelService, provider: vscode.RenameProvider) { + this._documents = documents; + this._provider = provider; + } + + rename(resource: URI, position: IPosition, newName: string): TPromise { + + let doc = this._documents.getDocument(resource); + let pos = TypeConverters.toPosition(position); + + return asWinJsPromise(token => this._provider.provideRenameEdits(doc, pos, newName, token)).then(value => { + + if (!value) { + return; + } + + let result = { + currentName: undefined, + 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: TypeConverters.fromRange(textEdit.range) + }); + } + } + return result; + }, err => { + if (typeof err === 'string') { + return { + currentName: undefined, + edits: undefined, + rejectReason: err + }; + } + return TPromise.wrapError(err); + }); + } +} + type Adapter = OutlineAdapter | CodeLensAdapter | DeclarationAdapter | ExtraInfoAdapter | OccurrencesAdapter | ReferenceAdapter | QuickFixAdapter - | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter; + | DocumentFormattingAdapter | RangeFormattingAdapter | OnTypeFormattingAdapter | NavigateTypeAdapter | RenameAdapter; @Remotable.PluginHostContext('ExtHostLanguageFeatures') export class ExtHostLanguageFeatures { @@ -638,6 +688,19 @@ export class ExtHostLanguageFeatures { $getNavigateToItems(handle: number, search: string): TPromise { return this._withAdapter(handle, NavigateTypeAdapter, adapter => adapter.getNavigateToItems(search)); } + + // --- rename + + registerRenameProvider(selector: vscode.DocumentSelector, provider: vscode.RenameProvider): vscode.Disposable { + const handle = this._nextHandle(); + this._adapter[handle] = new RenameAdapter(this._documents, provider); + this._proxy.$registerRenameSupport(handle, selector); + return this._createDisposable(handle); + } + + $rename(handle: number, resource: URI, position: IPosition, newName: string): TPromise { + return this._withAdapter(handle, RenameAdapter, adapter => adapter.rename(resource, position, newName)); + } } @Remotable.MainContext('MainThreadLanguageFeatures') @@ -798,4 +861,15 @@ export class MainThreadLanguageFeatures { }); return undefined; } + + // --- rename + + $registerRenameSupport(handle: number, selector: vscode.DocumentSelector): TPromise { + this._registrations[handle] = RenameRegistry.register(selector, { + rename: (resource: URI, position: IPosition, newName: string): TPromise => { + return this._proxy.$rename(handle, resource, position, newName); + } + }); + return undefined; + } } \ No newline at end of file diff --git a/src/vs/workbench/api/common/languageFeatures.ts b/src/vs/workbench/api/common/languageFeatures.ts index 0d4b5744de0..9a9a94c05f4 100644 --- a/src/vs/workbench/api/common/languageFeatures.ts +++ b/src/vs/workbench/api/common/languageFeatures.ts @@ -131,79 +131,6 @@ export abstract class AbstractExtensionHostFeature { - - constructor( @IThreadService threadService: IThreadService) { - super(threadService.getRemotable(MainThreadRename), threadService); - } - - _runAsCommand(resource: URI, position: IPosition, newName: string): TPromise { - - let document = this._models.getDocument(resource); - let pos = TypeConverters.toPosition(position); - - let hasResult = false; - let rejects: string[] = []; - let factory = this._getOrderedFor(document).map(provider => { - return () => { - if (!hasResult) { - return asWinJsPromise(token => provider.provideRenameEdits(document, pos, newName, token)).then(result => { - if (result && result.size > 0) { - hasResult = true; - return result; - } - }, err => { - if (typeof err === 'string') { - rejects.push(err); - } - }); - } - }; - }); - - return sequence(factory).then(results => { - let rename = results[0]; - if (!rename) { - return { - rejectReason: rejects.join('\n'), - edits: undefined, - currentName: undefined - }; - } - - let result = { - currentName: undefined, - edits: [] - }; - for (let entry of rename.entries()) { - let [uri, textEdits] = entry; - for (let textEdit of textEdits) { - result.edits.push({ - resource: uri, - newText: textEdit.newText, - range: TypeConverters.fromRange(textEdit.range) - }); - } - } - return result; - }); - } -} - -@Remotable.MainContext('MainThreadRename') -export class MainThreadRename extends AbstractMainThreadFeature implements modes.IRenameSupport { - - constructor( @IThreadService threadService: IThreadService) { - super('vscode.executeDocumentRenameProvider', RenameRegistry, threadService); - } - - rename(resource: URI, position: IPosition, newName: string): TPromise { - return this._executeCommand(resource, position, newName); - } -} - // --- format export class ExtHostFormatDocument extends AbstractExtensionHostFeature { @@ -628,14 +555,12 @@ export class MainThreadCompletions extends AbstractMainThreadFeature{ + provideRenameEdits(): any { + throw Error('evil'); + } + })); + + threadService.sync().then(() => { + + rename(model, { lineNumber: 1, column: 1 }, 'newName').then(value => { + done(new Error('')); + }, err => { + done(); // expected + }); + }); + }); + + test('Rename, evil provider 2/2', function(done) { + + disposables.push(extHost.registerRenameProvider('*', { + provideRenameEdits(): any { + throw Error('evil'); + } + })); + + disposables.push(extHost.registerRenameProvider(defaultSelector, { + provideRenameEdits(): any { + let edit = new types.WorkspaceEdit(); + edit.replace(model.getAssociatedResource(), new types.Range(0, 0, 0, 0), 'testing'); + return edit; + } + })); + + threadService.sync().then(() => { + + rename(model, { lineNumber: 1, column: 1 }, 'newName').then(value => { + assert.equal(value.edits.length, 1); + done(); + }); + }); + }); + + test('Rename, ordering', function(done) { + + disposables.push(extHost.registerRenameProvider('*', { + provideRenameEdits(): any { + let edit = new types.WorkspaceEdit(); + edit.replace(model.getAssociatedResource(), new types.Range(0, 0, 0, 0), 'testing'); + edit.replace(model.getAssociatedResource(), new types.Range(1, 0, 1, 0), 'testing'); + return edit; + } + })); + + disposables.push(extHost.registerRenameProvider(defaultSelector, { + provideRenameEdits(): any { + return; + } + })); + + threadService.sync().then(() => { + + rename(model, { lineNumber: 1, column: 1 }, 'newName').then(value => { + assert.equal(value.edits.length, 2); // least relevant renamer + done(); + }); + }); + }); }); \ No newline at end of file