diff --git a/package.json b/package.json index 4a504a6dac..e3693afa15 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,8 @@ "onCommand:extension.manageProfiles", "onCommand:extension.chooseDatabase", "onCommand:extension.cancelQuery", - "onCommand:extension.showGettingStarted" + "onCommand:extension.showGettingStarted", + "onCommand:extension.newQuery" ], "main": "./out/src/extension", "extensionDependencies": [ @@ -179,6 +180,11 @@ "command": "extension.showGettingStarted", "title": "Getting Started Guide", "category": "MS SQL" + }, + { + "command": "extension.newQuery", + "title": "New query", + "category": "MS SQL" } ], "keybindings": [ diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 71ccccfa16..e03f7b9bc3 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -16,6 +16,7 @@ import { IPrompter } from '../prompts/question'; import CodeAdapter from '../prompts/adapter'; import Telemetry from '../models/telemetry'; import VscodeWrapper from './vscodeWrapper'; +import UntitledSqlDocumentService from './untitledSqlDocumentService'; import { ISelectionData } from './../models/interfaces'; import * as path from 'path'; import fs = require('fs'); @@ -38,6 +39,7 @@ export default class MainController implements vscode.Disposable { private _lastSavedTimer: Utils.Timer; private _lastOpenedUri: string; private _lastOpenedTimer: Utils.Timer; + private _untitledSqlDocumentService: UntitledSqlDocumentService; /** * The main controller constructor @@ -50,9 +52,9 @@ export default class MainController implements vscode.Disposable { if (connectionManager) { this._connectionMgr = connectionManager; } - if (vscodeWrapper) { - this._vscodeWrapper = vscodeWrapper; - } + this._vscodeWrapper = vscodeWrapper || new VscodeWrapper(); + + this._untitledSqlDocumentService = new UntitledSqlDocumentService(this._vscodeWrapper); } /** @@ -104,8 +106,10 @@ export default class MainController implements vscode.Disposable { this._event.on(Constants.cmdCancelQuery, () => { self.onCancelQuery(); }); this.registerCommand(Constants.cmdShowGettingStarted); this._event.on(Constants.cmdShowGettingStarted, () => { self.launchGettingStartedPage(); }); + this.registerCommand(Constants.cmdNewQuery); + this._event.on(Constants.cmdNewQuery, () => { self.runAndLogErrors(self.onNewQuery(), 'onNewQuery'); }); - this._vscodeWrapper = new VscodeWrapper(); + // this._vscodeWrapper = new VscodeWrapper(); // Add handlers for VS Code generated commands this._vscodeWrapper.onDidCloseTextDocument(params => this.onDidCloseTextDocument(params)); @@ -306,6 +310,10 @@ export default class MainController implements vscode.Disposable { this._connectionMgr = connectionManager; } + public set untitledSqlDocumentService(untitledSqlDocumentService: UntitledSqlDocumentService) { + this._untitledSqlDocumentService = untitledSqlDocumentService; + } + /** * Verifies the extension is initilized and if not shows an error message @@ -360,6 +368,15 @@ export default class MainController implements vscode.Disposable { opener(Constants.gettingStartedGuideLink); } + /** + * Opens a new query and creates new connection + */ + public onNewQuery(): Promise { + return this._untitledSqlDocumentService.newQuery().then(x => { + return this._connectionMgr.onNewConnection(); + }); + } + /** * Check if the extension launched file exists. * This is to detect when we are running in a clean install scenario. diff --git a/src/controllers/untitledSqlDocumentService.ts b/src/controllers/untitledSqlDocumentService.ts new file mode 100644 index 0000000000..0c84d90144 --- /dev/null +++ b/src/controllers/untitledSqlDocumentService.ts @@ -0,0 +1,62 @@ +/* -------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + * ------------------------------------------------------------------------------------------ */ + +import VscodeWrapper from './vscodeWrapper'; +import vscode = require('vscode'); +import path = require('path'); +import os = require('os'); +const fs = require('fs'); + +/** + * Service for creating untitled documents for SQL query + */ +export default class UntitledSqlDocumentService { + private _counter: number = 1; + + constructor(private vscodeWrapper: VscodeWrapper) { + } + + /** + * Creates new untitled document for SQL query and opens in new editor tab + */ + public newQuery(): Promise { + + return new Promise((resolve, reject) => { + try { + let filePath = this.createUntitledFilePath(); + let docUri: vscode.Uri = vscode.Uri.parse('untitled:' + filePath); + + // Open an untitled document. So the file doesn't have to exist in disk + this.vscodeWrapper.openTextDocument(docUri).then(doc => { + // Show the new untitled document in the editor's first tab and change the focus to it. + this.vscodeWrapper.showTextDocument(doc, 1, false).then(textDoc => { + this._counter++; + resolve(true); + }); + }); + } catch (error) { + reject(error); + } + }); + } + + private createUntitledFilePath(): string { + let filePath = UntitledSqlDocumentService.createFilePath(this._counter); + while (fs.existsSync(filePath)) { + this._counter++; + filePath = UntitledSqlDocumentService.createFilePath(this._counter); + } + while (this.vscodeWrapper.textDocuments.find(x => x.fileName.toUpperCase() === filePath.toUpperCase())) { + this._counter++; + filePath = UntitledSqlDocumentService.createFilePath(this._counter); + } + return filePath; + } + + public static createFilePath(counter: number): string { + return path.join(os.tmpdir(), `SQLQuery${counter}.sql`); + } +} + diff --git a/src/models/constants.ts b/src/models/constants.ts index 42fc051709..d251275e08 100644 --- a/src/models/constants.ts +++ b/src/models/constants.ts @@ -15,6 +15,7 @@ export const cmdDisconnect = 'extension.disconnect'; export const cmdChooseDatabase = 'extension.chooseDatabase'; export const cmdShowReleaseNotes = 'extension.showReleaseNotes'; export const cmdShowGettingStarted = 'extension.showGettingStarted'; +export const cmdNewQuery = 'extension.newQuery'; export const cmdManageConnectionProfiles = 'extension.manageProfiles'; export const sqlDbPrefix = '.database.windows.net'; diff --git a/test/mainController.test.ts b/test/mainController.test.ts index 00a8881d1c..be8146c263 100644 --- a/test/mainController.test.ts +++ b/test/mainController.test.ts @@ -4,6 +4,7 @@ import * as TypeMoq from 'typemoq'; import vscode = require('vscode'); import MainController from '../src/controllers/mainController'; import ConnectionManager from '../src/controllers/connectionManager'; +import UntitledSqlDocumentService from '../src/controllers/untitledSqlDocumentService'; import * as Extension from '../src/extension'; import Constants = require('../src/models/constants'); import assert = require('assert'); @@ -13,6 +14,7 @@ suite('MainController Tests', () => { let newDocument: vscode.TextDocument; let mainController: MainController; let connectionManager: TypeMoq.Mock; + let untitledSqlDocumentService: TypeMoq.Mock; let docUri: string; let newDocUri: string; let docUriCallback: string; @@ -53,6 +55,9 @@ suite('MainController Tests', () => { connectionManager = TypeMoq.Mock.ofType(ConnectionManager); mainController.connectionManager = connectionManager.object; + untitledSqlDocumentService = TypeMoq.Mock.ofType(UntitledSqlDocumentService); + mainController.untitledSqlDocumentService = untitledSqlDocumentService.object; + // Watching these functions and input paramters connectionManager.setup(x => x.onDidOpenTextDocument(TypeMoq.It.isAny())).callback((doc) => { docUriCallback = doc.uri.toString(); @@ -173,4 +178,26 @@ suite('MainController Tests', () => { } }); + test('onNewQuery should call the new query and new connection' , () => { + + untitledSqlDocumentService.setup(x => x.newQuery()).returns(() => Promise.resolve(true)); + connectionManager.setup(x => x.onNewConnection()).returns(() => Promise.resolve(true)); + + return mainController.onNewQuery().then(result => { + untitledSqlDocumentService.verify(x => x.newQuery(), TypeMoq.Times.once()); + connectionManager.verify(x => x.onNewConnection(), TypeMoq.Times.once()); + }); + }); + + test('onNewQuery should not call the new connection if new query fails' , done => { + + untitledSqlDocumentService.setup(x => x.newQuery()).returns(() => { return Promise.reject('error'); } ); + connectionManager.setup(x => x.onNewConnection()).returns(() => { return Promise.resolve(true); } ); + + mainController.onNewQuery().catch(error => { + untitledSqlDocumentService.verify(x => x.newQuery(), TypeMoq.Times.once()); + connectionManager.verify(x => x.onNewConnection(), TypeMoq.Times.never()); + done(); + }); + }); }); diff --git a/test/untitledSqlDocumentService.test.ts b/test/untitledSqlDocumentService.test.ts new file mode 100644 index 0000000000..52fa30518f --- /dev/null +++ b/test/untitledSqlDocumentService.test.ts @@ -0,0 +1,128 @@ +import * as TypeMoq from 'typemoq'; +import vscode = require('vscode'); +import UntitledSqlDocumentService from '../src/controllers/untitledSqlDocumentService'; +import VscodeWrapper from '../src/controllers/vscodeWrapper'; +const fse = require('fs-extra'); +const fs = require('fs'); + +interface IFixture { + openDocResult: Promise; + showDocResult: Promise; + vscodeWrapper: TypeMoq.Mock; + service: UntitledSqlDocumentService; + textDocuments: vscode.TextDocument[]; +} + +suite('UntitledSqlDocumentService Tests', () => { + + function createTextDocumentObject(fileName: string = ''): vscode.TextDocument { + return { + uri: undefined, + fileName: fileName, + getText: undefined, + getWordRangeAtPosition: undefined, + isDirty: true, + isUntitled: true, + languageId: 'sql', + lineAt: undefined, + lineCount: undefined, + offsetAt: undefined, + positionAt: undefined, + save: undefined, + validatePosition: undefined, + validateRange: undefined, + version: undefined + }; + } + + function createUntitledSqlDocumentService(fixture: IFixture): IFixture { + let vscodeWrapper: TypeMoq.Mock; + vscodeWrapper = TypeMoq.Mock.ofType(VscodeWrapper); + + vscodeWrapper.setup(x => x.textDocuments).returns(() => { return fixture.textDocuments; }); + vscodeWrapper.setup(x => x.openTextDocument(TypeMoq.It.isAny())) + .returns(() => { return Promise.resolve(createTextDocumentObject()); }); + vscodeWrapper.setup(x => x.showTextDocument(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny())) + .returns(() => { return Promise.resolve(TypeMoq.It.isAny()); }); + fixture.vscodeWrapper = vscodeWrapper; + fixture.service = new UntitledSqlDocumentService(vscodeWrapper.object); + return fixture; + } + + test('newQuery should open a new untitled document and show in new tab' , () => { + let fixture: IFixture = { + openDocResult: Promise.resolve(createTextDocumentObject()), + showDocResult: Promise.resolve(TypeMoq.It.isAny()), + service: undefined, + vscodeWrapper: undefined, + textDocuments: [] + }; + fixture = createUntitledSqlDocumentService(fixture); + + return fixture.service.newQuery().then(result => { + fixture.vscodeWrapper.verify(x => x.openTextDocument( + TypeMoq.It.is(d => d.scheme === 'untitled')), TypeMoq.Times.once()); + fixture.vscodeWrapper.verify(x => x.showTextDocument(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + }); + + test('newQuery should increment the counter for untitled document if file exits' , () => { + let fixture: IFixture = { + openDocResult: Promise.resolve(createTextDocumentObject()), + showDocResult: Promise.resolve(TypeMoq.It.isAny()), + service: undefined, + vscodeWrapper: undefined, + textDocuments: [] + }; + let counter = getCounterForUntitledFile(1); + fixture = createUntitledSqlDocumentService(fixture); + let filePath = UntitledSqlDocumentService.createFilePath(counter); + if (!fs.existsSync(filePath)) { + fs.writeFileSync(filePath, 'test'); + } + return fixture.service.newQuery().then(result => { + fixture.vscodeWrapper.verify(x => x.openTextDocument( + TypeMoq.It.is(d => verifyDocumentUri(d, counter + 1))), TypeMoq.Times.once()); + fixture.vscodeWrapper.verify(x => x.showTextDocument(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + if (!fs.existsSync(filePath)) { + fse.remove(filePath, undefined); + } + }); + }); + + function verifyDocumentUri(uri: vscode.Uri, expectedNumber: number): boolean { + return uri.scheme === 'untitled' && uri.path.endsWith(`${expectedNumber}.sql`); + } + + function getCounterForUntitledFile(start: number): number { + let counter = start; + let filePath = UntitledSqlDocumentService.createFilePath(counter); + while (fs.existsSync(filePath)) { + counter++; + filePath = UntitledSqlDocumentService.createFilePath(counter); + } + return counter; + } + + test('newQuery should increment the counter for untitled document given text documents already open with current counter' , () => { + let counter = getCounterForUntitledFile(1); + let fixture: IFixture = { + openDocResult: Promise.resolve(createTextDocumentObject()), + showDocResult: Promise.resolve(TypeMoq.It.isAny()), + service: undefined, + vscodeWrapper: undefined, + textDocuments: [ + createTextDocumentObject(UntitledSqlDocumentService.createFilePath(counter + 1)), + createTextDocumentObject(UntitledSqlDocumentService.createFilePath(counter))] + }; + fixture = createUntitledSqlDocumentService(fixture); + let service = fixture.service; + + return service.newQuery().then(result => { + fixture.vscodeWrapper.verify(x => x.openTextDocument( + TypeMoq.It.is(d => verifyDocumentUri(d, counter + 2))), TypeMoq.Times.once()); + fixture.vscodeWrapper.verify(x => x.showTextDocument(TypeMoq.It.isAny(), TypeMoq.It.isAny(), TypeMoq.It.isAny()), TypeMoq.Times.once()); + }); + }); +}); +