diff --git a/src/sql/workbench/browser/parts/editor/editorStatusModeSelect.ts b/src/sql/workbench/browser/parts/editor/editorStatusModeSelect.ts index f973a7e450..a136f5c08c 100644 --- a/src/sql/workbench/browser/parts/editor/editorStatusModeSelect.ts +++ b/src/sql/workbench/browser/parts/editor/editorStatusModeSelect.ts @@ -41,7 +41,7 @@ export async function setMode(accessor: ServicesAccessor, modeSupport: IModeSupp if (newInputCreator) { // if we know how to handle the new language, tranform the input and replace the editor (e.x notebook, sql, etc) const newInput = newInputCreator.convertInput(input || activeEditor); if (newInput) { // the factory will return undefined if it doesn't know how to handle the input - await editorService.replaceEditors([{ editor: activeEditor, replacement: newInput }], activeControl.group); + await editorService.replaceEditors([{ editor: activeEditor, replacement: await newInput }], activeControl.group); } } else if (oldInputCreator) { // if we don't know handle to handle the new language but we know how to handle the current language, replace the editor with the reverted input (e.x sql -> text) await editorService.replaceEditors([{ editor: activeEditor, replacement: input }], activeControl.group); diff --git a/src/sql/workbench/contrib/editorReplacement/common/editorReplacerContribution.ts b/src/sql/workbench/contrib/editorReplacement/common/editorReplacerContribution.ts index 6514d3720c..fb83668915 100644 --- a/src/sql/workbench/contrib/editorReplacement/common/editorReplacerContribution.ts +++ b/src/sql/workbench/contrib/editorReplacement/common/editorReplacerContribution.ts @@ -16,6 +16,7 @@ import * as path from 'vs/base/common/path'; import { ILanguageAssociationRegistry, Extensions as LanguageAssociationExtensions } from 'sql/workbench/services/languageAssociation/common/languageAssociation'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { isThenable } from 'vs/base/common/async'; const languageAssociationRegistry = Registry.as(LanguageAssociationExtensions.LanguageAssociations); @@ -63,7 +64,7 @@ export class EditorReplacementContribution implements IWorkbenchContribution { editor.setMode(defaultInputCreator[0]); const newInput = defaultInputCreator[1].convertInput(editor); if (newInput) { - return { override: this.editorService.openEditor(newInput, options, group) }; + return { override: isThenable(newInput) ? newInput.then(input => this.editorService.openEditor(input, options, group)) : this.editorService.openEditor(newInput, options, group) }; } } } else { @@ -71,7 +72,7 @@ export class EditorReplacementContribution implements IWorkbenchContribution { if (inputCreator) { const newInput = inputCreator.convertInput(editor); if (newInput) { - return { override: this.editorService.openEditor(newInput, options, group) }; + return { override: isThenable(newInput) ? newInput.then(input => this.editorService.openEditor(input, options, group)) : this.editorService.openEditor(newInput, options, group) }; } } } diff --git a/src/sql/workbench/contrib/editorReplacement/test/browser/editorReplacerContribution.test.ts b/src/sql/workbench/contrib/editorReplacement/test/browser/editorReplacerContribution.test.ts index 318af3c555..48a6b0dadb 100644 --- a/src/sql/workbench/contrib/editorReplacement/test/browser/editorReplacerContribution.test.ts +++ b/src/sql/workbench/contrib/editorReplacement/test/browser/editorReplacerContribution.test.ts @@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri'; import { IOpenEditorOverrideHandler, IOpenEditorOverride, IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IDisposable, toDisposable, dispose } from 'vs/base/common/lifecycle'; import { isUndefinedOrNull } from 'vs/base/common/types'; -import { IEditorInput, EditorInput } from 'vs/workbench/common/editor'; +import { IEditorInput, EditorInput, IUntitledTextResourceInput } from 'vs/workbench/common/editor'; import { ITextEditorOptions, IEditorOptions } from 'vs/platform/editor/common/editor'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; @@ -29,9 +29,13 @@ import { UntitledQueryEditorInput } from 'sql/workbench/common/editor/query/unti import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; import { NotebookServiceStub } from 'sql/workbench/contrib/notebook/test/stubs'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService'; +import { TestQueryEditorService } from 'sql/workbench/services/queryEditor/test/common/testQueryEditorService'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; const languageAssociations = Registry.as(LanguageAssociationExtensions.LanguageAssociations); + suite('Editor Replacer Contribution', () => { let disposables: IDisposable[] = []; @@ -40,6 +44,9 @@ suite('Editor Replacer Contribution', () => { disposables.push(languageAssociations.registerLanguageAssociation(NotebookEditorInputAssociation.languages, NotebookEditorInputAssociation)); const instantiationService = workbenchInstantiationService(); instantiationService.stub(INotebookService, new NotebookServiceStub()); + const editorService = new MockEditorService(instantiationService); + instantiationService.stub(IEditorService, editorService); + instantiationService.stub(IQueryEditorService, instantiationService.createInstance(TestQueryEditorService)); instantiationService.invokeFunction(accessor => { languageAssociations.start(accessor); }); @@ -50,7 +57,8 @@ suite('Editor Replacer Contribution', () => { }); test('does proper lifecycle', () => { - const editorService = new MockEditorService(); + const instantiationService = workbenchInstantiationService(); + const editorService = new MockEditorService(instantiationService); const modeService = new TestModeService(); const contrib = new EditorReplacementContribution(editorService, modeService); assert.equal(editorService.overridenOpens.length, 1); @@ -59,8 +67,8 @@ suite('Editor Replacer Contribution', () => { }); test('does replace sql file input from uri (no mode service)', async () => { - const editorService = new MockEditorService(); const instantiationService = workbenchInstantiationService(); + const editorService = new MockEditorService(instantiationService); instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.sql'), undefined, undefined); @@ -74,8 +82,8 @@ suite('Editor Replacer Contribution', () => { }); test('does replace sql file input using input mode', async () => { - const editorService = new MockEditorService(); const instantiationService = workbenchInstantiationService(); + const editorService = new MockEditorService(instantiationService); instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.other'), undefined, 'sql'); @@ -88,9 +96,9 @@ suite('Editor Replacer Contribution', () => { contrib.dispose(); }); - test('does replace notebook file input using input mode', async () => { - const editorService = new MockEditorService(); + test('does replace notebook file input using input extension notebook', async () => { const instantiationService = workbenchInstantiationService(); + const editorService = new MockEditorService(instantiationService); instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.notebook'), undefined, undefined); @@ -103,9 +111,9 @@ suite('Editor Replacer Contribution', () => { contrib.dispose(); }); - test('does replace notebook file input using input mode', async () => { - const editorService = new MockEditorService(); + test('does replace notebook file input using input extension iynb', async () => { const instantiationService = workbenchInstantiationService(); + const editorService = new MockEditorService(instantiationService); instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.iynb'), undefined, 'notebook'); @@ -118,9 +126,9 @@ suite('Editor Replacer Contribution', () => { contrib.dispose(); }); - test('does replace notebook file input using input mode', async () => { - const editorService = new MockEditorService(); + test('does replace file input using default mode', async function () { const instantiationService = workbenchInstantiationService(); + const editorService = new MockEditorService(instantiationService); instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); const accessor = instantiationService.createInstance(ServiceAccessor); @@ -137,8 +145,8 @@ suite('Editor Replacer Contribution', () => { }); test('does not replace editors that it shouldnt', async () => { - const editorService = new MockEditorService(); const instantiationService = workbenchInstantiationService(); + const editorService = new MockEditorService(instantiationService); instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); const accessor = instantiationService.createInstance(ServiceAccessor); @@ -152,8 +160,8 @@ suite('Editor Replacer Contribution', () => { }); test('does not replace editors if it doesnt have a replacer', async () => { - const editorService = new MockEditorService(); const instantiationService = workbenchInstantiationService(); + const editorService = new MockEditorService(instantiationService); instantiationService.stub(IEditorService, editorService); const contrib = instantiationService.createInstance(EditorReplacementContribution); const accessor = instantiationService.createInstance(ServiceAccessor); @@ -167,6 +175,10 @@ suite('Editor Replacer Contribution', () => { }); class MockEditorService extends TestEditorService { + + constructor(private readonly instantiationService: IInstantiationService) { + super(); + } readonly overridenOpens: IOpenEditorOverrideHandler[] = []; overrideOpenEditor(_handler: IOpenEditorOverrideHandler): IDisposable { @@ -192,6 +204,12 @@ class MockEditorService extends TestEditorService { openEditor(_editor: any, _options?: any, _group?: any): Promise { return Promise.resolve(_editor); } + + createInput(_input: IUntitledTextResourceInput): EditorInput { + const accessor = this.instantiationService.createInstance(ServiceAccessor); + const service = accessor.untitledTextEditorService; + return this.instantiationService.createInstance(UntitledTextEditorInput, service.create()); + } } class TestModeService implements IModeService { diff --git a/src/sql/workbench/contrib/query/browser/queryInputFactory.ts b/src/sql/workbench/contrib/query/browser/queryInputFactory.ts index 41c932f4ab..524e16c7e5 100644 --- a/src/sql/workbench/contrib/query/browser/queryInputFactory.ts +++ b/src/sql/workbench/contrib/query/browser/queryInputFactory.ts @@ -21,6 +21,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { onUnexpectedError } from 'vs/base/common/errors'; import { IFileService } from 'vs/platform/files/common/files'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService'; const editorInputFactoryRegistry = Registry.as(EditorInputExtensions.EditorInputFactories); @@ -31,9 +32,37 @@ export class QueryEditorLanguageAssociation implements ILanguageAssociation { constructor(@IInstantiationService private readonly instantiationService: IInstantiationService, @IObjectExplorerService private readonly objectExplorerService: IObjectExplorerService, @IConnectionManagementService private readonly connectionManagementService: IConnectionManagementService, - @IEditorService private readonly editorService: IEditorService) { } + @IEditorService private readonly editorService: IEditorService, + @IQueryEditorService private readonly queryEditorService: IQueryEditorService) { } - convertInput(activeEditor: IEditorInput): QueryEditorInput | undefined { + async convertInput(activeEditor: IEditorInput): Promise { + const queryResultsInput = this.instantiationService.createInstance(QueryResultsInput, activeEditor.resource.toString(true)); + let queryEditorInput: QueryEditorInput; + if (activeEditor instanceof FileEditorInput) { + queryEditorInput = this.instantiationService.createInstance(FileQueryEditorInput, '', activeEditor, queryResultsInput); + } else if (activeEditor instanceof UntitledTextEditorInput) { + const content = (await activeEditor.resolve()).textEditorModel.getValue(); + queryEditorInput = (this.queryEditorService.newSqlEditor(content) as any) as UntitledQueryEditorInput; + } else { + return undefined; + } + + const profile = getCurrentGlobalConnection(this.objectExplorerService, this.connectionManagementService, this.editorService); + if (profile) { + const options: IConnectionCompletionOptions = { + params: { connectionType: ConnectionType.editor, runQueryOnCompletion: undefined, input: queryEditorInput }, + saveTheConnection: false, + showDashboard: false, + showConnectionDialogOnError: true, + showFirewallRuleOnError: true + }; + this.connectionManagementService.connect(profile, queryEditorInput.uri, options).catch(err => onUnexpectedError(err)); + } + + return queryEditorInput; + } + + syncConvertinput(activeEditor: IEditorInput): QueryEditorInput | undefined { const queryResultsInput = this.instantiationService.createInstance(QueryResultsInput, activeEditor.resource.toString(true)); let queryEditorInput: QueryEditorInput; if (activeEditor instanceof FileEditorInput) { diff --git a/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts b/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts index 5ea402a874..d26737a4c7 100644 --- a/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts +++ b/src/sql/workbench/contrib/query/test/browser/queryInputFactory.test.ts @@ -21,10 +21,11 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { UntitledQueryEditorInput } from 'sql/workbench/common/editor/query/untitledQueryEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; import { IUntitledTextEditorService } from 'vs/workbench/services/untitled/common/untitledTextEditorService'; +import { isThenable } from 'vs/base/common/async'; suite('Query Input Factory', () => { - test('query editor input is connected if global connection exists (OE)', () => { + test('sync query editor input is connected if global connection exists (OE)', () => { const editorService = new MockEditorService(); const instantiationService = workbenchInstantiationService(); const connectionManagementService = new MockConnectionManagementService(); @@ -37,7 +38,22 @@ suite('Query Input Factory', () => { assert(connectionManagementService.numberConnects === 1, 'Convert input should have called connect when active OE connection exists'); }); - test('query editor input is connected if global connection exists (Editor)', () => { + test('query editor input is connected if global connection exists (OE)', async () => { + const editorService = new MockEditorService(); + const instantiationService = workbenchInstantiationService(); + const connectionManagementService = new MockConnectionManagementService(); + instantiationService.stub(IObjectExplorerService, new MockObjectExplorerService()); + instantiationService.stub(IConnectionManagementService, connectionManagementService); + instantiationService.stub(IEditorService, editorService); + const queryEditorLanguageAssociation = instantiationService.createInstance(QueryEditorLanguageAssociation); + const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.sql'), undefined, undefined); + const response = queryEditorLanguageAssociation.convertInput(input); + assert(isThenable(response)); + await response; + assert(connectionManagementService.numberConnects === 1, 'Convert input should have called connect when active OE connection exists'); + }); + + test('sync query editor input is connected if global connection exists (Editor)', () => { const instantiationService = workbenchInstantiationService(); const editorService = new MockEditorService(instantiationService); const connectionManagementService = new MockConnectionManagementService(); @@ -50,7 +66,22 @@ suite('Query Input Factory', () => { assert(connectionManagementService.numberConnects === 1, 'Convert input should have called connect when active editor connection exists'); }); - test('query editor input is not connected if no global connection exists', () => { + test('query editor input is connected if global connection exists (Editor)', async () => { + const instantiationService = workbenchInstantiationService(); + const editorService = new MockEditorService(instantiationService); + const connectionManagementService = new MockConnectionManagementService(); + instantiationService.stub(IObjectExplorerService, new MockObjectExplorerService()); + instantiationService.stub(IConnectionManagementService, connectionManagementService); + instantiationService.stub(IEditorService, editorService); + const queryEditorLanguageAssociation = instantiationService.createInstance(QueryEditorLanguageAssociation); + const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.sql'), undefined, undefined); + const response = queryEditorLanguageAssociation.convertInput(input); + assert(isThenable(response)); + await response; + assert(connectionManagementService.numberConnects === 1, 'Convert input should have called connect when active editor connection exists'); + }); + + test('sync query editor input is not connected if no global connection exists', () => { const instantiationService = workbenchInstantiationService(); const editorService = new MockEditorService(); const connectionManagementService = new MockConnectionManagementService(); @@ -58,7 +89,21 @@ suite('Query Input Factory', () => { instantiationService.stub(IEditorService, editorService); const queryEditorLanguageAssociation = instantiationService.createInstance(QueryEditorLanguageAssociation); const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.sql'), undefined, undefined); - queryEditorLanguageAssociation.convertInput(input); + queryEditorLanguageAssociation.syncConvertinput(input); + assert(connectionManagementService.numberConnects === 0, 'Convert input should not have been called connect when no global connections exist'); + }); + + test('async query editor input is not connected if no global connection exists', async () => { + const instantiationService = workbenchInstantiationService(); + const editorService = new MockEditorService(); + const connectionManagementService = new MockConnectionManagementService(); + instantiationService.stub(IConnectionManagementService, connectionManagementService); + instantiationService.stub(IEditorService, editorService); + const queryEditorLanguageAssociation = instantiationService.createInstance(QueryEditorLanguageAssociation); + const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.sql'), undefined, undefined); + const response = queryEditorLanguageAssociation.convertInput(input); + assert(isThenable(response)); + await response; assert(connectionManagementService.numberConnects === 0, 'Convert input should not have been called connect when no global connections exist'); }); diff --git a/src/sql/workbench/services/languageAssociation/common/doHandleUpgrade.ts b/src/sql/workbench/services/languageAssociation/common/doHandleUpgrade.ts index 79eb191c43..6cbb53ab4f 100644 --- a/src/sql/workbench/services/languageAssociation/common/doHandleUpgrade.ts +++ b/src/sql/workbench/services/languageAssociation/common/doHandleUpgrade.ts @@ -20,8 +20,8 @@ export function doHandleUpgrade(editor?: EditorInput): EditorInput | undefined { editor.getPreferredMode(); } const association = languageRegistry.getAssociationForLanguage(language); - if (association) { - return association.convertInput(editor); + if (association && association.syncConvertinput) { + return association.syncConvertinput(editor); } } return editor; diff --git a/src/sql/workbench/services/languageAssociation/common/languageAssociation.ts b/src/sql/workbench/services/languageAssociation/common/languageAssociation.ts index c9ff488b07..33d75f9127 100644 --- a/src/sql/workbench/services/languageAssociation/common/languageAssociation.ts +++ b/src/sql/workbench/services/languageAssociation/common/languageAssociation.ts @@ -12,7 +12,12 @@ export type InputCreator = (servicesAccessor: ServicesAccessor, activeEditor: IE export type BaseInputCreator = (activeEditor: IEditorInput) => IEditorInput; export interface ILanguageAssociation { - convertInput(activeEditor: IEditorInput): EditorInput | undefined; + convertInput(activeEditor: IEditorInput): Promise | EditorInput | undefined; + /** + * Used for scenarios when we need to synchrounly create inputs, currently only for handling upgrades + * and planned to be removed eventually + */ + syncConvertinput?(activeEditor: IEditorInput): EditorInput | undefined; createBase(activeEditor: IEditorInput): IEditorInput; } diff --git a/src/sql/workbench/services/queryEditor/test/common/testQueryEditorService.ts b/src/sql/workbench/services/queryEditor/test/common/testQueryEditorService.ts new file mode 100644 index 0000000000..456b87e7a6 --- /dev/null +++ b/src/sql/workbench/services/queryEditor/test/common/testQueryEditorService.ts @@ -0,0 +1,31 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService'; +import { IConnectableInput } from 'sql/platform/connection/common/connectionManagement'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { UntitledQueryEditorInput } from 'sql/workbench/common/editor/query/untitledQueryEditorInput'; +import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; +import { QueryResultsInput } from 'sql/workbench/common/editor/query/queryResultsInput'; +import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; + +export class TestQueryEditorService implements IQueryEditorService { + _serviceBrand: undefined; + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IEditorService private readonly editorService: IEditorService) { + + } + + newSqlEditor(sqlContent?: string, connectionProviderName?: string, isDirty?: boolean, objectName?: string): Promise { + const base = this.editorService.createInput({ forceUntitled: true }) as UntitledTextEditorInput; + return Promise.resolve(this.instantiationService.createInstance(UntitledQueryEditorInput, '', base, new QueryResultsInput(base.resource.toString(true)))); + } + + newEditDataEditor(schemaName: string, tableName: string, queryString: string): Promise { + throw new Error('Method not implemented.'); + } +} diff --git a/src/sql/workbench/test/workbenchTestServices.ts b/src/sql/workbench/test/workbenchTestServices.ts index 8dcb5b3b1b..da9fb4a69b 100644 --- a/src/sql/workbench/test/workbenchTestServices.ts +++ b/src/sql/workbench/test/workbenchTestServices.ts @@ -10,9 +10,12 @@ import { IConnectionManagementService } from 'sql/platform/connection/common/con import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService'; import { TestObjectExplorerService } from 'sql/workbench/services/objectExplorer/test/browser/testObjectExplorerService'; import { IObjectExplorerService } from 'sql/workbench/services/objectExplorer/browser/objectExplorerService'; +import { IQueryEditorService } from 'sql/workbench/services/queryEditor/common/queryEditorService'; +import { TestQueryEditorService } from 'sql/workbench/services/queryEditor/test/common/testQueryEditorService'; export function workbenchInstantiationService(): ITestInstantiationService { const instantiationService = vsworkbenchInstantiationService(); + instantiationService.stub(IQueryEditorService, instantiationService.createInstance(TestQueryEditorService)); instantiationService.stub(IConnectionManagementService, new TestConnectionManagementService()); instantiationService.stub(IQueryModelService, new TestQueryModelService()); instantiationService.stub(IObjectExplorerService, new TestObjectExplorerService());