From f3bed39e8e17a7459eff8b012f7a88d630b8123e Mon Sep 17 00:00:00 2001 From: Leila Lali Date: Wed, 28 Dec 2016 16:22:36 -0800 Subject: [PATCH 1/4] added new command to create new SQL query document --- package.json | 13 +- src/controllers/mainController.ts | 29 +++- src/controllers/untitledSqlDocumentService.ts | 62 +++++++++ src/models/constants.ts | 1 + test/mainController.test.ts | 27 ++++ test/untitledSqlDocumentService.test.ts | 128 ++++++++++++++++++ 6 files changed, 257 insertions(+), 3 deletions(-) create mode 100644 src/controllers/untitledSqlDocumentService.ts create mode 100644 test/untitledSqlDocumentService.test.ts diff --git a/package.json b/package.json index 4a504a6dac..1fe1732859 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": [ @@ -199,6 +205,11 @@ "key": "ctrl+shift+d", "mac": "cmd+shift+d", "when": "editorTextFocus && editorLangId == 'sql'" + }, + { + "command": "extension.newQuery", + "key": "ctrl+shift+n", + "mac": "cmd+shift+n+q" } ], "configuration": { diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index 71ccccfa16..d3cef6ad20 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 @@ -45,13 +47,21 @@ export default class MainController implements vscode.Disposable { */ constructor(context: vscode.ExtensionContext, connectionManager?: ConnectionManager, - vscodeWrapper?: VscodeWrapper) { + vscodeWrapper?: VscodeWrapper, + untitledSqlDocumentService?: UntitledSqlDocumentService) { this._context = context; if (connectionManager) { this._connectionMgr = connectionManager; } if (vscodeWrapper) { this._vscodeWrapper = vscodeWrapper; + } else { + this._vscodeWrapper = new VscodeWrapper(); + } + if (untitledSqlDocumentService) { + this._untitledSqlDocumentService = untitledSqlDocumentService; + } else { + this._untitledSqlDocumentService = new UntitledSqlDocumentService(this._vscodeWrapper); } } @@ -104,8 +114,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 +318,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 +376,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..1e84f89230 --- /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()); + }); + }); +}); + From dd4f8fa492299fcbe70ee9011b2323af3ed33933 Mon Sep 17 00:00:00 2001 From: Leila Lali Date: Thu, 29 Dec 2016 10:11:12 -0800 Subject: [PATCH 2/4] removed the shortcut for the new command --- package.json | 5 ----- src/controllers/mainController.ts | 9 ++------- 2 files changed, 2 insertions(+), 12 deletions(-) diff --git a/package.json b/package.json index 1fe1732859..e3693afa15 100644 --- a/package.json +++ b/package.json @@ -205,11 +205,6 @@ "key": "ctrl+shift+d", "mac": "cmd+shift+d", "when": "editorTextFocus && editorLangId == 'sql'" - }, - { - "command": "extension.newQuery", - "key": "ctrl+shift+n", - "mac": "cmd+shift+n+q" } ], "configuration": { diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index d3cef6ad20..ae8bc7e563 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -47,8 +47,7 @@ export default class MainController implements vscode.Disposable { */ constructor(context: vscode.ExtensionContext, connectionManager?: ConnectionManager, - vscodeWrapper?: VscodeWrapper, - untitledSqlDocumentService?: UntitledSqlDocumentService) { + vscodeWrapper?: VscodeWrapper) { this._context = context; if (connectionManager) { this._connectionMgr = connectionManager; @@ -58,11 +57,7 @@ export default class MainController implements vscode.Disposable { } else { this._vscodeWrapper = new VscodeWrapper(); } - if (untitledSqlDocumentService) { - this._untitledSqlDocumentService = untitledSqlDocumentService; - } else { - this._untitledSqlDocumentService = new UntitledSqlDocumentService(this._vscodeWrapper); - } + this._untitledSqlDocumentService = new UntitledSqlDocumentService(this._vscodeWrapper); } /** From 1ffbcf8314f4ee9403cc8d6aa2fb4ff8d8a001cb Mon Sep 17 00:00:00 2001 From: Leila Lali Date: Tue, 3 Jan 2017 09:38:46 -0800 Subject: [PATCH 3/4] fixed a reference to a file which should start with lowercase --- test/untitledSqlDocumentService.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/untitledSqlDocumentService.test.ts b/test/untitledSqlDocumentService.test.ts index 1e84f89230..52fa30518f 100644 --- a/test/untitledSqlDocumentService.test.ts +++ b/test/untitledSqlDocumentService.test.ts @@ -1,7 +1,7 @@ import * as TypeMoq from 'typemoq'; import vscode = require('vscode'); import UntitledSqlDocumentService from '../src/controllers/untitledSqlDocumentService'; -import VscodeWrapper from '../src/controllers/VscodeWrapper'; +import VscodeWrapper from '../src/controllers/vscodeWrapper'; const fse = require('fs-extra'); const fs = require('fs'); From 007a8b542093f506ef4fc0a2704696bd4063da09 Mon Sep 17 00:00:00 2001 From: Leila Lali Date: Wed, 4 Jan 2017 10:57:16 -0800 Subject: [PATCH 4/4] Fixes based on code review comments --- src/controllers/mainController.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/controllers/mainController.ts b/src/controllers/mainController.ts index ae8bc7e563..e03f7b9bc3 100644 --- a/src/controllers/mainController.ts +++ b/src/controllers/mainController.ts @@ -52,11 +52,8 @@ export default class MainController implements vscode.Disposable { if (connectionManager) { this._connectionMgr = connectionManager; } - if (vscodeWrapper) { - this._vscodeWrapper = vscodeWrapper; - } else { - this._vscodeWrapper = new VscodeWrapper(); - } + this._vscodeWrapper = vscodeWrapper || new VscodeWrapper(); + this._untitledSqlDocumentService = new UntitledSqlDocumentService(this._vscodeWrapper); }