diff --git a/extensions/mssql/src/main.ts b/extensions/mssql/src/main.ts index 7dda2a7fee..548fb2aedd 100644 --- a/extensions/mssql/src/main.ts +++ b/extensions/mssql/src/main.ts @@ -340,17 +340,21 @@ async function handleOpenNotebookTask(profile: azdata.IConnectionProfile): Promi } async function handleOpenClusterStatusNotebookTask(profile: azdata.IConnectionProfile, appContext: AppContext): Promise { - const notebookRelativePath = 'notebooks/tsg/cluster-status.ipynb'; - const notebookFullPath = path.join(appContext.extensionContext.extensionPath, notebookRelativePath); + const notebookRelativePath: string = 'notebooks/tsg/cluster-status.ipynb'; + const notebookFullPath: string = path.join(appContext.extensionContext.extensionPath, notebookRelativePath); if (!Utils.fileExists(notebookFullPath)) { vscode.window.showErrorMessage(localize("fileNotFound", "Unable to find the file specified")); } else { - const targetFile = Utils.getTargetFileName(notebookFullPath); - Utils.copyFile(notebookFullPath, targetFile); - let fileUri = vscode.Uri.file(targetFile); - await azdata.nb.showNotebookDocument(fileUri, { - connectionProfile: profile, - preview: false + const title: string = Utils.findNextUntitledEditorName(notebookFullPath); + const untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${title}`); + vscode.workspace.openTextDocument(notebookFullPath).then((document) => { + let initialContent = document.getText(); + azdata.nb.showNotebookDocument(untitledFileName, { + connectionProfile: profile, + preview: true, + initialContent: initialContent, + initialDirtyState: false + }); }); } } diff --git a/extensions/mssql/src/utils.ts b/extensions/mssql/src/utils.ts index 1e3fd6037b..ff27d414e9 100644 --- a/extensions/mssql/src/utils.ts +++ b/extensions/mssql/src/utils.ts @@ -35,19 +35,18 @@ export function getAppDataPath() { * @param filePath source notebook file name * @param fileExtension file type */ -export function getTargetFileName(filePath: string): string { - const targetDirectory = os.homedir(); +export function findNextUntitledEditorName(filePath: string): string { const fileExtension = path.extname(filePath); const baseName = path.basename(filePath, fileExtension); - let targetFileName; let idx = 0; + let title = `${baseName}`; do { const suffix = idx === 0 ? '' : `-${idx}`; - targetFileName = path.join(targetDirectory, `${baseName}${suffix}${fileExtension}`); + title = `${baseName}${suffix}`; idx++; - } while (fs.existsSync(targetFileName)); + } while (azdata.nb.notebookDocuments.findIndex(doc => doc.isUntitled && doc.fileName === title) > -1); - return targetFileName; + return title; } export function fileExists(file: string): boolean { diff --git a/extensions/resource-deployment/src/services/notebookService.ts b/extensions/resource-deployment/src/services/notebookService.ts index 90542556be..ba4a882eae 100644 --- a/extensions/resource-deployment/src/services/notebookService.ts +++ b/extensions/resource-deployment/src/services/notebookService.ts @@ -4,9 +4,10 @@ *--------------------------------------------------------------------------------------------*/ 'use strict'; +import * as vscode from 'vscode'; +import * as azdata from 'azdata'; import { NotebookInfo } from '../interfaces'; import { isString } from 'util'; -import * as os from 'os'; import * as path from 'path'; import * as nls from 'vscode-nls'; import { IPlatformService } from './platformService'; @@ -28,9 +29,7 @@ export class NotebookService implements INotebookService { const notebookRelativePath = this.getNotebook(notebook); const notebookFullPath = path.join(__dirname, '../../', notebookRelativePath); if (notebookRelativePath && this.platformService.fileExists(notebookFullPath)) { - const targetFileName = this.getTargetNotebookFileName(notebookFullPath, os.homedir()); - this.platformService.copyFile(notebookFullPath, targetFileName); - this.platformService.openFile(targetFileName); + this.showNotebookAsUntitled(notebookFullPath); } else { this.platformService.showErrorMessage(localize('resourceDeployment.notebookNotFound', 'The notebook {0} does not exist', notebookFullPath)); @@ -58,22 +57,31 @@ export class NotebookService implements INotebookService { return notebookPath; } - /** - * Get a file name that is not already used in the target directory - * @param notebook source notebook file name - * @param targetDirectory target directory - */ - getTargetNotebookFileName(notebook: string, targetDirectory: string): string { - const notebookFileExtension = '.ipynb'; - const baseName = path.basename(notebook, notebookFileExtension); - let targetFileName; + findNextUntitledEditorName(filePath: string): string { + const fileExtension = path.extname(filePath); + const baseName = path.basename(filePath, fileExtension); let idx = 0; + let title = `${baseName}`; do { const suffix = idx === 0 ? '' : `-${idx}`; - targetFileName = path.join(targetDirectory, `${baseName}${suffix}${notebookFileExtension}`); + title = `${baseName}${suffix}`; idx++; - } while (this.platformService.fileExists(targetFileName)); + } while (this.platformService.isNotebookNameUsed(title)); - return targetFileName; + return title; + } + + showNotebookAsUntitled(notebookPath: string): void { + let targetFileName: string = this.findNextUntitledEditorName(notebookPath); + const untitledFileName: vscode.Uri = vscode.Uri.parse(`untitled:${targetFileName}`); + vscode.workspace.openTextDocument(notebookPath).then((document) => { + let initialContent = document.getText(); + azdata.nb.showNotebookDocument(untitledFileName, { + connectionProfile: undefined, + preview: false, + initialContent: initialContent, + initialDirtyState: false + }); + }); } } \ No newline at end of file diff --git a/extensions/resource-deployment/src/services/platformService.ts b/extensions/resource-deployment/src/services/platformService.ts index 5f442df282..c62dd34fac 100644 --- a/extensions/resource-deployment/src/services/platformService.ts +++ b/extensions/resource-deployment/src/services/platformService.ts @@ -6,6 +6,7 @@ import * as fs from 'fs'; import * as vscode from 'vscode'; +import * as azdata from 'azdata'; /** * Abstract of platform dependencies @@ -16,6 +17,7 @@ export interface IPlatformService { fileExists(file: string): boolean; openFile(filePath: string): void; showErrorMessage(message: string): void; + isNotebookNameUsed(title: string): boolean; } export class PlatformService implements IPlatformService { @@ -38,4 +40,8 @@ export class PlatformService implements IPlatformService { showErrorMessage(message: string): void { vscode.window.showErrorMessage(message); } + + isNotebookNameUsed(title: string): boolean { + return (azdata.nb.notebookDocuments.findIndex(doc => doc.isUntitled && doc.fileName === title) > -1); + } } \ No newline at end of file diff --git a/extensions/resource-deployment/src/test/notebookService.test.ts b/extensions/resource-deployment/src/test/notebookService.test.ts index c5afc79274..01037f3551 100644 --- a/extensions/resource-deployment/src/test/notebookService.test.ts +++ b/extensions/resource-deployment/src/test/notebookService.test.ts @@ -7,8 +7,6 @@ import * as TypeMoq from 'typemoq'; import 'mocha'; -import * as path from 'path'; -import * as os from 'os'; import { NotebookService } from '../services/NotebookService'; import assert = require('assert'); import { NotebookInfo } from '../interfaces'; @@ -62,59 +60,30 @@ suite('Notebook Service Tests', function (): void { mockPlatformService.verify((service) => service.platform(), TypeMoq.Times.once()); }); - test('launchNotebook', () => { - const mockPlatformService = TypeMoq.Mock.ofType(); - const notebookService = new NotebookService(mockPlatformService.object); - const notebookFileName = 'mynotebook.ipynb'; - const notebookPath = `./notebooks/${notebookFileName}`; - - let actualSourceFile; - const expectedSourceFile = path.join(__dirname, '../../', notebookPath); - let actualTargetFile; - const expectedTargetFile = path.join(os.homedir(), notebookFileName); - mockPlatformService.setup((service) => service.platform()).returns(() => { return 'win32'; }); - mockPlatformService.setup((service) => service.openFile(TypeMoq.It.isAnyString())); - mockPlatformService.setup((service) => service.fileExists(TypeMoq.It.isAnyString())) - .returns((path) => { - if (path === expectedSourceFile) { - return true; - } - return false; - }); - mockPlatformService.setup((service) => service.copyFile(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString())) - .returns((source, target) => { actualSourceFile = source; actualTargetFile = target; }); - notebookService.launchNotebook(notebookPath); - mockPlatformService.verify((service) => service.copyFile(TypeMoq.It.isAnyString(), TypeMoq.It.isAnyString()), TypeMoq.Times.once()); - mockPlatformService.verify((service) => service.openFile(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); - assert.equal(actualSourceFile, expectedSourceFile, 'source file is not correct'); - assert.equal(actualTargetFile, expectedTargetFile, 'target file is not correct'); - }); - - test('getTargetNotebookFileName with no name conflict', () => { + test('findNextUntitledEditorName with no name conflict', () => { const mockPlatformService = TypeMoq.Mock.ofType(); const notebookService = new NotebookService(mockPlatformService.object); const notebookFileName = 'mynotebook.ipynb'; const sourceNotebookPath = `./notebooks/${notebookFileName}`; - const expectedTargetFile = path.join(os.homedir(), notebookFileName); - mockPlatformService.setup((service) => service.fileExists(TypeMoq.It.isAnyString())) + const expectedTargetFile = 'mynotebook'; + mockPlatformService.setup((service) => service.isNotebookNameUsed(TypeMoq.It.isAnyString())) .returns((path) => { return false; }); - const actualFileName = notebookService.getTargetNotebookFileName(sourceNotebookPath, os.homedir()); - mockPlatformService.verify((service) => service.fileExists(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); + const actualFileName = notebookService.findNextUntitledEditorName(sourceNotebookPath); + mockPlatformService.verify((service) => service.isNotebookNameUsed(TypeMoq.It.isAnyString()), TypeMoq.Times.once()); assert.equal(actualFileName, expectedTargetFile, 'target file name is not correct'); }); - test('getTargetNotebookFileName with name conflicts', () => { + test('findNextUntitledEditorName with name conflicts', () => { const mockPlatformService = TypeMoq.Mock.ofType(); const notebookService = new NotebookService(mockPlatformService.object); const notebookFileName = 'mynotebook.ipynb'; const sourceNotebookPath = `./notebooks/${notebookFileName}`; - const expectedFileName = 'mynotebook-2.ipynb'; + const expectedFileName = 'mynotebook-2'; - const expected1stAttemptTargetFile = path.join(os.homedir(), notebookFileName); - const expected2ndAttemptTargetFile = path.join(os.homedir(), 'mynotebook-1.ipynb'); - const expectedTargetFile = path.join(os.homedir(), expectedFileName); - mockPlatformService.setup((service) => service.fileExists(TypeMoq.It.isAnyString())) + const expected1stAttemptTargetFile = 'mynotebook'; + const expected2ndAttemptTargetFile = 'mynotebook-1'; + mockPlatformService.setup((service) => service.isNotebookNameUsed(TypeMoq.It.isAnyString())) .returns((path) => { // list all the possible values here and handle them // if we only handle the expected value and return true for anything else, the test might run forever until times out @@ -123,8 +92,8 @@ suite('Notebook Service Tests', function (): void { } return false; }); - const actualFileName = notebookService.getTargetNotebookFileName(sourceNotebookPath, os.homedir()); - mockPlatformService.verify((service) => service.fileExists(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(3)); - assert.equal(actualFileName, expectedTargetFile, 'target file name is not correct'); + const actualFileName = notebookService.findNextUntitledEditorName(sourceNotebookPath); + mockPlatformService.verify((service) => service.isNotebookNameUsed(TypeMoq.It.isAnyString()), TypeMoq.Times.exactly(3)); + assert.equal(actualFileName, expectedFileName, 'target file name is not correct'); }); }); \ No newline at end of file diff --git a/src/sql/azdata.proposed.d.ts b/src/sql/azdata.proposed.d.ts index 0e4fdb120c..fd603abb43 100644 --- a/src/sql/azdata.proposed.d.ts +++ b/src/sql/azdata.proposed.d.ts @@ -4485,6 +4485,11 @@ declare module 'azdata' { * Optional content used to give an initial notebook state */ initialContent?: nb.INotebookContents | string; + + /** + * A optional boolean value indicating the dirty state after the intial content is loaded, default value is true + */ + initialDirtyState?: boolean; } /** diff --git a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts index 87c6ded919..1f26140dc2 100644 --- a/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/browser/mainThreadNotebookDocumentsAndEditors.ts @@ -460,6 +460,9 @@ export class MainThreadNotebookDocumentsAndEditors extends Disposable implements let untitledModel = await input.textInput.resolve(); await untitledModel.load(); input.untitledEditorModel = untitledModel; + if (options.initialDirtyState === false) { + input.untitledEditorModel.setDirty(false); + } } let editor = await this._editorService.openEditor(input, editorOptions, viewColumnToEditorGroup(this._editorGroupService, options.position)); if (!editor) { diff --git a/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts b/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts index 233722ab87..e6f6bce8eb 100644 --- a/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts +++ b/src/sql/workbench/api/node/extHostNotebookDocumentsAndEditors.ts @@ -206,6 +206,7 @@ export class ExtHostNotebookDocumentsAndEditors implements ExtHostNotebookDocume options.initialContent = showOptions.initialContent; } } + options.initialDirtyState = showOptions.initialDirtyState; } let id = await this._proxy.$tryShowNotebookDocument(uri, options); let editor = this.getEditor(id); diff --git a/src/sql/workbench/api/node/sqlExtHost.protocol.ts b/src/sql/workbench/api/node/sqlExtHost.protocol.ts index 598918449d..80bf01c48a 100644 --- a/src/sql/workbench/api/node/sqlExtHost.protocol.ts +++ b/src/sql/workbench/api/node/sqlExtHost.protocol.ts @@ -908,6 +908,7 @@ export interface INotebookShowOptions { connectionProfile?: azdata.IConnectionProfile; defaultKernel?: azdata.nb.IKernelSpec; initialContent?: string; + initialDirtyState?: boolean; } export interface ExtHostNotebookDocumentsAndEditorsShape {