diff --git a/src/vs/base/browser/ui/list/listView.ts b/src/vs/base/browser/ui/list/listView.ts index 2600b3aacf6..bb274a1b8b0 100644 --- a/src/vs/base/browser/ui/list/listView.ts +++ b/src/vs/base/browser/ui/list/listView.ts @@ -412,7 +412,11 @@ export class ListView implements ISpliceable, IDisposable { private eventuallyUpdateScrollDimensions(): void { this._scrollHeight = this.contentHeight; - this.rowsContainer.style.height = `${this._scrollHeight}px`; + if (this.scrollableElement.getScrollDimensions().height > this._scrollHeight) { + this.rowsContainer.style.height = `${this.scrollableElement.getScrollDimensions().height}`; + } else { + this.rowsContainer.style.height = `${this._scrollHeight}px`; + } if (!this.scrollableElementUpdateDisposable) { this.scrollableElementUpdateDisposable = DOM.scheduleAtNextAnimationFrame(() => { diff --git a/src/vs/editor/common/modes.ts b/src/vs/editor/common/modes.ts index 9aba0ecbfdd..7e143e679d2 100644 --- a/src/vs/editor/common/modes.ts +++ b/src/vs/editor/common/modes.ts @@ -1498,6 +1498,7 @@ export type IOutput = IStreamOutput | any; export interface ICell { handle: number; source: string[]; + language: string; cell_type: 'markdown' | 'code'; outputs: IOutput[]; onDidChangeOutputs?: Event; @@ -1523,8 +1524,10 @@ export interface IMetadata { export interface INotebook { handle: number; // metadata: IMetadata; + readonly uri: URI; cells: ICell[]; onDidChangeCells?: Event; + onWillDispose(listener: () => void): IDisposable; } export interface CodeLens { diff --git a/src/vs/vscode.proposed.d.ts b/src/vs/vscode.proposed.d.ts index 21ecf7e0c28..922083d151b 100644 --- a/src/vs/vscode.proposed.d.ts +++ b/src/vs/vscode.proposed.d.ts @@ -1349,6 +1349,7 @@ declare module 'vscode' { export interface NotebookProvider { resolveNotebook(editor: NotebookEditor): Promise; executeCell(document: NotebookDocument, cell: NotebookCell | undefined): Promise; + save(document: NotebookDocument): Promise; } namespace window { diff --git a/src/vs/workbench/api/browser/mainThreadNotebook.ts b/src/vs/workbench/api/browser/mainThreadNotebook.ts index 37befffcf30..d418f1043be 100644 --- a/src/vs/workbench/api/browser/mainThreadNotebook.ts +++ b/src/vs/workbench/api/browser/mainThreadNotebook.ts @@ -5,7 +5,7 @@ import { extHostNamedCustomer } from 'vs/workbench/api/common/extHostCustomers'; import { MainContext, MainThreadNotebookShape, NotebookExtensionDescription, IExtHostContext, ExtHostNotebookShape, ExtHostContext } from '../common/extHost.protocol'; -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { INotebookService, IMainNotebookController } from 'vs/workbench/contrib/notebook/browser/notebookService'; import { INotebook, IMetadata, ICell, IOutput } from 'vs/editor/common/modes'; @@ -29,6 +29,7 @@ export class MainThreadCell implements ICell { constructor( public handle: number, public source: string[], + public language: string, public cell_type: 'markdown' | 'code', outputs: IOutput[] ) { @@ -36,8 +37,9 @@ export class MainThreadCell implements ICell { } } -export class MainThreadNotebookDocument implements INotebook { - +export class MainThreadNotebookDocument extends Disposable implements INotebook { + private readonly _onWillDispose: Emitter = this._register(new Emitter()); + public readonly onWillDispose: Event = this._onWillDispose.event; private readonly _onDidChangeCells = new Emitter(); get onDidChangeCells(): Event { return this._onDidChangeCells.event; } private _mapping: Map = new Map(); @@ -47,8 +49,9 @@ export class MainThreadNotebookDocument implements INotebook { constructor( private readonly _proxy: ExtHostNotebookShape, public handle: number, - public resource: URI + public uri: URI ) { + super(); this.cells = []; } @@ -65,7 +68,7 @@ export class MainThreadNotebookDocument implements INotebook { if (this.cells.length === 0) { newCells.forEach(cell => { - let mainCell = new MainThreadCell(cell.handle, cell.source, cell.cell_type, cell.outputs); + let mainCell = new MainThreadCell(cell.handle, cell.source, cell.language, cell.cell_type, cell.outputs); this._mapping.set(cell.handle, mainCell); this.cells.push(mainCell); }); @@ -84,6 +87,11 @@ export class MainThreadNotebookDocument implements INotebook { updateActiveCell(handle: number) { this.activeCell = this._mapping.get(handle); } + + dispose() { + this._onWillDispose.fire(); + super.dispose(); + } } @extHostNamedCustomer(MainContext.MainThreadNotebook) diff --git a/src/vs/workbench/api/common/extHostNotebook.ts b/src/vs/workbench/api/common/extHostNotebook.ts index c664cda9f98..28fa316d06a 100644 --- a/src/vs/workbench/api/common/extHostNotebook.ts +++ b/src/vs/workbench/api/common/extHostNotebook.ts @@ -84,6 +84,7 @@ export class ExtHostNotebookDocument implements vscode.NotebookDocument { this._proxy.$updateNotebookCell(this.handle, { handle: cell.handle, source: cell.source, + language: cell.language, cell_type: cell.cell_type, outputs: cell.outputs }); @@ -107,6 +108,7 @@ export class ExtHostNotebookDocument implements vscode.NotebookDocument { return await this._proxy.$updateNotebookCells(this.handle, this.cells.map(cell => ({ handle: cell.handle, source: cell.source, + language: cell.language, cell_type: cell.cell_type, outputs: cell.outputs }))); @@ -145,7 +147,7 @@ export class ExtHostNotebookEditor implements vscode.NotebookEditor { private readonly documentsProxy: MainThreadDocumentsShape, private _documentsAndEditors: ExtHostDocumentsAndEditors ) { - let regex = new RegExp(`${viewType}-(\\d+)-(\\d+)`); + let regex = new RegExp(`notebook\\+${viewType}-(\\d+)-(\\d+)`); this._documentsAndEditors.onDidAddDocuments(documents => { for (const data of documents) { let textDocument = data.document; diff --git a/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts b/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts index 37f0aaa4c65..03bec14009e 100644 --- a/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts +++ b/src/vs/workbench/contrib/notebook/browser/cellRenderer.ts @@ -160,11 +160,12 @@ export class ViewCell { getTextModel(): ITextModel { if (!this._textModel) { + let mode = this.modeService.create(this.cell.language); let ext = this.cellType === 'markdown' ? 'md' : 'py'; let resource = URI.parse(`notebookcell-${Date.now()}.${ext}`); - resource = resource.with({ authority: `${this.viewType}-${this.notebookHandle}-${this.cell.handle}` }); + resource = resource.with({ authority: `notebook+${this.viewType}-${this.notebookHandle}-${this.cell.handle}` }); let content = this.cell.source.join('\n'); - this._textModel = this.modelService.createModel(content, this.modeService.createByFilepathOrFirstLine(resource), resource, false); + this._textModel = this.modelService.createModel(content, mode, resource, false); } return this._textModel; diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index 31c883da7d3..6b57db6b3b4 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -6,14 +6,13 @@ import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors'; import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; -import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { NotebookEditor, NOTEBOOK_EDITOR_FOCUSED } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { IEditorService, IOpenEditorOverride } from 'vs/workbench/services/editor/common/editorService'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { IEditorInput } from 'vs/workbench/common/editor'; import { IEditorOptions, ITextEditorOptions } from 'vs/platform/editor/common/editor'; -import { endsWith } from 'vs/base/common/strings'; import { IEditorGroup } from 'vs/workbench/services/editor/common/editorGroupsService'; import { LifecyclePhase } from 'vs/platform/lifecycle/common/lifecycle'; import { INotebookService, NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService'; @@ -22,8 +21,6 @@ import { IWorkbenchActionRegistry, Extensions as WorkbenchActionExtensions } fro import { IActiveCodeEditor, isDiffEditor, isCodeEditor } from 'vs/editor/browser/editorBrowser'; import { Action } from 'vs/base/common/actions'; import { SyncActionDescriptor, MenuRegistry, MenuId } from 'vs/platform/actions/common/actions'; -import { ResourceContextKey } from 'vs/workbench/common/resources'; -import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService'; Registry.as(EditorExtensions.Editors).registerEditor( EditorDescriptor.create( @@ -151,7 +148,7 @@ export class NotebookContribution implements IWorkbenchContribution { }, order: -1, group: 'navigation', - when: ResourceContextKey.Extension.isEqualTo('.ipynb') + when: NOTEBOOK_EDITOR_FOCUSED }); workbenchActionsRegistry.registerWorkbenchAction(SyncActionDescriptor.create(ExecuteNotebookCellAction, ExecuteNotebookCellAction.ID, ExecuteNotebookCellAction.LABEL), 'Execute Notebook Cell', 'Notebook'); @@ -164,7 +161,7 @@ export class NotebookContribution implements IWorkbenchContribution { }, order: -1, group: 'navigation', - when: ResourceContextKey.Extension.isEqualTo('.ipynb') + when: NOTEBOOK_EDITOR_FOCUSED }); } diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.css b/src/vs/workbench/contrib/notebook/browser/notebook.css index 812430bf8dc..c608c802b27 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/notebook.css @@ -17,9 +17,9 @@ white-space: initial; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > div.cell { +/* .monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > div.cell { transform: translate3d(0, 0, 0); -} +} */ .monaco-workbench .part.editor > .content .notebook-editor .cell-list-container { position: relative; diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index 8e80b370794..8784a0edfc1 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -31,10 +31,13 @@ import { IConfigurationService } from 'vs/platform/configuration/common/configur import { getZoomLevel } from 'vs/base/browser/browser'; import { IEnvironmentService } from 'vs/platform/environment/common/environment'; import { INotebook } from 'vs/editor/common/modes'; +import { IContextKeyService, IContextKey, RawContextKey } from 'vs/platform/contextkey/common/contextkey'; const $ = DOM.$; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; +export const NOTEBOOK_EDITOR_FOCUSED = new RawContextKey('notebookEditorFocused', false); + interface INotebookEditorViewState { editingCells: { [key: number]: boolean }; } @@ -56,6 +59,7 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { private fontInfo: BareFontInfo | undefined; private relayoutDisposable: IDisposable | null = null; private dimension: DOM.Dimension | null = null; + private editorFocus: IContextKey | null = null; constructor( @ITelemetryService telemetryService: ITelemetryService, @@ -68,7 +72,8 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { @INotebookService private notebookService: INotebookService, @IEditorGroupsService editorGroupService: IEditorGroupsService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IEnvironmentService private readonly environmentSerice: IEnvironmentService + @IEnvironmentService private readonly environmentSerice: IEnvironmentService, + @IContextKeyService private readonly contextKeyService: IContextKeyService ) { super(NotebookEditor.ID, telemetryService, themeService, storageService); @@ -87,6 +92,14 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { this.rootElement = DOM.append(parent, $('.notebook-editor')); this.createBody(this.rootElement); this.generateFontInfo(); + this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService); + this._register(this.onDidFocus(() => { + this.editorFocus?.set(true); + })); + + this._register(this.onDidBlur(() => { + this.editorFocus?.set(false); + })); } private generateFontInfo(): void { @@ -343,6 +356,7 @@ export class NotebookEditor extends BaseEditor implements NotebookHandler { let newCell = new ViewCell(this.viewType!, this.notebook!.handle, { handle: -1, cell_type: type, + language: '', source: [], outputs: [] }, false, this.modelService, this.modeService); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookService.ts b/src/vs/workbench/contrib/notebook/browser/notebookService.ts index 47a6a94b051..836a22aea24 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookService.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookService.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Disposable } from 'vs/base/common/lifecycle'; +import { Disposable, IDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { INotebook } from 'vs/editor/common/modes'; import { URI } from 'vs/base/common/uri'; @@ -11,6 +11,10 @@ import { notebookExtensionPoint } from 'vs/workbench/contrib/notebook/browser/ex import { NotebookProviderInfo } from 'vs/workbench/contrib/notebook/common/notebookProvider'; import { NotebookExtensionDescription } from 'vs/workbench/api/common/extHost.protocol'; +function MODEL_ID(resource: URI): string { + return resource.toString(); +} + export const INotebookService = createDecorator('notebookService'); export interface IMainNotebookController { @@ -58,15 +62,32 @@ export class NotebookInfoStore { } } +class ModelData implements IDisposable { + private readonly _modelEventListeners = new DisposableStore(); + + constructor( + public model: INotebook, + onWillDispose: (model: INotebook) => void + ) { + this._modelEventListeners.add(model.onWillDispose(() => onWillDispose(model))); + } + + public dispose(): void { + this._modelEventListeners.dispose(); + } +} + export class NotebookService extends Disposable implements INotebookService { _serviceBrand: undefined; private readonly _notebookProviders = new Map(); public notebookProviderInfoStore: NotebookInfoStore = new NotebookInfoStore(); + private readonly _models: { [modelId: string]: ModelData; }; constructor() { super(); + this._models = {}; notebookExtensionPoint.setHandler((extensions) => { this.notebookProviderInfoStore.clear(); @@ -96,7 +117,19 @@ export class NotebookService extends Disposable implements INotebookService { let provider = this._notebookProviders.get(viewType); if (provider) { - return provider.controller.resolveNotebook(viewType, uri); + let notebookModel = await provider.controller.resolveNotebook(viewType, uri); + + if (notebookModel) { + // new notebook model created + const modelId = MODEL_ID(uri); + + const modelData = new ModelData( + notebookModel, + (model) => this._onWillDispose(model), + ); + this._models[modelId] = modelData; + return modelData.model; + } } return; @@ -140,4 +173,14 @@ export class NotebookService extends Disposable implements INotebookService { return ret; } + + private _onWillDispose(model: INotebook): void { + let modelId = MODEL_ID(model.uri); + let modelData = this._models[modelId]; + + delete this._models[modelId]; + modelData.dispose(); + + // this._onModelRemoved.fire(model); + } }