/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the Source EULA. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ 'use strict'; import { IEditorModel } from 'vs/platform/editor/common/editor'; import { EditorInput, EditorModel, ConfirmResult } from 'vs/workbench/common/editor'; import { Emitter, Event } from 'vs/base/common/event'; import { URI } from 'vs/base/common/uri'; import * as resources from 'vs/base/common/resources'; import * as azdata from 'azdata'; import { IStandardKernelWithProvider, getProvidersForFileName, getStandardKernelsForProvider } from 'sql/parts/notebook/notebookUtils'; import { INotebookService, DEFAULT_NOTEBOOK_PROVIDER, IProviderInfo } from 'sql/workbench/services/notebook/common/notebookService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ITextModelService } from 'vs/editor/common/services/resolverService'; import { INotebookModel, IContentManager } from 'sql/parts/notebook/models/modelInterfaces'; import { TextFileEditorModel } from 'vs/workbench/services/textfile/common/textFileEditorModel'; import { Range } from 'vs/editor/common/core/range'; import { UntitledEditorModel } from 'vs/workbench/common/editor/untitledEditorModel'; import { Schemas } from 'vs/base/common/network'; import { ITextFileService, ISaveOptions } from 'vs/workbench/services/textfile/common/textfiles'; import { LocalContentManager } from 'sql/workbench/services/notebook/node/localContentManager'; import { IConnectionProfile } from 'sql/platform/connection/common/interfaces'; import { UntitledEditorInput } from 'vs/workbench/common/editor/untitledEditorInput'; import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; export type ModeViewSaveHandler = (handle: number) => Thenable; export class NotebookEditorModel extends EditorModel { private dirty: boolean; private readonly _onDidChangeDirty: Emitter = this._register(new Emitter()); constructor(public readonly notebookUri: URI, private textEditorModel: TextFileEditorModel | UntitledEditorModel, @INotebookService private notebookService: INotebookService, @ITextFileService private textFileService: ITextFileService ) { super(); this._register(this.notebookService.onNotebookEditorAdd(notebook => { if (notebook.id === this.notebookUri.toString()) { // Hook to content change events notebook.modelReady.then(() => { this._register(notebook.model.kernelChanged(e => this.updateModel())); this._register(notebook.model.contentChanged(e => this.updateModel())); }, err => undefined); } })); if (this.textEditorModel instanceof UntitledEditorModel) { this._register(this.textEditorModel.onDidChangeDirty(e => this.setDirty(this.textEditorModel.isDirty()))); } else { this._register(this.textEditorModel.onDidStateChange(e => this.setDirty(this.textEditorModel.isDirty()))); } this.dirty = this.textEditorModel.isDirty(); } public get contentString(): string { let model = this.textEditorModel.textEditorModel; return model.getValue(); } get isDirty(): boolean { return this.textEditorModel.isDirty(); } public setDirty(dirty: boolean): void { if (this.dirty === dirty) { return; } this.dirty = dirty; this._onDidChangeDirty.fire(); } /** * UntitledEditor uses TextFileService to save data from UntitledEditorInput * Titled editor uses TextFileEditorModel to save existing notebook */ save(options: ISaveOptions): Promise { if (this.textEditorModel instanceof TextFileEditorModel) { this.textEditorModel.save(options); return Promise.resolve(true); } else { return this.textFileService.save(this.notebookUri, options); } } public updateModel(): void { let notebookModel = this.getNotebookModel(); if (notebookModel && this.textEditorModel && this.textEditorModel.textEditorModel) { let content = JSON.stringify(notebookModel.toJSON(), undefined, ' '); let model = this.textEditorModel.textEditorModel; let endLine = model.getLineCount(); let endCol = model.getLineMaxColumn(endLine); this.textEditorModel.textEditorModel.applyEdits([{ range: new Range(1, 1, endLine, endCol), text: content }]); } } isModelCreated(): boolean { return this.getNotebookModel() !== undefined; } private getNotebookModel(): INotebookModel { let editor = this.notebookService.listNotebookEditors().find(n => n.id === this.notebookUri.toString()); if (editor) { return editor.model; } return undefined; } get onDidChangeDirty(): Event { return this._onDidChangeDirty.event; } } export class NotebookInput extends EditorInput { public static ID: string = 'workbench.editorinputs.notebookInput'; private _providerId: string; private _providers: string[]; private _standardKernels: IStandardKernelWithProvider[]; private _connectionProfile: IConnectionProfile; private _defaultKernel: azdata.nb.IKernelSpec; private _isTrusted: boolean = false; public hasBootstrapped = false; // Holds the HTML content for the editor when the editor discards this input and loads another private _parentContainer: HTMLElement; private readonly _layoutChanged: Emitter = this._register(new Emitter()); private _model: NotebookEditorModel; private _untitledEditorModel: UntitledEditorModel; private _contentManager: IContentManager; private _providersLoaded: Promise; constructor(private _title: string, private resource: URI, private _textInput: UntitledEditorInput, @ITextModelService private textModelService: ITextModelService, @IInstantiationService private instantiationService: IInstantiationService, @INotebookService private notebookService: INotebookService, @IExtensionService private extensionService: IExtensionService ) { super(); this.resource = resource; this._standardKernels = []; this._providersLoaded = this.assignProviders(); } public get textInput(): UntitledEditorInput { return this._textInput; } public confirmSave(): Promise { return this._textInput.confirmSave(); } public revert(): Promise { return this._textInput.revert(); } public get notebookUri(): URI { return this.resource; } public get contentManager(): IContentManager { if (!this._contentManager) { this._contentManager = new NotebookEditorContentManager(this); } return this._contentManager; } public getName(): string { if (!this._title) { this._title = resources.basenameOrAuthority(this.resource); } return this._title; } public async getProviderInfo(): Promise { await this._providersLoaded; return { providerId: this._providerId ? this._providerId : DEFAULT_NOTEBOOK_PROVIDER, providers: this._providers ? this._providers : [DEFAULT_NOTEBOOK_PROVIDER] }; } public get isTrusted(): boolean { return this._isTrusted; } public set isTrusted(value: boolean) { this._isTrusted = value; } public set connectionProfile(value: IConnectionProfile) { this._connectionProfile = value; } public get connectionProfile(): IConnectionProfile { return this._connectionProfile; } public get standardKernels(): IStandardKernelWithProvider[] { return this._standardKernels; } public save(): Promise { let options: ISaveOptions = { force: false }; return this._model.save(options); } public set standardKernels(value: IStandardKernelWithProvider[]) { value.forEach(kernel => { this._standardKernels.push({ connectionProviderIds: kernel.connectionProviderIds, name: kernel.name, displayName: kernel.displayName, notebookProvider: kernel.notebookProvider }); }); } public get defaultKernel(): azdata.nb.IKernelSpec { return this._defaultKernel; } public set defaultKernel(kernel: azdata.nb.IKernelSpec) { this._defaultKernel = kernel; } get layoutChanged(): Event { return this._layoutChanged.event; } doChangeLayout(): any { this._layoutChanged.fire(); } public getTypeId(): string { return NotebookInput.ID; } getResource(): URI { return this.resource; } public get untitledEditorModel() : UntitledEditorModel { return this._untitledEditorModel; } public set untitledEditorModel(value : UntitledEditorModel) { this._untitledEditorModel = value; } async resolve(): Promise { if (this._model && this._model.isModelCreated()) { return Promise.resolve(this._model); } else { let textOrUntitledEditorModel: UntitledEditorModel | IEditorModel; if (this.resource.scheme === Schemas.untitled) { textOrUntitledEditorModel = this._untitledEditorModel ? this._untitledEditorModel : await this._textInput.resolve(); } else { const textEditorModelReference = await this.textModelService.createModelReference(this.resource); textOrUntitledEditorModel = await textEditorModelReference.object.load(); } this._model = this.instantiationService.createInstance(NotebookEditorModel, this.resource, textOrUntitledEditorModel); this._model.onDidChangeDirty(() => this._onDidChangeDirty.fire()); return this._model; } } private async assignProviders(): Promise { await this.extensionService.whenInstalledExtensionsRegistered(); let providerIds: string[] = getProvidersForFileName(this._title, this.notebookService); if (providerIds && providerIds.length > 0) { this._providerId = providerIds.filter(provider => provider !== DEFAULT_NOTEBOOK_PROVIDER)[0]; this._providers = providerIds; this._standardKernels = []; this._providers.forEach(provider => { let standardKernels = getStandardKernelsForProvider(provider, this.notebookService); this._standardKernels.push(...standardKernels); }); } } public dispose(): void { this._disposeContainer(); super.dispose(); } private _disposeContainer() { if (!this._parentContainer) { return; } let parentNode = this._parentContainer.parentNode; if (parentNode) { parentNode.removeChild(this._parentContainer); this._parentContainer = null; } } set container(container: HTMLElement) { this._disposeContainer(); this._parentContainer = container; } get container(): HTMLElement { return this._parentContainer; } /** * An editor that is dirty will be asked to be saved once it closes. */ isDirty(): boolean { if (this._model) { return this._model.isDirty; } return false; } /** * Sets active editor with dirty value. * @param isDirty boolean value to set editor dirty */ setDirty(isDirty: boolean): void { if (this._model) { this._model.setDirty(isDirty); } } updateModel(): void { this._model.updateModel(); } public matches(otherInput: any): boolean { if (super.matches(otherInput) === true) { return true; } if (otherInput instanceof NotebookInput) { const otherNotebookEditorInput = otherInput; // Compare by resource return otherNotebookEditorInput.notebookUri.toString() === this.notebookUri.toString(); } return false; } } class NotebookEditorContentManager implements IContentManager { constructor(private notebookInput: NotebookInput) { } async loadContent(): Promise { let notebookEditorModel = await this.notebookInput.resolve(); let contentManager = new LocalContentManager(); let contents = await contentManager.loadFromContentString(notebookEditorModel.contentString); return contents; } }