diff --git a/src/vs/editor/contrib/quickFix/browser/quickFix.ts b/src/vs/editor/contrib/quickFix/browser/quickFix.ts index 3f708ee8392..160d467807f 100644 --- a/src/vs/editor/contrib/quickFix/browser/quickFix.ts +++ b/src/vs/editor/contrib/quickFix/browser/quickFix.ts @@ -24,7 +24,7 @@ import {IEventService} from 'vs/platform/event/common/event'; import {IEditorService} from 'vs/platform/editor/common/editor'; import {IMessageService} from 'vs/platform/message/common/message'; import {bulkEdit} from 'vs/editor/common/services/bulkEdit'; -import QuickFixRegistry, {IQuickFix2} from '../common/quickFix'; +import {QuickFixRegistry, IQuickFix2} from '../common/quickFix'; import {KeyMod, KeyCode} from 'vs/base/common/keyCodes'; export class QuickFixController implements EditorCommon.IEditorContribution { diff --git a/src/vs/editor/contrib/quickFix/browser/quickFixModel.ts b/src/vs/editor/contrib/quickFix/browser/quickFixModel.ts index f7fdff0c7eb..29677542a14 100644 --- a/src/vs/editor/contrib/quickFix/browser/quickFixModel.ts +++ b/src/vs/editor/contrib/quickFix/browser/quickFixModel.ts @@ -16,7 +16,7 @@ import schedulers = require('vs/base/common/async'); import errors = require('vs/base/common/errors'); import {Range} from 'vs/editor/common/core/range'; import {IMarkerService, IMarker} from 'vs/platform/markers/common/markers'; -import QuickFixRegistry, {IQuickFix2} from '../common/quickFix'; +import {QuickFixRegistry, IQuickFix2, getQuickFixes} from '../common/quickFix'; import LightBulpWidget = require('./lightBulpWidget'); enum QuickFixSuggestState { @@ -212,27 +212,7 @@ export class QuickFixModel extends events.EventEmitter { } this.quickFixRequestPromiseRange = range; - let quickFixes: IQuickFix2[] = []; - let promises = QuickFixRegistry.all(model).map(support => { - return support.getQuickFixes(model.getAssociatedResource(), range).then(result => { - if (!Array.isArray(result)) { - return - } - for (let fix of result) { - quickFixes.push({ - id: fix.id, - label: fix.label, - documentation: fix.documentation, - score: fix.score, - support - }); - } - }, err => { - errors.onUnexpectedError(err); - }); - }); - - this.quickFixRequestPromise = TPromise.join(promises).then(() => quickFixes); + this.quickFixRequestPromise = getQuickFixes(model, range); return this.quickFixRequestPromise; } diff --git a/src/vs/editor/contrib/quickFix/common/quickFix.ts b/src/vs/editor/contrib/quickFix/common/quickFix.ts index 3b17a46819b..4e7e841c8ae 100644 --- a/src/vs/editor/contrib/quickFix/common/quickFix.ts +++ b/src/vs/editor/contrib/quickFix/common/quickFix.ts @@ -5,13 +5,39 @@ 'use strict'; +import {IModel, IRange} from 'vs/editor/common/editorCommon'; +import {TPromise} from 'vs/base/common/winjs.base'; +import {onUnexpectedError} from 'vs/base/common/errors'; import {IQuickFixSupport, IQuickFix} from 'vs/editor/common/modes'; import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; -const QuickFixRegistry = new LanguageFeatureRegistry('quickFixSupport'); - -export default QuickFixRegistry; +export const QuickFixRegistry = new LanguageFeatureRegistry('quickFixSupport'); export interface IQuickFix2 extends IQuickFix { support: IQuickFixSupport; +} + +export function getQuickFixes(model: IModel, range: IRange): TPromise { + + const quickFixes: IQuickFix2[] = []; + const promises = QuickFixRegistry.all(model).map(support => { + return support.getQuickFixes(model.getAssociatedResource(), range).then(result => { + if (!Array.isArray(result)) { + return + } + for (let fix of result) { + quickFixes.push({ + id: fix.id, + label: fix.label, + documentation: fix.documentation, + score: fix.score, + support + }); + } + }, err => { + onUnexpectedError(err); + }); + }); + + return TPromise.join(promises).then(() => quickFixes); } \ No newline at end of file diff --git a/src/vs/editor/contrib/referenceSearch/common/referenceSearch.ts b/src/vs/editor/contrib/referenceSearch/common/referenceSearch.ts index 333047e044e..df94dd6e558 100644 --- a/src/vs/editor/contrib/referenceSearch/common/referenceSearch.ts +++ b/src/vs/editor/contrib/referenceSearch/common/referenceSearch.ts @@ -5,8 +5,7 @@ 'use strict'; -import {IReferenceSupport} from 'vs/editor/common/modes'; -import {IReference} from 'vs/editor/common/modes'; +import {IReferenceSupport, IReference} from 'vs/editor/common/modes'; import {IModel, IPosition} from 'vs/editor/common/editorCommon'; import {TPromise} from 'vs/base/common/winjs.base'; import {onUnexpectedError} from 'vs/base/common/errors'; diff --git a/src/vs/platform/thread/common/abstractThreadService.ts b/src/vs/platform/thread/common/abstractThreadService.ts index 8cbd86d9965..251646149c5 100644 --- a/src/vs/platform/thread/common/abstractThreadService.ts +++ b/src/vs/platform/thread/common/abstractThreadService.ts @@ -45,7 +45,8 @@ export abstract class AbstractThreadService implements remote.IManyHandler { public isInMainThread:boolean; - private _instantiationService:instantiation.IInstantiationService; + protected _instantiationService: instantiation.IInstantiationService; + _boundObjects:{[id:string]:IThreadSynchronizableObject;}; _pendingObjects:winjs.Promise[]; private _localObjMap: { [id:string]: any; }; diff --git a/src/vs/workbench/api/browser/pluginHost.api.impl.ts b/src/vs/workbench/api/browser/pluginHost.api.impl.ts index d36f3f7a2de..a958e68f6a8 100644 --- a/src/vs/workbench/api/browser/pluginHost.api.impl.ts +++ b/src/vs/workbench/api/browser/pluginHost.api.impl.ts @@ -265,7 +265,7 @@ export class PluginHostAPIImplementation { return score(selector, { uri: document.uri, language: document.languageId }); }, registerCodeActionsProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider): vscode.Disposable { - return features.codeActions.register(selector, provider); + return languageFeatures.registerCodeActionProvider(selector, provider); }, registerCodeLensProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable { return languageFeatures.registerCodeLensProvider(selector, provider); diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index 61df0119d4c..497e9cade19 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -27,7 +27,7 @@ import {DeclarationRegistry} from 'vs/editor/contrib/goToDeclaration/common/goTo import {ExtraInfoRegistry} from 'vs/editor/contrib/hover/common/hover'; import {OccurrencesRegistry} from 'vs/editor/contrib/wordHighlighter/common/wordHighlighter'; import {ReferenceRegistry} from 'vs/editor/contrib/referenceSearch/common/referenceSearch'; -import QuickFixRegistry from 'vs/editor/contrib/quickFix/common/quickFix'; +import {QuickFixRegistry} from 'vs/editor/contrib/quickFix/common/quickFix'; import {OutlineRegistry, IOutlineEntry, IOutlineSupport} from 'vs/editor/contrib/quickOpen/common/quickOpen'; import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; import {NavigateTypesSupportRegistry, INavigateTypesSupport, ITypeBearing} from 'vs/workbench/parts/search/common/search' @@ -295,7 +295,68 @@ class ReferenceAdapter implements modes.IReferenceSupport { } } -type Adapter = OutlineAdapter | CodeLensAdapter | DeclarationAdapter | ExtraInfoAdapter | OccurrencesAdapter | ReferenceAdapter; +class QuickFixAdapter implements modes.IQuickFixSupport { + + private _documents: PluginHostModelService; + private _commands: PluginHostCommands; + private _provider: vscode.CodeActionProvider; + private _cache: { [key: string]: vscode.Command[] } = Object.create(null); + + constructor(documents: PluginHostModelService, commands: PluginHostCommands, provider: vscode.CodeActionProvider) { + this._documents = documents; + this._commands = commands; + this._provider = provider; + } + + getQuickFixes(resource: URI, range: IRange, marker?: IMarker[]): TPromise { + + // return this._executeCommand(resource, range, markers); + const key = resource.toString(); + delete this._cache[key]; + + const doc = this._documents.getDocument(resource); + const ran = TypeConverters.toRange(range); + const diagnostics = marker.map(marker => { + const diag = new Diagnostic(TypeConverters.toRange(marker), marker.message); + diag.code = marker.code; + diag.severity = TypeConverters.toDiagnosticSeverty(marker.severity); + return diag; + }); + + return asWinJsPromise(token => this._provider.provideCodeActions(doc, ran, { diagnostics: diagnostics }, token)).then(commands => { + if (!Array.isArray(commands)) { + return; + } + + this._cache[key] = commands; + + return commands.map((command, i) => { + return { + id: String(i), + label: command.title, + score: 1 + }; + }); + }); + } + + runQuickFixAction(resource: URI, range: IRange, id: string): any { + + let commands = this._cache[resource.toString()]; + if (!commands) { + return TPromise.wrapError('no command for ' + resource.toString()); + } + + let command = commands[Number(id)]; + if (!command) { + return TPromise.wrapError('no command for ' + resource.toString()); + } + + return this._commands.executeCommand(command.command, ...command.arguments); + } +} + +type Adapter = OutlineAdapter | CodeLensAdapter | DeclarationAdapter | ExtraInfoAdapter | OccurrencesAdapter | ReferenceAdapter | QuickFixAdapter; @Remotable.PluginHostContext('ExtHostLanguageFeatures') export class ExtHostLanguageFeatures { @@ -304,11 +365,13 @@ export class ExtHostLanguageFeatures { private _proxy: MainThreadLanguageFeatures; private _documents: PluginHostModelService; + private _commands: PluginHostCommands; private _adapter: { [handle: number]: Adapter } = Object.create(null); constructor( @IThreadService threadService: IThreadService) { this._proxy = threadService.getRemotable(MainThreadLanguageFeatures); this._documents = threadService.getRemotable(PluginHostModelService); + this._commands = threadService.getRemotable(PluginHostCommands); } private _createDisposable(handle: number): Disposable { @@ -412,16 +475,34 @@ export class ExtHostLanguageFeatures { return this._withAdapter(handle, ReferenceAdapter, adapter => adapter.findReferences(resource, position, includeDeclaration)); } + // --- quick fix + + registerCodeActionProvider(selector: vscode.DocumentSelector, provider: vscode.CodeActionProvider): vscode.Disposable { + const handle = this._nextHandle(); + this._adapter[handle] = new QuickFixAdapter(this._documents, this._commands, provider); + this._proxy.$registerQuickFixSupport(handle, selector); + return this._createDisposable(handle); + } + + $getQuickFixes(handle:number, resource: URI, range: IRange, marker: IMarker[]): TPromise { + return this._withAdapter(handle, QuickFixAdapter, adapter => adapter.getQuickFixes(resource, range, marker)); + } + + $runQuickFixAction(handle: number, resource: URI, range: IRange, id: string): any { + return this._withAdapter(handle, QuickFixAdapter, adapter => adapter.runQuickFixAction(resource, range, id)); + } } @Remotable.MainContext('MainThreadLanguageFeatures') export class MainThreadLanguageFeatures { private _proxy: ExtHostLanguageFeatures; + private _markerService: IMarkerService; private _registrations: { [handle: number]: IDisposable; } = Object.create(null); - constructor( @IThreadService threadService: IThreadService) { + constructor( @IThreadService threadService: IThreadService, @IMarkerService markerService: IMarkerService) { this._proxy = threadService.getRemotable(ExtHostLanguageFeatures); + this._markerService = markerService; } $unregister(handle: number): TPromise { @@ -507,4 +588,24 @@ export class MainThreadLanguageFeatures { }); return undefined; } + + // --- quick fix + + $registerQuickFixSupport(handle: number, selector: vscode.DocumentSelector): TPromise { + this._registrations[handle] = QuickFixRegistry.register(selector, { + getQuickFixes: (resource: URI, range: IRange): TPromise => { + let markers: IMarker[] = []; + this._markerService.read({ resource }).forEach(marker => { + if (EditorRange.lift(marker).intersectRanges(range)) { + markers.push(marker); + } + }); + return this._proxy.$getQuickFixes(handle, resource, range, markers); + }, + runQuickFixAction: (resource: URI, range: IRange, id: string) => { + return this._proxy.$runQuickFixAction(handle, resource, range, id); + } + }); + 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 4b82cf172d9..bd81407d0ec 100644 --- a/src/vs/workbench/api/common/languageFeatures.ts +++ b/src/vs/workbench/api/common/languageFeatures.ts @@ -22,7 +22,6 @@ import {CancellationTokenSource} from 'vs/base/common/cancellation'; import {PluginHostModelService} from 'vs/workbench/api/common/pluginHostDocuments'; import {IMarkerService, IMarker} from 'vs/platform/markers/common/markers'; import {PluginHostCommands, MainThreadCommands} from 'vs/workbench/api/common/pluginHostCommands'; -import QuickFixRegistry from 'vs/editor/contrib/quickFix/common/quickFix'; import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; import {NavigateTypesSupportRegistry, INavigateTypesSupport, ITypeBearing} from 'vs/workbench/parts/search/common/search' import {RenameRegistry} from 'vs/editor/contrib/rename/common/rename'; @@ -131,104 +130,6 @@ export abstract class AbstractExtensionHostFeature { - - private _disposable: Disposable; - - constructor(@IThreadService threadService: IThreadService) { - super(threadService.getRemotable(MainThreadCodeActions), threadService); - } - - _runAsCommand(resource: URI, range: IRange, marker:IMarker[]): TPromise { - - let document = this._models.getDocument(resource); - let _range = TypeConverters.toRange(range); - let commands: vscode.Command[] = []; - - let diagnostics = marker.map(marker => { - let diag = new Diagnostic(TypeConverters.toRange(marker), marker.message); - diag.code = marker.code; - diag.severity = TypeConverters.toDiagnosticSeverty(marker.severity); - return diag; - }); - - let promises = this._getAllFor(document).map(provider => { - return asWinJsPromise(token => provider.provideCodeActions(document, _range, { diagnostics }, token)).then(result => { - if (Array.isArray(result)) { - commands.push(...result); - } - }, err => { - console.error(err); - }); - }); - - return TPromise.join(promises).then(() => { - if (this._disposable) { - this._disposable.dispose(); - } - - let disposables: IDisposable[] = []; - let quickFixes: modes.IQuickFix[] = []; - - commands.forEach((command, i) => { - - let id = '_code_action_action_wrapper_#' + i; - - // create fake action such that the aruments don't - // have to be send between ext-host - disposables.push(this._commands.registerCommand(id, () => { - return this._commands.executeCommand(command.command, ...command.arguments); - })); - - // create quick fix - quickFixes.push({ - id, - label: command.title, - score: 1 - }); - }); - - // not very nice... we need - // some sort of event to tell us when - // quick fix is bored of our commands - this._disposable = Disposable.from(...disposables); - return quickFixes; - }); - } -} - -@Remotable.MainContext('MainThreadCodeAction') -export class MainThreadCodeActions extends AbstractMainThreadFeature implements modes.IQuickFixSupport { - - private _keybindingService: IKeybindingService; - private _markerService: IMarkerService; - - constructor( @IThreadService threadService: IThreadService, @IKeybindingService keybindingService: IKeybindingService, - @IMarkerService markerService: IMarkerService) { - - super('vscode.executeCodeActionProvider', QuickFixRegistry, threadService); - this._keybindingService = keybindingService; - this._markerService = markerService; - } - - getQuickFixes(resource: URI, range: IRange): TPromise { - - let markers: IMarker[] = []; - this._markerService.read({ resource }).forEach(marker => { - if (EditorRange.lift(marker).intersectRanges(range)) { - markers.push(marker); - } - }); - - return this._executeCommand(resource, range, markers); - } - - runQuickFixAction (resource:URI, range:IRange, id:string) { - return TPromise.as(this._keybindingService.executeCommand(id)); - } -} // -- Rename provider @@ -826,7 +727,6 @@ export class MainThreadWorkspaceSymbols implements INavigateTypesSupport { export namespace LanguageFeatures { export function createMainThreadInstances(threadService: IThreadService): void { - threadService.getRemotable(MainThreadCodeActions); threadService.getRemotable(MainThreadWorkspaceSymbols); threadService.getRemotable(MainThreadRename); threadService.getRemotable(MainThreadFormatDocument); @@ -838,7 +738,6 @@ export namespace LanguageFeatures { export function createExtensionHostInstances(threadService: IThreadService) { return { - codeActions: new ExtensionHostCodeActions(threadService), workspaceSymbols: new ExtensionHostWorkspaceSymbols(threadService), rename: new ExtensionHostRename(threadService), formatDocument: new ExtHostFormatDocument(threadService), diff --git a/src/vs/workbench/test/common/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/common/api/extHostLanguageFeatures.test.ts index b7b89a03c96..997e30d7bfa 100644 --- a/src/vs/workbench/test/common/api/extHostLanguageFeatures.test.ts +++ b/src/vs/workbench/test/common/api/extHostLanguageFeatures.test.ts @@ -17,6 +17,10 @@ import {Range as CodeEditorRange} from 'vs/editor/common/core/range'; import * as EditorCommon from 'vs/editor/common/editorCommon'; import {Model as EditorModel} from 'vs/editor/common/model/model'; import threadService from './testThreadService' +import {create as createInstantiationService} from 'vs/platform/instantiation/common/instantiationService'; +import {MarkerService} from 'vs/platform/markers/common/markerService'; +import {IMarkerService} from 'vs/platform/markers/common/markers'; +import {IThreadService} from 'vs/platform/thread/common/thread'; import {ExtHostLanguageFeatures, MainThreadLanguageFeatures} from 'vs/workbench/api/common/extHostLanguageFeatures'; import {PluginHostCommands, MainThreadCommands} from 'vs/workbench/api/common/pluginHostCommands'; import {PluginHostModelService} from 'vs/workbench/api/common/pluginHostDocuments'; @@ -28,6 +32,7 @@ import {DeclarationRegistry, getDeclarationsAtPosition} from 'vs/editor/contrib/ import {ExtraInfoRegistry, getExtraInfoAtPosition} from 'vs/editor/contrib/hover/common/hover'; import {OccurrencesRegistry, getOccurrencesAtPosition} from 'vs/editor/contrib/wordHighlighter/common/wordHighlighter'; import {ReferenceRegistry, findReferences} from 'vs/editor/contrib/referenceSearch/common/referenceSearch'; +import {getQuickFixes} from 'vs/editor/contrib/quickFix/common/quickFix'; const defaultSelector = { scheme: 'far' }; const model: EditorCommon.IModel = new EditorModel( @@ -48,6 +53,11 @@ suite('ExtHostLanguageFeatures', function() { suiteSetup(() => { + let instantiationService = createInstantiationService(); + threadService.setInstantiationService(instantiationService); + instantiationService.addSingleton(IMarkerService, new MarkerService(threadService)); + instantiationService.addSingleton(IThreadService, threadService); + originalErrorHandler = errorHandler.getUnexpectedErrorHandler(); setUnexpectedErrorHandler(() => { }); @@ -591,4 +601,84 @@ suite('ExtHostLanguageFeatures', function() { }); }); + + // --- quick fix + + test('Quick Fix, data conversion', function(done) { + + disposables.push(extHost.registerCodeActionProvider(defaultSelector, { + provideCodeActions(): any { + return [ + { command: 'test', title: 'Testing1' }, + { command: 'test', title: 'Testing2' } + ]; + } + })); + + threadService.sync().then(() => { + getQuickFixes(model, model.getFullModelRange()).then(value => { + assert.equal(value.length, 2); + + let [first, second] = value; + assert.equal(first.label, 'Testing1'); + assert.equal(first.id, String(0)); + assert.equal(second.label, 'Testing2'); + assert.equal(second.id, String(1)); + done(); + }); + }); + }); + + test('Quick Fix, invoke command+args', function(done) { + let actualArgs: any; + let commands = threadService.getRemotable(PluginHostCommands); + disposables.push(commands.registerCommand('test1', function(...args: any[]) { + actualArgs = args; + })); + + disposables.push(extHost.registerCodeActionProvider(defaultSelector, { + provideCodeActions(): any { + return [{ command: 'test1', title: 'Testing', arguments: [true, 1, { bar: 'boo', foo: 'far' }, null] }]; + } + })); + + threadService.sync().then(() => { + getQuickFixes(model, model.getFullModelRange()).then(value => { + assert.equal(value.length, 1); + + let [entry] = value; + entry.support.runQuickFixAction(model.getAssociatedResource(), model.getFullModelRange(), entry.id).then(value => { + assert.equal(value, undefined); + + assert.equal(actualArgs.length, 4); + assert.equal(actualArgs[0], true) + assert.equal(actualArgs[1], 1) + assert.deepEqual(actualArgs[2], { bar: 'boo', foo: 'far' }); + assert.equal(actualArgs[3], null) + done(); + }); + }); + }); + }); + + test('Quick Fix, evil provider', function(done) { + + disposables.push(extHost.registerCodeActionProvider(defaultSelector, { + provideCodeActions(): any { + throw new Error('evil'); + } + })); + disposables.push(extHost.registerCodeActionProvider(defaultSelector, { + provideCodeActions(): any { + return [{ command: 'test', title: 'Testing' }]; + } + })); + + threadService.sync().then(() => { + getQuickFixes(model, model.getFullModelRange()).then(value => { + assert.equal(value.length, 1); + done(); + }); + }); + }); }); \ No newline at end of file diff --git a/src/vs/workbench/test/common/api/testThreadService.ts b/src/vs/workbench/test/common/api/testThreadService.ts index 1d83c6feab6..6bf5118e96a 100644 --- a/src/vs/workbench/test/common/api/testThreadService.ts +++ b/src/vs/workbench/test/common/api/testThreadService.ts @@ -9,6 +9,7 @@ import {NullThreadService} from 'vs/platform/test/common/nullThreadService'; import {create} from 'vs/base/common/types'; import {SyncDescriptor0} from 'vs/platform/instantiation/common/descriptors'; import {TPromise} from 'vs/base/common/winjs.base'; +import {IInstantiationService} from 'vs/platform/instantiation/common/instantiation'; export class TestThreadService extends NullThreadService { @@ -57,7 +58,7 @@ export class TestThreadService extends NullThreadService { return TPromise.timeout(0).then(() => { if (!_instance) { - _instance = create(descriptor.ctor, this); + _instance = this._instantiationService.createInstance(descriptor.ctor); } let p: TPromise; try {