diff --git a/src/vs/editor/contrib/quickOpen/common/quickOpen.ts b/src/vs/editor/contrib/quickOpen/common/quickOpen.ts index b4e2dc1c9ea..5894c18e632 100644 --- a/src/vs/editor/contrib/quickOpen/common/quickOpen.ts +++ b/src/vs/editor/contrib/quickOpen/common/quickOpen.ts @@ -11,6 +11,7 @@ import {Range} from 'vs/editor/common/core/range'; import {IModel} from 'vs/editor/common/editorCommon'; import {IOutlineEntry, IOutlineSupport} from 'vs/editor/common/modes'; import LanguageFeatureRegistry from 'vs/editor/common/modes/languageFeatureRegistry'; +import {ModelLike} from 'vs/editor/common/modes/languageSelector'; const OutlineRegistry = new LanguageFeatureRegistry('outlineSupport'); @@ -20,13 +21,12 @@ export { IOutlineSupport } -export function getOutlineEntries(model: IModel): TPromise<{ entries: IOutlineEntry[], outlineGroupLabel: { [n: string]: string;} }> { +export function getOutlineEntries(model: ModelLike): TPromise<{ entries: IOutlineEntry[], outlineGroupLabel: { [n: string]: string;} }> { - let resource = model.getAssociatedResource(); let groupLabels: { [n: string]: string } = Object.create(null); let entries: IOutlineEntry[] = []; - let promises = OutlineRegistry.all(model).map(support => { + let promises = OutlineRegistry.all(model).map(support => { if (support.outlineGroupLabel) { for (var key in support.outlineGroupLabel) { @@ -36,7 +36,7 @@ export function getOutlineEntries(model: IModel): TPromise<{ entries: IOutlineEn } } - return support.getOutline(resource).then(result => { + return support.getOutline( model.uri).then(result => { if (Array.isArray(result)) { entries.push(...result); } diff --git a/src/vs/workbench/api/common/extHostLanguageFeatures.ts b/src/vs/workbench/api/common/extHostLanguageFeatures.ts index d99e870d0e5..baa4d1e25b4 100644 --- a/src/vs/workbench/api/common/extHostLanguageFeatures.ts +++ b/src/vs/workbench/api/common/extHostLanguageFeatures.ts @@ -8,6 +8,7 @@ import URI from 'vs/base/common/uri'; import Event, {Emitter} from 'vs/base/common/event'; import Severity from 'vs/base/common/severity'; import {TPromise} from 'vs/base/common/winjs.base'; +import {onUnexpectedError} from 'vs/base/common/errors'; import {sequence} from 'vs/base/common/async'; import {Range as EditorRange} from 'vs/editor/common/core/range'; import {IDisposable} from 'vs/base/common/lifecycle'; @@ -113,8 +114,8 @@ export class ExtHostLanguageFeatures { registerDocumentSymbolProvider(selector: vscode.DocumentSelector, provider: vscode.DocumentSymbolProvider): vscode.Disposable { const handle = ExtHostLanguageFeatures._handlePool++; - this._proxy.$registerOutlineSupport(handle, selector); this._adapter[handle] = new OutlineSupportAdapter(this._documents, provider); + this._proxy.$registerOutlineSupport(handle, selector); return this._createDisposable(handle); } @@ -131,17 +132,17 @@ export class ExtHostLanguageFeatures { export class MainThreadLanguageFeatures { private _proxy: ExtHostLanguageFeatures; - private _disposables: { [handle: number]: IDisposable; } = Object.create(null); + private _registrations: { [handle: number]: IDisposable; } = Object.create(null); constructor( @IThreadService threadService: IThreadService) { this._proxy = threadService.getRemotable(ExtHostLanguageFeatures); } $unregister(handle: number): TPromise { - let d = this._disposables[handle]; - if (d) { - d.dispose(); - delete this._disposables[handle]; + let registration = this._registrations[handle]; + if (registration) { + registration.dispose(); + delete this._registrations[handle]; } return undefined; } @@ -154,7 +155,7 @@ export class MainThreadLanguageFeatures { return this._proxy.$getOutline(handle, resource); } }); - this._disposables[handle] = disposable; + this._registrations[handle] = disposable; return undefined; } } \ No newline at end of file diff --git a/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts b/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts index 576ce3f82c2..a4f86a7bc11 100644 --- a/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts +++ b/src/vs/workbench/parts/quickopen/browser/gotoSymbolHandler.ts @@ -527,7 +527,10 @@ export class GotoSymbolHandler extends QuickOpenHandler { return TPromise.as(this.outlineToModelCache[modelId]); } - return getOutlineEntries(model).then(outline => { + return getOutlineEntries({ + uri: ( model).getAssociatedResource(), + language: (model).getModeId() + }).then(outline => { let model = new OutlineModel(outline, this.toQuickOpenEntries(outline)); diff --git a/src/vs/workbench/test/common/api/extHostLanguageFeatures.test.ts b/src/vs/workbench/test/common/api/extHostLanguageFeatures.test.ts new file mode 100644 index 00000000000..46a90608856 --- /dev/null +++ b/src/vs/workbench/test/common/api/extHostLanguageFeatures.test.ts @@ -0,0 +1,142 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +import * as assert from 'assert'; +import {setUnexpectedErrorHandler, errorHandler} from 'vs/base/common/errors'; +import {create} from 'vs/base/common/types'; +import URI from 'vs/base/common/uri'; +import {TPromise} from 'vs/base/common/winjs.base'; +import {PluginHostDocument} from 'vs/workbench/api/common/pluginHostDocuments'; +import * as phTypes from 'vs/workbench/api/common/pluginHostTypes'; +import {Range as CodeEditorRange} from 'vs/editor/common/core/range'; +import * as EditorCommon from 'vs/editor/common/editorCommon'; +import threadService from './testThreadService' +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'; +import {SyncDescriptor0} from 'vs/platform/instantiation/common/descriptors'; +import {OutlineRegistry, getOutlineEntries} from 'vs/editor/contrib/quickOpen/common/quickOpen'; +import {LanguageSelector, ModelLike} from 'vs/editor/common/modes/languageSelector'; + + +let model: ModelLike; +let extHost: ExtHostLanguageFeatures; +let mainThread: MainThreadLanguageFeatures; +let disposables: vscode.Disposable[] = []; +let originalErrorHandler: (e: any) => any; + +suite('ExtHostLanguageFeatures', function() { + + suiteSetup(() => { + + originalErrorHandler = errorHandler.getUnexpectedErrorHandler(); + setUnexpectedErrorHandler(() => { }); + + model = { + language: 'far', + uri: URI.parse('far://testing/file.a') + }; + + threadService.getRemotable(PluginHostModelService)._acceptModelAdd({ + isDirty: false, + versionId: 1, + modeId: 'far', + url: model.uri, + value: { + EOL: '\n', + lines: [ + 'This is the first line', + 'This is the second line', + 'This is the third line', + ], + BOM: '', + length: -1 + }, + }) + + threadService.getRemotable(PluginHostCommands); + threadService.getRemotable(MainThreadCommands); + mainThread = threadService.getRemotable(MainThreadLanguageFeatures); + extHost = threadService.getRemotable(ExtHostLanguageFeatures); + }); + + suiteTeardown(() => { + setUnexpectedErrorHandler(originalErrorHandler); + }); + + teardown(function(done) { + while (disposables.length) { + disposables.pop().dispose(); + } + threadService.sync() + .then(() => done(), err => done(err)); + }); + + test('DocumentSymbols, register/deregister', function(done) { + assert.equal(OutlineRegistry.all(model).length, 0); + let d1 = extHost.registerDocumentSymbolProvider('far', { + provideDocumentSymbols() { + return []; + } + }); + + threadService.sync().then(() => { + assert.equal(OutlineRegistry.all(model).length, 1); + d1.dispose(); + threadService.sync().then(() => { + done(); + }); + }); + + }); + + test('DocumentSymbols, evil provider', function(done) { + disposables.push(extHost.registerDocumentSymbolProvider('far', { + provideDocumentSymbols(): any { + throw new Error('evil document symbol provider'); + } + })); + disposables.push(extHost.registerDocumentSymbolProvider('far', { + provideDocumentSymbols(): any { + return [new phTypes.SymbolInformation('test', phTypes.SymbolKind.Field, new phTypes.Range(0, 0, 0, 0))]; + } + })); + + threadService.sync().then(() => { + + getOutlineEntries(model).then(value => { + assert.equal(value.entries.length, 1); + done(); + }, err => { + done(err); + }); + }); + }); + + test('DocumentSymbols, data conversion', function(done) { + disposables.push(extHost.registerDocumentSymbolProvider('far', { + provideDocumentSymbols(): any { + return [new phTypes.SymbolInformation('test', phTypes.SymbolKind.Field, new phTypes.Range(0, 0, 0, 0))]; + } + })); + + threadService.sync().then(() => { + + getOutlineEntries(model).then(value => { + assert.equal(value.entries.length, 1); + + let entry = value.entries[0]; + assert.equal(entry.label, 'test'); + assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }); + done(); + + }, err => { + done(err); + }); + }); + }); +}); \ No newline at end of file diff --git a/src/vs/workbench/test/common/api/languageFeatures.test.ts b/src/vs/workbench/test/common/api/languageFeatures.test.ts deleted file mode 100644 index bf6a92187e6..00000000000 --- a/src/vs/workbench/test/common/api/languageFeatures.test.ts +++ /dev/null @@ -1,177 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -'use strict'; - -import * as assert from 'assert'; -import {create} from 'vs/base/common/types'; -import URI from 'vs/base/common/uri'; -import {TPromise} from 'vs/base/common/winjs.base'; -import {PluginHostDocument} from 'vs/workbench/api/common/pluginHostDocuments'; -import * as phTypes from 'vs/workbench/api/common/pluginHostTypes'; -import {Range as CodeEditorRange} from 'vs/editor/common/core/range'; -import * as EditorCommon from 'vs/editor/common/editorCommon'; -import {NullThreadService} from 'vs/platform/test/common/nullThreadService' -import * as LF from 'vs/workbench/api/common/languageFeatures'; -import {PluginHostCommands, MainThreadCommands} from 'vs/workbench/api/common/pluginHostCommands'; -import {PluginHostModelService} from 'vs/workbench/api/common/pluginHostDocuments'; -import {SyncDescriptor0} from 'vs/platform/instantiation/common/descriptors'; -import {OutlineRegistry} from 'vs/editor/contrib/quickOpen/common/quickOpen'; -import {LanguageSelector, ModelLike} from 'vs/editor/common/modes/languageSelector'; - -class ThreadService extends NullThreadService { - - protected _registerAndInstantiateMainProcessActor(id: string, descriptor: SyncDescriptor0): T { - - let instance: any; - - return this._getOrCreateProxyInstance({ - callOnRemote: (proxyId: string, path: string, args: any[]): TPromise => { - if (!instance) { - instance = create(descriptor.ctor, this); - } - try { - let result = (instance[path]).apply(instance, args); - return TPromise.is(result) ? result : TPromise.as(result); - } catch (err) { - return TPromise.wrapError(err); - } - } - }, id, descriptor) - } - - protected _registerAndInstantiatePluginHostActor(id: string, descriptor: SyncDescriptor0): T { - return this._getOrCreateLocalInstance(id, descriptor); - } -} - -let threadService: ThreadService; -let model: ModelLike = { language: 'far', uri: URI.parse('far://testing/file.a') }; - -let extHost: LF.ExtensionHostDocumentSymbols; -let mainHost: LF.MainThreadDocumentSymbols; - -suite('ExtHostLanguageFeatures', function() { - - suiteSetup(() => { - threadService = new ThreadService(); - let documents = threadService.getRemotable(PluginHostModelService); - documents._acceptModelAdd({ - isDirty: false, - versionId: 1, - modeId: 'far', - url: model.uri, - value: { - EOL: '\n', - lines: [ - 'This is the first line', - 'This is the second line', - 'This is the third line', - ], - BOM: '', - length: -1 - }, - }) - threadService.getRemotable(PluginHostCommands); - threadService.getRemotable(MainThreadCommands); - threadService.getRemotable(LF.MainThreadDocumentSymbols); - extHost = new LF.ExtensionHostDocumentSymbols(threadService); - mainHost = threadService.getRemotable(LF.MainThreadDocumentSymbols); - }); - - test('DocumentSymbols, register/deregister', function() { - - - // register - assert.equal(OutlineRegistry.all(model).length, 0); - let disposable = extHost.register('far', { - provideDocumentSymbols() { - return []; - } - }); - assert.equal(OutlineRegistry.all(model).length, 1); - - // deregister - disposable.dispose(); - assert.equal(OutlineRegistry.all(model).length, 0); - - // all extension host provider appear as one - disposable = extHost.register('far', { - provideDocumentSymbols() { - return []; - } - }); - let disposable2 = extHost.register('far', { - provideDocumentSymbols() { - return []; - } - }); - assert.equal(OutlineRegistry.all(model).length, 1); - - disposable.dispose(); - assert.equal(OutlineRegistry.all(model).length, 1); - disposable2.dispose(); - assert.equal(OutlineRegistry.all(model).length, 0); - }); - - test('DocumentSymbols, evil provider', function(done) { - - - let disposable = extHost.register('far', { - provideDocumentSymbols():any { - throw new Error('ddd'); - } - }); - let disposable2 = extHost.register('far', { - provideDocumentSymbols():any { - return [ - new phTypes.SymbolInformation('boo', phTypes.SymbolKind.Field, new phTypes.Range(0, 0, 0, 0)) - ]; - } - }); - - mainHost.getOutline(model.uri).then(result => { - assert.equal(result.length, 1); - done(); - - disposable.dispose(); - disposable2.dispose(); - - }, err => { - done(err); - }); - }); - - test('DocumentSymbols, data conversion', function(done) { - - let d = extHost.register('far', { - provideDocumentSymbols():any { - return [ - new phTypes.SymbolInformation('boo', - phTypes.SymbolKind.Field, - new phTypes.Range(0, 0, 0, 0), - model.uri, - 'far') - ]; - } - }); - - mainHost.getOutline(model.uri).then(result => { - assert.equal(result.length, 1); - let entry = result[0]; - - assert.equal(entry.label, 'boo'); - assert.equal(entry.containerLabel, 'far'); - assert.equal(entry.children, undefined); - assert.deepEqual(entry.range, { startLineNumber: 1, startColumn: 1, endLineNumber: 1, endColumn: 1 }); - d.dispose(); - done(); - - }, err => { - done(err); - }); - - }); -}); \ 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 new file mode 100644 index 00000000000..5b408734768 --- /dev/null +++ b/src/vs/workbench/test/common/api/testThreadService.ts @@ -0,0 +1,84 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +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'; + +export class TestThreadService extends NullThreadService { + + private _callCountValue: number = 0; + private _idle: TPromise; + private _completeIdle: Function; + + private get _callCount(): number { + return this._callCountValue; + } + + private set _callCount(value:number) { + this._callCountValue = value; + if (this._callCountValue === 0) { + this._completeIdle(); + this._idle = undefined; + } + } + + sync(): TPromise { + if (this._callCount === 0) { + return TPromise.as(undefined); + } + if (!this._idle) { + this._idle = new TPromise((c, e) => { + this._completeIdle = c; + }, function() { + // no cancel + }); + } + return this._idle; + } + + protected _registerAndInstantiateMainProcessActor(id: string, descriptor: SyncDescriptor0): T { + + let instance: any; + + return this._getOrCreateProxyInstance({ + callOnRemote: (proxyId: string, path: string, args: any[]): TPromise => { + + this._callCount++; + + return TPromise.timeout(0).then(() => { + if (!instance) { + instance = create(descriptor.ctor, this); + } + let p: TPromise; + try { + let result = (instance[path]).apply(instance, args); + p = TPromise.is(result) ? result : TPromise.as(result); + } catch (err) { + p = TPromise.wrapError(err); + } + + return p.then(result => { + this._callCount--; + return result; + }, err => { + this._callCount--; + return TPromise.wrapError(err); + }); + }); + } + }, id, descriptor) + } + + protected _registerAndInstantiatePluginHostActor(id: string, descriptor: SyncDescriptor0): T { + return this._getOrCreateLocalInstance(id, descriptor); + } +} + +const Instance = new TestThreadService(); +export default Instance; \ No newline at end of file