diff --git a/src/sql/platform/connection/test/common/testConnectionManagementService.ts b/src/sql/platform/connection/test/common/testConnectionManagementService.ts index a781aafbf7..590340ac14 100644 --- a/src/sql/platform/connection/test/common/testConnectionManagementService.ts +++ b/src/sql/platform/connection/test/common/testConnectionManagementService.ts @@ -10,7 +10,7 @@ import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { ConnectionProfile } from 'sql/platform/connection/common/connectionProfile'; import { ConnectionManagementInfo } from 'sql/platform/connection/common/connectionManagementInfo'; import * as azdata from 'azdata'; -import { Event, Emitter } from 'vs/base/common/event'; +import { Event } from 'vs/base/common/event'; import { ConnectionProviderProperties } from 'sql/platform/capabilities/common/capabilitiesService'; // Test stubs for commonly used objects @@ -23,13 +23,11 @@ export class TestConnectionManagementService implements IConnectionManagementSer onLanguageFlavorChanged = undefined; public get onConnect(): Event { - let conEvent = new Emitter(); - return conEvent.event; + return Event.None; } public get onDisconnect(): Event { - let conEvent = new Emitter(); - return conEvent.event; + return Event.None; } public get providerNameToDisplayNameMap(): { [providerDisplayName: string]: string } { diff --git a/src/sql/platform/query/test/common/testQueryModelService.ts b/src/sql/platform/query/test/common/testQueryModelService.ts index f8fab5b9ce..5bddd69f9e 100644 --- a/src/sql/platform/query/test/common/testQueryModelService.ts +++ b/src/sql/platform/query/test/common/testQueryModelService.ts @@ -72,11 +72,11 @@ export class TestQueryModelService implements IQueryModelService { throw new Error('Method not implemented.'); } get onRunQueryStart(): Event { - throw new Error('Method not implemented.'); + return Event.None; } get onRunQueryComplete(): Event { - throw new Error('Method not implemented.'); + return Event.None; } get onQueryEvent(): Event { diff --git a/src/sql/workbench/browser/parts/editor/editorStatusModeSelect.ts b/src/sql/workbench/browser/parts/editor/editorStatusModeSelect.ts index 747048dd6f..54c3f1ec39 100644 --- a/src/sql/workbench/browser/parts/editor/editorStatusModeSelect.ts +++ b/src/sql/workbench/browser/parts/editor/editorStatusModeSelect.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation'; import { IModeSupport, IEditorInput } from 'vs/workbench/common/editor'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; @@ -20,14 +20,13 @@ const languageAssociationRegistry = Registry.as(La */ export async function setMode(accessor: ServicesAccessor, modeSupport: IModeSupport, activeEditor: IEditorInput, language: string): Promise { const editorService = accessor.get(IEditorService); - const instantiationService = accessor.get(IInstantiationService); const activeWidget = getCodeEditor(editorService.activeTextEditorWidget); const activeControl = editorService.activeControl; const textModel = activeWidget.getModel(); const oldLanguage = textModel.getLanguageIdentifier().language; if (language !== oldLanguage) { - const oldInputCreator = languageAssociationRegistry.getAssociations().filter(e => e.language === oldLanguage)[0]; // who knows how to handle the current language - const newInputCreator = languageAssociationRegistry.getAssociations().filter(e => e.language === language)[0]; // who knows how to handle the requested language + const oldInputCreator = languageAssociationRegistry.getAssociationForLanguage(oldLanguage); // who knows how to handle the current language + const newInputCreator = languageAssociationRegistry.getAssociationForLanguage(language); // who knows how to handle the requested language if ((oldInputCreator || newInputCreator) && activeEditor.isDirty()) { // theres some issues with changing the language on a dirty file with one of our editors (we should look into this) const notificationService = accessor.get(INotificationService); notificationService.error(localize('languageChangeUnsupported', "Changing editor types on unsaved files is unsupported")); @@ -36,11 +35,11 @@ export async function setMode(accessor: ServicesAccessor, modeSupport: IModeSupp modeSupport.setMode(language); let input: IEditorInput; if (oldInputCreator) { // only transform the input if we have someone who knows how to deal with it (e.x QueryInput -> UntitledInput, etc) - input = oldInputCreator.baseInputCreator(activeEditor); + input = oldInputCreator.createBase(activeEditor); } 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 = instantiationService.invokeFunction(newInputCreator.creator, input || activeEditor); + 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); } diff --git a/src/sql/workbench/common/editorReplacerContribution.ts b/src/sql/workbench/common/editorReplacerContribution.ts index 319825e7b1..7130609e81 100644 --- a/src/sql/workbench/common/editorReplacerContribution.ts +++ b/src/sql/workbench/common/editorReplacerContribution.ts @@ -11,7 +11,6 @@ import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/ed import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IModeService } from 'vs/editor/common/services/modeService'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Registry } from 'vs/platform/registry/common/platform'; import * as path from 'vs/base/common/path'; @@ -25,7 +24,6 @@ export class EditorReplacementContribution implements IWorkbenchContribution { constructor( @IEditorService private readonly editorService: IEditorService, - @IInstantiationService private readonly instantiationService: IInstantiationService, @IModeService private readonly modeService: IModeService ) { this.editorOpeningListener = this.editorService.overrideOpenEditor((editor, options, group) => this.onEditorOpening(editor, options, group)); @@ -48,8 +46,6 @@ export class EditorReplacementContribution implements IWorkbenchContribution { language = editor.getPreferredMode(); } else if (editor instanceof UntitledTextEditorInput) { language = editor.getMode(); - } else { - return undefined; } if (!language) { // in the case the input doesn't have a preferred mode set we will attempt to guess the mode from the file path @@ -62,18 +58,18 @@ export class EditorReplacementContribution implements IWorkbenchContribution { } if (!language) { - const defaultInputCreator = languageAssociationRegistry.getAssociations().filter(e => e.isDefault)[0]; + const defaultInputCreator = languageAssociationRegistry.defaultAssociation; if (defaultInputCreator) { - editor.setMode(defaultInputCreator.language); - const newInput = this.instantiationService.invokeFunction(defaultInputCreator.creator, editor); + editor.setMode(defaultInputCreator[0]); + const newInput = defaultInputCreator[1].convertInput(editor); if (newInput) { return { override: this.editorService.openEditor(newInput, options, group) }; } } } else { - const inputCreator = languageAssociationRegistry.getAssociations().filter(e => e.language === language)[0]; + const inputCreator = languageAssociationRegistry.getAssociationForLanguage(language); if (inputCreator) { - const newInput = this.instantiationService.invokeFunction(inputCreator.creator, editor); + const newInput = inputCreator.convertInput(editor); if (newInput) { return { override: this.editorService.openEditor(newInput, options, group) }; } diff --git a/src/sql/workbench/common/languageAssociation.ts b/src/sql/workbench/common/languageAssociation.ts index 99a382c388..cdc8b48489 100644 --- a/src/sql/workbench/common/languageAssociation.ts +++ b/src/sql/workbench/common/languageAssociation.ts @@ -5,29 +5,75 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { IEditorInput, EditorInput } from 'vs/workbench/common/editor'; -import { ServicesAccessor, IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { find } from 'vs/base/common/arrays'; +import { ServicesAccessor, IInstantiationService, BrandedService } from 'vs/platform/instantiation/common/instantiation'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { getCodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; export type InputCreator = (servicesAccessor: ServicesAccessor, activeEditor: IEditorInput) => EditorInput | undefined; export type BaseInputCreator = (activeEditor: IEditorInput) => IEditorInput; +export interface ILanguageAssociation { + convertInput(activeEditor: IEditorInput): EditorInput | undefined; + createBase(activeEditor: IEditorInput): IEditorInput; +} + +type ILanguageAssociationSignature = new (...services: Services) => ILanguageAssociation; + export interface ILanguageAssociationRegistry { - registerLanguageAssociation(language: string, creator: InputCreator, baseInputCreator: BaseInputCreator, isDefault?: boolean): void; - getAssociations(): Array<{ language: string, creator: InputCreator, baseInputCreator: BaseInputCreator, isDefault: boolean }>; + registerLanguageAssociation(languages: string[], contribution: ILanguageAssociationSignature, isDefault?: boolean): IDisposable; + getAssociationForLanguage(language: string): ILanguageAssociation; + readonly defaultAssociation: [string, ILanguageAssociation]; + + /** + * Starts the registry by providing the required services. + */ + start(accessor: ServicesAccessor): void; } const languageAssociationRegistery = new class implements ILanguageAssociationRegistry { - private associations = new Array<{ language: string, creator: InputCreator, baseInputCreator: BaseInputCreator, isDefault: boolean }>(); + private associationsInstances = new Map(); + private associationContructors = new Map>(); + private defaultAssociationsInstance?: [string, ILanguageAssociation]; + private defaultAssociationsConstructor?: [string, ILanguageAssociationSignature]; - registerLanguageAssociation(language: string, creator: InputCreator, baseInputCreator: BaseInputCreator, isDefault: boolean = false): void { - this.associations.push({ language, creator, baseInputCreator, isDefault }); + start(accessor: ServicesAccessor): void { + const instantiationService = accessor.get(IInstantiationService); + + for (const [language, ctor] of this.associationContructors) { + const instance = instantiationService.createInstance(ctor); + this.associationsInstances.set(language, instance); + } + + if (this.defaultAssociationsConstructor) { + this.defaultAssociationsInstance = [this.defaultAssociationsConstructor[0], instantiationService.createInstance(this.defaultAssociationsConstructor[1])]; + } } - getAssociations(): Array<{ language: string, creator: InputCreator, baseInputCreator: BaseInputCreator, isDefault: boolean }> { - return this.associations.slice(); + registerLanguageAssociation(languages: string[], contribution: ILanguageAssociationSignature, isDefault?: boolean): IDisposable { + for (const language of languages) { + this.associationContructors.set(language, contribution); + } + + if (isDefault) { + this.defaultAssociationsConstructor = [languages[0], contribution]; + } + + return toDisposable(() => { + for (const language of languages) { + this.associationContructors.delete(language); + this.associationsInstances.delete(language); + } + }); + } + + getAssociationForLanguage(language: string): ILanguageAssociation | undefined { + return this.associationsInstances.get(language); + } + + get defaultAssociation(): [string, ILanguageAssociation] | undefined { + return this.defaultAssociationsInstance; } }; @@ -37,15 +83,14 @@ export const Extensions = { Registry.add(Extensions.LanguageAssociations, languageAssociationRegistery); -export function doHandleUpgrade(accessor: ServicesAccessor, editor: EditorInput): EditorInput { +export function doHandleUpgrade(editor: EditorInput): EditorInput { if (editor instanceof UntitledTextEditorInput || editor instanceof FileEditorInput) { - const instantiationService = accessor.get(IInstantiationService); const activeWidget = getCodeEditor(editor); const textModel = activeWidget.getModel(); const oldLanguage = textModel.getLanguageIdentifier().language; - const association = find(languageAssociationRegistery.getAssociations(), l => l.language === oldLanguage); + const association = languageAssociationRegistery.getAssociationForLanguage(oldLanguage); if (association) { - return instantiationService.invokeFunction(association.creator, editor); + return association.convertInput(editor); } } return editor; diff --git a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts index e64aa2b642..8ff9e44d56 100644 --- a/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/sql/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -6,18 +6,16 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, IEditorRegistry, Extensions as EditorExtensions } from 'vs/workbench/browser/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { localize } from 'vs/nls'; import { IEditorInputFactoryRegistry, Extensions as EditorInputFactoryExtensions } from 'vs/workbench/common/editor'; import { ILanguageAssociationRegistry, Extensions as LanguageAssociationExtensions } from 'sql/workbench/common/languageAssociation'; import { UntitledNotebookInput } from 'sql/workbench/contrib/notebook/common/models/untitledNotebookInput'; import { FileNotebookInput } from 'sql/workbench/contrib/notebook/common/models/fileNotebookInput'; -import { FileNoteBookEditorInputFactory, UntitledNoteBookEditorInputFactory } from 'sql/workbench/contrib/notebook/common/models/nodebookInputFactory'; +import { FileNoteBookEditorInputFactory, UntitledNoteBookEditorInputFactory, NotebookEditorInputAssociation } from 'sql/workbench/contrib/notebook/common/models/nodebookInputFactory'; import { IWorkbenchActionRegistry, Extensions as WorkbenchActionsExtensions } from 'vs/workbench/common/actions'; import { SyncActionDescriptor, registerAction, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; import { NotebookEditor } from 'sql/workbench/contrib/notebook/browser/notebookEditor'; import { NewNotebookAction } from 'sql/workbench/contrib/notebook/browser/notebookActions'; import { KeyMod } from 'vs/editor/common/standalone/standaloneBase'; @@ -45,7 +43,6 @@ import { IHostService } from 'vs/workbench/services/host/browser/host'; import { MarkdownOutputComponent } from 'sql/workbench/contrib/notebook/browser/outputs/markdownOutput.component'; import { registerCellComponent } from 'sql/platform/notebooks/common/outputRegistry'; import { TextCellComponent } from 'sql/workbench/contrib/notebook/browser/cellViews/textCell.component'; -import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; Registry.as(EditorInputFactoryExtensions.EditorInputFactories) .registerEditorInputFactory(FileNotebookInput.ID, FileNoteBookEditorInputFactory); @@ -54,16 +51,7 @@ Registry.as(EditorInputFactoryExtensions.EditorInpu .registerEditorInputFactory(UntitledNotebookInput.ID, UntitledNoteBookEditorInputFactory); Registry.as(LanguageAssociationExtensions.LanguageAssociations) - .registerLanguageAssociation('notebook', (accessor, editor) => { - const instantiationService = accessor.get(IInstantiationService); - if (editor instanceof FileEditorInput) { - return instantiationService.createInstance(FileNotebookInput, editor.getName(), editor.getResource(), editor); - } else if (editor instanceof UntitledTextEditorInput) { - return instantiationService.createInstance(UntitledNotebookInput, editor.getName(), editor.getResource(), editor); - } else { - return undefined; - } - }, (editor: NotebookInput) => editor.textInput); + .registerLanguageAssociation(NotebookEditorInputAssociation.languages, NotebookEditorInputAssociation); Registry.as(EditorExtensions.Editors) .registerEditor(new EditorDescriptor(NotebookEditor, NotebookEditor.ID, localize('notebookEditor.name', "Notebook Editor")), [new SyncDescriptor(UntitledNotebookInput), new SyncDescriptor(FileNotebookInput)]); diff --git a/src/sql/workbench/contrib/notebook/common/models/nodebookInputFactory.ts b/src/sql/workbench/contrib/notebook/common/models/nodebookInputFactory.ts index 638791523b..06bc9e64d6 100644 --- a/src/sql/workbench/contrib/notebook/common/models/nodebookInputFactory.ts +++ b/src/sql/workbench/contrib/notebook/common/models/nodebookInputFactory.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; +import { IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, IEditorInput } from 'vs/workbench/common/editor'; import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FILE_EDITOR_INPUT_ID } from 'vs/workbench/contrib/files/common/files'; @@ -11,9 +11,31 @@ import { FileNotebookInput } from 'sql/workbench/contrib/notebook/common/models/ import { UntitledNotebookInput } from 'sql/workbench/contrib/notebook/common/models/untitledNotebookInput'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; +import { ILanguageAssociation } from 'sql/workbench/common/languageAssociation'; +import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; const editorInputFactoryRegistry = Registry.as(EditorInputExtensions.EditorInputFactories); +export class NotebookEditorInputAssociation implements ILanguageAssociation { + static readonly languages = ['notebook']; + + constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { } + + convertInput(activeEditor: IEditorInput): NotebookInput { + if (activeEditor instanceof FileEditorInput) { + return this.instantiationService.createInstance(FileNotebookInput, activeEditor.getName(), activeEditor.getResource(), activeEditor); + } else if (activeEditor instanceof UntitledTextEditorInput) { + return this.instantiationService.createInstance(UntitledNotebookInput, activeEditor.getName(), activeEditor.getResource(), activeEditor); + } else { + return undefined; + } + } + + createBase(activeEditor: NotebookInput): IEditorInput { + return activeEditor.textInput; + } +} + export class FileNoteBookEditorInputFactory implements IEditorInputFactory { serialize(editorInput: FileNotebookInput): string { const factory = editorInputFactoryRegistry.getEditorInputFactory(FILE_EDITOR_INPUT_ID); diff --git a/src/sql/workbench/contrib/notebook/test/stubs.ts b/src/sql/workbench/contrib/notebook/test/stubs.ts index f83413d694..5751fd0c47 100644 --- a/src/sql/workbench/contrib/notebook/test/stubs.ts +++ b/src/sql/workbench/contrib/notebook/test/stubs.ts @@ -220,7 +220,7 @@ export class NotebookServiceStub implements INotebookService { throw new Error('Method not implemented.'); } getProvidersForFileType(fileType: string): string[] { - throw new Error('Method not implemented.'); + return []; } getStandardKernelsForProvider(provider: string): nb.IStandardKernel[] { throw new Error('Method not implemented.'); diff --git a/src/sql/workbench/contrib/query/browser/query.contribution.ts b/src/sql/workbench/contrib/query/browser/query.contribution.ts index 9e0025ef1b..fea8c4cdbb 100644 --- a/src/sql/workbench/contrib/query/browser/query.contribution.ts +++ b/src/sql/workbench/contrib/query/browser/query.contribution.ts @@ -34,18 +34,15 @@ import { TimeElapsedStatusBarContributions, RowCountStatusBarContributions, Quer import { SqlFlavorStatusbarItem, ChangeFlavorAction } from 'sql/workbench/contrib/query/browser/flavorStatus'; import { IEditorInputFactoryRegistry, Extensions as EditorInputFactoryExtensions } from 'vs/workbench/common/editor'; import { FileQueryEditorInput } from 'sql/workbench/contrib/query/common/fileQueryEditorInput'; -import { FileQueryEditorInputFactory, UntitledQueryEditorInputFactory } from 'sql/workbench/contrib/query/common/queryInputFactory'; +import { FileQueryEditorInputFactory, UntitledQueryEditorInputFactory, QueryEditorLanguageAssociation } from 'sql/workbench/contrib/query/common/queryInputFactory'; import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/untitledQueryEditorInput'; import { ILanguageAssociationRegistry, Extensions as LanguageAssociationExtensions } from 'sql/workbench/common/languageAssociation'; -import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; -import { QueryEditorInput } from 'sql/workbench/contrib/query/common/queryEditorInput'; import { NewQueryTask, OE_NEW_QUERY_ACTION_ID, DE_NEW_QUERY_COMMAND_ID } from 'sql/workbench/contrib/query/browser/queryActions'; import { TreeNodeContextKey } from 'sql/workbench/contrib/objectExplorer/common/treeNodeContextKey'; import { MssqlNodeContext } from 'sql/workbench/contrib/dataExplorer/browser/mssqlNodeContext'; import { CommandsRegistry, ICommandService } from 'vs/platform/commands/common/commands'; import { ManageActionContext } from 'sql/workbench/browser/actions'; import { ItemContextKey } from 'sql/workbench/contrib/dashboard/browser/widgets/explorer/explorerTreeContext'; -import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; export const QueryEditorVisibleCondition = ContextKeyExpr.has(queryContext.queryEditorVisibleId); export const ResultsGridFocusCondition = ContextKeyExpr.and(ContextKeyExpr.has(queryContext.resultsVisibleId), ContextKeyExpr.has(queryContext.resultsGridFocussedId)); @@ -58,17 +55,7 @@ Registry.as(EditorInputFactoryExtensions.EditorInpu .registerEditorInputFactory(UntitledQueryEditorInput.ID, UntitledQueryEditorInputFactory); Registry.as(LanguageAssociationExtensions.LanguageAssociations) - .registerLanguageAssociation('sql', (accessor, editor) => { - const instantiationService = accessor.get(IInstantiationService); - const queryResultsInput = instantiationService.createInstance(QueryResultsInput, editor.getResource().toString(true)); - if (editor instanceof FileEditorInput) { - return instantiationService.createInstance(FileQueryEditorInput, '', editor, queryResultsInput); - } else if (editor instanceof UntitledTextEditorInput) { - return instantiationService.createInstance(UntitledQueryEditorInput, '', editor, queryResultsInput); - } else { - return undefined; - } - }, (editor: QueryEditorInput) => editor.text, true); + .registerLanguageAssociation(QueryEditorLanguageAssociation.languages, QueryEditorLanguageAssociation, QueryEditorLanguageAssociation.isDefault); Registry.as(EditorExtensions.Editors) .registerEditor(new EditorDescriptor(QueryResultsEditor, QueryResultsEditor.ID, localize('queryResultsEditor.name', "Query Results")), [new SyncDescriptor(QueryResultsInput)]); diff --git a/src/sql/workbench/contrib/query/common/queryInputFactory.ts b/src/sql/workbench/contrib/query/common/queryInputFactory.ts index 10df6b9827..a9e18b7668 100644 --- a/src/sql/workbench/contrib/query/common/queryInputFactory.ts +++ b/src/sql/workbench/contrib/query/common/queryInputFactory.ts @@ -3,7 +3,7 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions } from 'vs/workbench/common/editor'; +import { IEditorInputFactory, IEditorInputFactoryRegistry, Extensions as EditorInputExtensions, IEditorInput } from 'vs/workbench/common/editor'; import { Registry } from 'vs/platform/registry/common/platform'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { QueryResultsInput } from 'sql/workbench/contrib/query/common/queryResultsInput'; @@ -12,9 +12,33 @@ import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/unt import { FileQueryEditorInput } from 'sql/workbench/contrib/query/common/fileQueryEditorInput'; import { FileEditorInput } from 'vs/workbench/contrib/files/common/editors/fileEditorInput'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; +import { ILanguageAssociation } from 'sql/workbench/common/languageAssociation'; +import { QueryEditorInput } from 'sql/workbench/contrib/query/common/queryEditorInput'; const editorInputFactoryRegistry = Registry.as(EditorInputExtensions.EditorInputFactories); +export class QueryEditorLanguageAssociation implements ILanguageAssociation { + static readonly isDefault = true; + static readonly languages = ['sql']; + + constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { } + + convertInput(activeEditor: IEditorInput): QueryEditorInput { + const queryResultsInput = this.instantiationService.createInstance(QueryResultsInput, activeEditor.getResource().toString(true)); + if (activeEditor instanceof FileEditorInput) { + return this.instantiationService.createInstance(FileQueryEditorInput, '', activeEditor, queryResultsInput); + } else if (activeEditor instanceof UntitledTextEditorInput) { + return this.instantiationService.createInstance(UntitledQueryEditorInput, '', activeEditor, queryResultsInput); + } else { + return undefined; + } + } + + createBase(activeEditor: QueryEditorInput): IEditorInput { + return activeEditor.text; + } +} + export class FileQueryEditorInputFactory implements IEditorInputFactory { serialize(editorInput: FileQueryEditorInput): string { const factory = editorInputFactoryRegistry.getEditorInputFactory(FILE_EDITOR_INPUT_ID); diff --git a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts index 8ae45e8d7b..185b1ec1a1 100644 --- a/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts +++ b/src/sql/workbench/contrib/queryPlan/browser/queryPlan.contribution.ts @@ -3,13 +3,12 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { QueryPlanInput } from 'sql/workbench/contrib/queryPlan/common/queryPlanInput'; +import { QueryPlanInput, QueryPlanConverter } from 'sql/workbench/contrib/queryPlan/common/queryPlanInput'; import { EditorDescriptor, IEditorRegistry, Extensions } from 'vs/workbench/browser/editor'; import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { QueryPlanEditor } from 'sql/workbench/contrib/queryPlan/browser/queryPlanEditor'; import { ILanguageAssociationRegistry, Extensions as LanguageAssociationExtensions } from 'sql/workbench/common/languageAssociation'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; // Query Plan editor registration @@ -23,7 +22,4 @@ Registry.as(Extensions.Editors) .registerEditor(queryPlanEditorDescriptor, [new SyncDescriptor(QueryPlanInput)]); Registry.as(LanguageAssociationExtensions.LanguageAssociations) - .registerLanguageAssociation('sqlplan', (accessor, editor) => { - const instantiationService = accessor.get(IInstantiationService); - return instantiationService.createInstance(QueryPlanInput, editor.getResource()); - }, (editor: QueryPlanInput) => undefined); + .registerLanguageAssociation(QueryPlanConverter.languages, QueryPlanConverter); diff --git a/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts b/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts index 6334a29c52..14daf33dca 100644 --- a/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts +++ b/src/sql/workbench/contrib/queryPlan/common/queryPlanInput.ts @@ -3,11 +3,27 @@ * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { EditorInput, EditorModel } from 'vs/workbench/common/editor'; +import { EditorInput, EditorModel, IEditorInput } from 'vs/workbench/common/editor'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { IFileService } from 'vs/platform/files/common/files'; import { URI } from 'vs/base/common/uri'; import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { ILanguageAssociation } from 'sql/workbench/common/languageAssociation'; + +export class QueryPlanConverter implements ILanguageAssociation { + static readonly languages = ['sqlplan']; + + constructor(@IInstantiationService private instantiationService: IInstantiationService) { } + + convertInput(activeEditor: IEditorInput): QueryPlanInput { + return this.instantiationService.createInstance(QueryPlanInput, activeEditor.getResource()); + } + + createBase(activeEditor: QueryPlanInput): IEditorInput { + return undefined; + } +} export class QueryPlanInput extends EditorInput { diff --git a/src/sql/workbench/test/common/editorReplacerContribution.test.ts b/src/sql/workbench/test/common/editorReplacerContribution.test.ts new file mode 100644 index 0000000000..076c61eab6 --- /dev/null +++ b/src/sql/workbench/test/common/editorReplacerContribution.test.ts @@ -0,0 +1,256 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { EditorReplacementContribution } from 'sql/workbench/common/editorReplacerContribution'; +import { TestEditorService } from 'vs/workbench/test/workbenchTestServices'; +import { IModeService, ILanguageSelection } from 'vs/editor/common/services/modeService'; +import { Event } from 'vs/base/common/event'; +import { IMode, LanguageId, LanguageIdentifier } from 'vs/editor/common/modes'; +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 { 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'; +import { QueryEditorInput } from 'sql/workbench/contrib/query/common/queryEditorInput'; +import { Registry } from 'vs/platform/registry/common/platform'; +import { ILanguageAssociationRegistry, Extensions as LanguageAssociationExtensions } from 'sql/workbench/common/languageAssociation'; +import { QueryEditorLanguageAssociation } from 'sql/workbench/contrib/query/common/queryInputFactory'; +import { workbenchInstantiationService } from 'sql/workbench/test/workbenchTestServices'; +import { NotebookEditorInputAssociation } from 'sql/workbench/contrib/notebook/common/models/nodebookInputFactory'; +import { NotebookInput } from 'sql/workbench/contrib/notebook/browser/models/notebookInput'; +import { UntitledTextEditorInput } from 'vs/workbench/common/editor/untitledTextEditorInput'; +import { UntitledQueryEditorInput } from 'sql/workbench/contrib/query/common/untitledQueryEditorInput'; +import { INotebookService } from 'sql/workbench/services/notebook/browser/notebookService'; +import { NotebookServiceStub } from 'sql/workbench/contrib/notebook/test/stubs'; + +const languageAssociations = Registry.as(LanguageAssociationExtensions.LanguageAssociations); + +suite('Editor Replacer Contribution', () => { + let disposables: IDisposable[] = []; + + setup(() => { + disposables.push(languageAssociations.registerLanguageAssociation(QueryEditorLanguageAssociation.languages, QueryEditorLanguageAssociation, QueryEditorLanguageAssociation.isDefault)); + disposables.push(languageAssociations.registerLanguageAssociation(NotebookEditorInputAssociation.languages, NotebookEditorInputAssociation)); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(INotebookService, new NotebookServiceStub()); + instantiationService.invokeFunction(accessor => { + languageAssociations.start(accessor); + }); + }); + + teardown(() => { + disposables = dispose(disposables); + }); + + test('does proper lifecycle', () => { + const editorService = new MockEditorService(); + const modeService = new TestModeService(); + const contrib = new EditorReplacementContribution(editorService, modeService); + assert.equal(editorService.overridenOpens.length, 1); + contrib.dispose(); + assert.equal(editorService.overridenOpens.length, 0); + }); + + test('does replace sql file input from uri (no mode service)', async () => { + const editorService = new MockEditorService(); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(IEditorService, editorService); + const contrib = instantiationService.createInstance(EditorReplacementContribution); + const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.sql'), undefined, undefined); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + assert(response?.override); + const newinput = (await response.override) as EditorInput; // our test service returns this so we are fine to cast this + + assert(newinput instanceof QueryEditorInput); + + contrib.dispose(); + }); + + test('does replace sql file input using input mode', async () => { + const editorService = new MockEditorService(); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(IEditorService, editorService); + const contrib = instantiationService.createInstance(EditorReplacementContribution); + const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.other'), undefined, 'sql'); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + assert(response?.override); + const newinput = (await response.override) as EditorInput; // our test service returns this so we are fine to cast this + + assert(newinput instanceof QueryEditorInput); + + contrib.dispose(); + }); + + test('does replace notebook file input using input mode', async () => { + const editorService = new MockEditorService(); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(IEditorService, editorService); + const contrib = instantiationService.createInstance(EditorReplacementContribution); + const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.notebook'), undefined, undefined); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + assert(response?.override); + const newinput = (await response.override) as EditorInput; // our test service returns this so we are fine to cast this + + assert(newinput instanceof NotebookInput); + + contrib.dispose(); + }); + + test('does replace notebook file input using input mode', async () => { + const editorService = new MockEditorService(); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(IEditorService, editorService); + const contrib = instantiationService.createInstance(EditorReplacementContribution); + const input = instantiationService.createInstance(FileEditorInput, URI.file('/test/file.iynb'), undefined, 'notebook'); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + assert(response?.override); + const newinput = (await response.override) as EditorInput; // our test service returns this so we are fine to cast this + + assert(newinput instanceof NotebookInput); + + contrib.dispose(); + }); + + test('does replace notebook file input using input mode', async () => { + const editorService = new MockEditorService(); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(IEditorService, editorService); + const contrib = instantiationService.createInstance(EditorReplacementContribution); + const input = instantiationService.createInstance(UntitledTextEditorInput, URI.file('/test/file'), false, undefined, undefined, undefined); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + assert(response?.override); + const newinput = (await response.override) as EditorInput; // our test service returns this so we are fine to cast this + + assert(newinput instanceof QueryEditorInput); + + contrib.dispose(); + }); + + test('does not replace editors that it shouldnt', async () => { + const editorService = new MockEditorService(); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(IEditorService, editorService); + const contrib = instantiationService.createInstance(EditorReplacementContribution); + const untitled = instantiationService.createInstance(UntitledTextEditorInput, URI.file('/test/file'), false, undefined, undefined, undefined); + const input = instantiationService.createInstance(UntitledQueryEditorInput, '', untitled, undefined); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + assert(response === undefined); + + contrib.dispose(); + }); + + test('does not replace editors if it doesnt have a replacer', async () => { + const editorService = new MockEditorService(); + const instantiationService = workbenchInstantiationService(); + instantiationService.stub(IEditorService, editorService); + const contrib = instantiationService.createInstance(EditorReplacementContribution); + const input = instantiationService.createInstance(UntitledTextEditorInput, URI.file('/test/file.unknown'), false, undefined, undefined, undefined); + const response = editorService.fireOpenEditor(input, undefined, undefined as IEditorGroup); + assert(response === undefined); + + contrib.dispose(); + }); +}); + +class MockEditorService extends TestEditorService { + readonly overridenOpens: IOpenEditorOverrideHandler[] = []; + + overrideOpenEditor(_handler: IOpenEditorOverrideHandler): IDisposable { + this.overridenOpens.push(_handler); + return toDisposable(() => { + const index = this.overridenOpens.findIndex(v => v === _handler); + if (!isUndefinedOrNull(index)) { + this.overridenOpens.splice(index, 1); + } + }); + } + + fireOpenEditor(editor: IEditorInput, options: IEditorOptions | ITextEditorOptions | undefined, group: IEditorGroup) { + for (const handler of this.overridenOpens) { + let response: IOpenEditorOverride | undefined; + if (response = handler(editor, options, group)) { + return response; + } + } + return undefined; + } + + openEditor(_editor: any, _options?: any, _group?: any): Promise { + return Promise.resolve(_editor); + } +} + +class TestModeService implements IModeService { + _serviceBrand: undefined; + onDidCreateMode: Event; + + isRegisteredMode(mimetypeOrModeId: string): boolean { + throw new Error('Method not implemented.'); + } + + getRegisteredModes(): string[] { + throw new Error('Method not implemented.'); + } + + getRegisteredLanguageNames(): string[] { + throw new Error('Method not implemented.'); + } + + getExtensions(alias: string): string[] { + throw new Error('Method not implemented.'); + } + + getFilenames(alias: string): string[] { + throw new Error('Method not implemented.'); + } + + getMimeForMode(modeId: string): string { + throw new Error('Method not implemented.'); + } + + getLanguageName(modeId: string): string { + throw new Error('Method not implemented.'); + } + + getModeIdForLanguageName(alias: string): string { + throw new Error('Method not implemented.'); + } + + getModeIdByFilepathOrFirstLine(resource: URI, firstLine?: string): string { + throw new Error('Method not implemented.'); + } + + getModeId(commaSeparatedMimetypesOrCommaSeparatedIds: string): string { + throw new Error('Method not implemented.'); + } + + getLanguageIdentifier(modeId: string | LanguageId): LanguageIdentifier { + throw new Error('Method not implemented.'); + } + + getConfigurationFiles(modeId: string): URI[] { + throw new Error('Method not implemented.'); + } + + create(commaSeparatedMimetypesOrCommaSeparatedIds: string): ILanguageSelection { + throw new Error('Method not implemented.'); + } + + createByLanguageName(languageName: string): ILanguageSelection { + throw new Error('Method not implemented.'); + } + + createByFilepathOrFirstLine(rsource: URI, firstLine?: string): ILanguageSelection { + throw new Error('Method not implemented.'); + } + + triggerMode(commaSeparatedMimetypesOrCommaSeparatedIds: string): void { + throw new Error('Method not implemented.'); + } +} diff --git a/src/sql/workbench/test/workbenchTestServices.ts b/src/sql/workbench/test/workbenchTestServices.ts new file mode 100644 index 0000000000..dc82516cd0 --- /dev/null +++ b/src/sql/workbench/test/workbenchTestServices.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the Source EULA. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { ITestInstantiationService, workbenchInstantiationService as vsworkbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices'; +import { IQueryModelService } from 'sql/platform/query/common/queryModel'; +import { TestQueryModelService } from 'sql/platform/query/test/common/testQueryModelService'; +import { IConnectionManagementService } from 'sql/platform/connection/common/connectionManagement'; +import { TestConnectionManagementService } from 'sql/platform/connection/test/common/testConnectionManagementService'; + +export function workbenchInstantiationService(): ITestInstantiationService { + const instantiationService = vsworkbenchInstantiationService(); + instantiationService.stub(IConnectionManagementService, new TestConnectionManagementService()); + instantiationService.stub(IQueryModelService, new TestQueryModelService()); + return instantiationService; +} diff --git a/src/vs/workbench/browser/workbench.ts b/src/vs/workbench/browser/workbench.ts index 9e9165c1be..1f75227ac0 100644 --- a/src/vs/workbench/browser/workbench.ts +++ b/src/vs/workbench/browser/workbench.ts @@ -45,6 +45,7 @@ import { coalesce } from 'vs/base/common/arrays'; import { InstantiationService } from 'vs/platform/instantiation/common/instantiationService'; import { Layout } from 'vs/workbench/browser/layout'; import { IHostService } from 'vs/workbench/services/host/browser/host'; +import { ILanguageAssociationRegistry, Extensions as LanguageExtensions } from 'sql/workbench/common/languageAssociation'; export class Workbench extends Layout { @@ -221,6 +222,7 @@ export class Workbench extends Layout { Registry.as(ActionBarExtensions.Actionbar).start(accessor); Registry.as(WorkbenchExtensions.Workbench).start(accessor); Registry.as(EditorExtensions.EditorInputFactories).start(accessor); + Registry.as(LanguageExtensions.LanguageAssociations).start(accessor); } private registerListeners( diff --git a/src/vs/workbench/common/editor/editorGroup.ts b/src/vs/workbench/common/editor/editorGroup.ts index 73573537eb..debe776153 100644 --- a/src/vs/workbench/common/editor/editorGroup.ts +++ b/src/vs/workbench/common/editor/editorGroup.ts @@ -676,7 +676,7 @@ export class EditorGroup extends Disposable { this.editors = coalesce(data.editors.map(e => { const factory = registry.getEditorInputFactory(e.id); if (factory) { - const editor = this.instantiationService.invokeFunction(doHandleUpgrade, factory.deserialize(this.instantiationService, e.value)); // {{SQL CARBON EDIT}} handle upgrade path to new serialization + const editor = doHandleUpgrade(factory.deserialize(this.instantiationService, e.value)); // {{SQL CARBON EDIT}} handle upgrade path to new serialization if (editor) { this.registerEditorListeners(editor); }