/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import { QueryResultsInput } from 'sql/parts/query/common/queryResultsInput'; import { QueryInput } from 'sql/parts/query/common/queryInput'; import { EditDataInput } from 'sql/parts/editData/common/editDataInput'; import { IConnectableInput, IConnectionManagementService } from 'sql/parts/connection/common/connectionManagement'; import { IEditorGroupService } from 'vs/workbench/services/group/common/groupService'; import { IQueryEditorService, IQueryEditorOptions } from 'sql/parts/query/common/queryEditorService'; import { QueryPlanInput } from 'sql/parts/queryPlan/queryPlanInput'; import { sqlModeId, untitledFilePrefix, getSupportedInputResource } from 'sql/parts/common/customInputConverter'; import * as TaskUtilities from 'sql/workbench/common/taskUtilities'; import { IMode } from 'vs/editor/common/modes'; import { ITextModel } from 'vs/editor/common/model'; import { IEditor, IEditorInput, Position } from 'vs/platform/editor/common/editor'; import { CodeEditor } from 'vs/editor/browser/codeEditor'; import { IEditorGroup } from 'vs/workbench/common/editor'; import { IUntitledEditorService } from 'vs/workbench/services/untitled/common/untitledEditorService'; import { IWorkbenchEditorService } from 'vs/workbench/services/editor/common/editorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { FileEditorInput } from 'vs/workbench/parts/files/common/editors/fileEditorInput'; import Severity from 'vs/base/common/severity'; import nls = require('vs/nls'); import URI from 'vs/base/common/uri'; import paths = require('vs/base/common/paths'); import { isLinux } from 'vs/base/common/platform'; import { Schemas } from 'vs/base/common/network'; import { INotificationService } from 'vs/platform/notification/common/notification'; import { EditDataResultsInput } from 'sql/parts/editData/common/editDataResultsInput'; const fs = require('fs'); /** * Service wrapper for opening and creating SQL documents as sql editor inputs */ export class QueryEditorService implements IQueryEditorService { public _serviceBrand: any; private static CHANGE_UNSUPPORTED_ERROR_MESSAGE = nls.localize( 'queryEditorServiceChangeUnsupportedError', 'Change Language Mode is not supported for unsaved queries' ); private static CHANGE_ERROR_MESSAGE = nls.localize( 'queryEditorServiceChangeError', 'Please save or discard changes before switching to/from the SQL Language Mode' ); // service references for static functions private static editorService: IWorkbenchEditorService; private static instantiationService: IInstantiationService; private static editorGroupService: IEditorGroupService; private static notificationService: INotificationService; constructor( @IUntitledEditorService private _untitledEditorService: IUntitledEditorService, @IInstantiationService private _instantiationService: IInstantiationService, @IWorkbenchEditorService private _editorService: IWorkbenchEditorService, @IEditorGroupService private _editorGroupService: IEditorGroupService, @INotificationService private _notificationService: INotificationService, @IConnectionManagementService private _connectionManagementService: IConnectionManagementService, ) { QueryEditorService.editorService = _editorService; QueryEditorService.instantiationService = _instantiationService; QueryEditorService.editorGroupService = _editorGroupService; QueryEditorService.notificationService = _notificationService; } ////// Public functions /** * Creates new untitled document for SQL query and opens in new editor tab */ public newSqlEditor(sqlContent?: string, connectionProviderName?: string): Promise { return new Promise((resolve, reject) => { try { // Create file path and file URI let filePath = this.createUntitledSqlFilePath(); let docUri: URI = URI.from({ scheme: Schemas.untitled, path: filePath }); // Create a sql document pane with accoutrements const fileInput = this._untitledEditorService.createOrGet(docUri, 'sql'); fileInput.resolve().then(m => { if (sqlContent) { m.textEditorModel.setValue(sqlContent); } }); //input.resolve().then(model => this.backupFileService.backupResource(resource, model.getValue(), model.getVersionId())).done(null, errors.onUnexpectedError); const queryResultsInput: QueryResultsInput = this._instantiationService.createInstance(QueryResultsInput, docUri.toString()); let queryInput: QueryInput = this._instantiationService.createInstance(QueryInput, fileInput.getName(), '', fileInput, queryResultsInput, connectionProviderName); this._editorService.openEditor(queryInput, { pinned: true }) .then((editor) => { let params = editor.input; resolve(params); }, (error) => { reject(error); }); } catch (error) { reject(error); } }); } // Creates a new query plan document public newQueryPlanEditor(xmlShowPlan: string): Promise { const self = this; return new Promise((resolve, reject) => { let queryPlanInput: QueryPlanInput = self._instantiationService.createInstance(QueryPlanInput, xmlShowPlan, 'aaa', undefined); self._editorService.openEditor(queryPlanInput, { pinned: true }, false); resolve(true); }); } /** * Creates new edit data session */ public newEditDataEditor(schemaName: string, tableName: string, sqlContent: string): Promise { return new Promise((resolve, reject) => { try { // Create file path and file URI let objectName = schemaName ? schemaName + '.' + tableName : tableName; let filePath = this.createEditDataFileName(objectName); let docUri: URI = URI.from({ scheme: Schemas.untitled, path: filePath }); // Create a sql document pane with accoutrements const fileInput = this._untitledEditorService.createOrGet(docUri, 'sql'); fileInput.resolve().then(m => { if (sqlContent) { m.textEditorModel.setValue(sqlContent); } }); // Create an EditDataInput for editing const resultsInput: EditDataResultsInput = this._instantiationService.createInstance(EditDataResultsInput, docUri.toString()); let editDataInput: EditDataInput = this._instantiationService.createInstance(EditDataInput, docUri, schemaName, tableName, fileInput, sqlContent, resultsInput); this._editorService.openEditor(editDataInput, { pinned: true }) .then((editor) => { let params = editor.input; resolve(params); }, (error) => { reject(error); }); } catch (error) { reject(error); } }); } /** * Clears any QueryEditor data for the given URI held by this service */ public onQueryInputClosed(uri: string): void { } onSaveAsCompleted(oldResource: URI, newResource: URI): void { let oldResourceString: string = oldResource.toString(); const stacks = this._editorGroupService.getStacksModel(); stacks.groups.forEach(group => { group.getEditors().forEach(input => { if (input instanceof QueryInput) { const resource = input.getResource(); // Update Editor if file (or any parent of the input) got renamed or moved // Note: must check the new file name for this since this method is called after the rename is completed if (paths.isEqualOrParent(resource.fsPath, newResource.fsPath, !isLinux /* ignorecase */)) { // In this case, we know that this is a straight rename so support this as a rename / replace operation TaskUtilities.replaceConnection(oldResourceString, newResource.toString(), this._connectionManagementService).then(result => { if (result && result.connected) { input.onConnectSuccess(); } else { input.onConnectReject(); } }); } } }); }); } ////// Public static functions // These functions are static to reduce extra lines needed in the vscode code base /** * Checks if the Language Mode is being changed to/from SQL. If so, swaps out the input of the * given editor with a new input, opens a new editor, then returns the new editor's IModel. * * Returns an immediately resolved promise if the SQL Language mode is not involved. In this case, * the calling function in editorStatus.ts will handle the language change normally. * * Returns an immediately resolved promise with undefined if SQL is involved in the language change * and the editor is dirty. In this case, the calling function in editorStatus.ts will not perform * the language change. TODO: change this - tracked by issue #727 * * In all other cases (when SQL is involved in the language change and the editor is not dirty), * returns a promise that will resolve when the old editor has been replaced by a new editor. */ public static sqlLanguageModeCheck(model: ITextModel, mode: IMode, editor: IEditor): Promise { if (!model || !mode || !editor) { return Promise.resolve(undefined); } let newLanguage: string = mode.getLanguageIdentifier().language; let oldLanguage: string = model.getLanguageIdentifier().language; let changingToSql = sqlModeId === newLanguage; let changingFromSql = sqlModeId === oldLanguage; let changingLanguage = newLanguage !== oldLanguage; if (!changingLanguage) { return Promise.resolve(model); } if (!changingFromSql && !changingToSql) { return Promise.resolve(model); } let uri: URI = QueryEditorService._getEditorChangeUri(editor.input, changingToSql); if(uri.scheme === Schemas.untitled && (editor.input instanceof QueryInput || editor.input instanceof EditDataInput)) { QueryEditorService.notificationService.notify({ severity: Severity.Error, message: QueryEditorService.CHANGE_UNSUPPORTED_ERROR_MESSAGE }); return Promise.resolve(undefined); } // Return undefined to notify the calling funciton to not perform the language change // TODO change this - tracked by issue #727 if (editor.input.isDirty()) { QueryEditorService.notificationService.notify({ severity: Severity.Error, message: QueryEditorService.CHANGE_ERROR_MESSAGE }); return Promise.resolve(undefined); } let group: IEditorGroup = QueryEditorService.editorGroupService.getStacksModel().groupAt(editor.position); let index: number = group.indexOf(editor.input); let position: Position = editor.position; let options: IQueryEditorOptions = editor.options ? editor.options : {}; options.index = index; options.pinned = group.isPinned(index); // Return a promise that will resovle when the old editor has been replaced by a new editor return new Promise((resolve, reject) => { let newEditorInput = QueryEditorService._getNewEditorInput(changingToSql, editor.input, uri); // Override queryEditorCheck to not open this file in a QueryEditor if (!changingToSql) { options.denyQueryEditor = true; } // Close the current editor QueryEditorService.editorService.closeEditor(position, editor.input).then(() => { // Reopen a new editor in the same position/index QueryEditorService.editorService.openEditor(newEditorInput, options, position).then((editor) => { resolve(QueryEditorService._onEditorOpened(editor, uri.toString(), position, options.pinned)); }, (error) => { reject(error); }); }); }); } ////// Private functions private createUntitledSqlFilePath(): string { let sqlFileName = (counter: number): string => { return `${untitledFilePrefix}${counter}`; }; let counter = 1; // Get document name and check if it exists let filePath = sqlFileName(counter); while (fs.existsSync(filePath)) { counter++; filePath = sqlFileName(counter); } // check if this document name already exists in any open documents let untitledEditors = this._untitledEditorService.getAll(); while (untitledEditors.find(x => x.getName().toUpperCase() === filePath.toUpperCase())) { counter++; filePath = sqlFileName(counter); } return filePath; } private createEditDataFileName(tableName: string): string { let editDataFileName = (counter: number): string => { return encodeURIComponent(`${tableName}_${counter}`); }; let counter = 1; // Get document name and check if it exists let filePath = editDataFileName(counter); while (fs.existsSync(filePath)) { counter++; filePath = editDataFileName(counter); } let untitledEditors = this._untitledEditorService.getAll(); while (untitledEditors.find(x => x.getName().toUpperCase() === filePath.toUpperCase())) { counter++; filePath = editDataFileName(counter); } return filePath; } ////// Private static functions /** * Returns a QueryInput if we are changingToSql. Returns a FileEditorInput if we are !changingToSql. */ private static _getNewEditorInput(changingToSql: boolean, input: IEditorInput, uri: URI): IEditorInput { if (!uri) { return undefined; } let newEditorInput: IEditorInput = undefined; if (changingToSql) { const queryResultsInput: QueryResultsInput = QueryEditorService.instantiationService.createInstance(QueryResultsInput, uri.toString()); let queryInput: QueryInput = QueryEditorService.instantiationService.createInstance(QueryInput, input.getName(), '', input, queryResultsInput, undefined); newEditorInput = queryInput; } else { let uriCopy: URI = URI.from( { scheme: uri.scheme, authority: uri.authority, path: uri.path, query: uri.query, fragment: uri.fragment } ); newEditorInput = QueryEditorService.instantiationService.createInstance(FileEditorInput, uriCopy, undefined); } return newEditorInput; } /** * Gets the URI for this IEditorInput or returns undefined if one does not exist. */ private static _getEditorChangeUri(input: IEditorInput, changingToSql: boolean): URI { let uriSource: IEditorInput = input; // It is assumed that if we got here, !changingToSql is logically equivalent to changingFromSql let changingFromSql = !changingToSql; if (input instanceof QueryInput && changingFromSql) { let queryInput: QueryInput = input; uriSource = queryInput.sql; } return getSupportedInputResource(uriSource); } /** * Handle all cleanup actions that need to wait until the editor is fully open. */ private static _onEditorOpened(editor: IEditor, uri: string, position: Position, isPinned: boolean): ITextModel { // Reset the editor pin state // TODO: change this so it happens automatically in openEditor in sqlLanguageModeCheck. Performing this here // causes the text on the tab to slightly flicker for unpinned files (from non-italic to italic to non-italic). // This is currently unavoidable because vscode ignores "pinned" on IEditorOptions if "index" is not undefined, // and we need to specify "index"" so the editor tab remains in the same place let group: IEditorGroup = QueryEditorService.editorGroupService.getStacksModel().groupAt(position); if (isPinned) { QueryEditorService.editorGroupService.pinEditor(group, editor.input); } // @SQLTODO do we need the below // else { // QueryEditorService.editorGroupService.p .unpinEditor(group, editor.input); // } // Grab and returns the IModel that will be used to resolve the sqlLanguageModeCheck promise. let control = editor.getControl(); let codeEditor: CodeEditor = control; let newModel = codeEditor ? codeEditor.getModel() : undefined; return newModel; } }