diff --git a/src/vs/workbench/contrib/notebook/browser/constants.ts b/src/vs/workbench/contrib/notebook/browser/constants.ts index fb156b10bb1..e65cd871273 100644 --- a/src/vs/workbench/contrib/notebook/browser/constants.ts +++ b/src/vs/workbench/contrib/notebook/browser/constants.ts @@ -3,6 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +// Scrollable Element + +export const SCROLLABLE_ELEMENT_PADDING_TOP = 16; + // Cell sizing related export const CELL_MARGIN = 20; export const CELL_RUN_GUTTER = 32; @@ -17,3 +21,4 @@ export const EDITOR_TOP_MARGIN = 0; // Top and bottom padding inside the monaco editor in a cell, which are included in `cell.editorHeight` export const EDITOR_TOP_PADDING = 12; export const EDITOR_BOTTOM_PADDING = 12; + diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts index c4620b3a197..641ff94306b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/coreActions.ts @@ -407,7 +407,7 @@ registerAction2(class extends Action2 { export function getActiveNotebookEditor(editorService: IEditorService): INotebookEditor | undefined { // TODO can `isNotebookEditor` be on INotebookEditor to avoid a circular dependency? const activeEditorPane = editorService.activeEditorPane as any | undefined; - return activeEditorPane?.isNotebookEditor ? activeEditorPane : undefined; + return activeEditorPane?.isNotebookEditor ? activeEditorPane.editorWidget : undefined; } async function runActiveCell(accessor: ServicesAccessor): Promise { diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts index 2e3dc5e0909..685144a83e4 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/toc/tocProvider.ts @@ -14,6 +14,7 @@ TableOfContentsProviderRegistry.register(NotebookEditor.ID, new class implements return undefined; } // return an entry per markdown header + const editorWidget = editor.editorWidget; const result: ITableOfContentsEntry[] = []; for (let cell of editor.viewModel.viewCells) { const content = cell.getText(); @@ -27,8 +28,8 @@ TableOfContentsProviderRegistry.register(NotebookEditor.ID, new class implements result.push({ label: matches[j].replace(/^[ \t]*(\#+)/, ''), reveal: () => { - editor.revealInCenterIfOutsideViewport(cell); - editor.selectElement(cell); + editorWidget.revealInCenterIfOutsideViewport(cell); + editorWidget.selectElement(cell); // editor.focusNotebookCell(cell, 'container'); } }); diff --git a/src/vs/workbench/contrib/notebook/browser/media/notebook.css b/src/vs/workbench/contrib/notebook/browser/media/notebook.css index e17833c1ff5..a9584f7cdf1 100644 --- a/src/vs/workbench/contrib/notebook/browser/media/notebook.css +++ b/src/vs/workbench/contrib/notebook/browser/media/notebook.css @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -.monaco-workbench .part.editor > .content .notebook-editor { +.monaco-workbench .notebookOverlay.notebook-editor { box-sizing: border-box; line-height: 22px; user-select: initial; @@ -17,44 +17,40 @@ white-space: initial; } -/* .monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element { - overflow: visible !important; -} */ - -.monaco-workbench .part.editor > .content .notebook-editor .simple-fr-find-part-wrapper.visible { +.notebookOverlay .simple-fr-find-part-wrapper.visible { z-index: 100; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .overflowingContentWidgets > div { +.notebookOverlay .cell-list-container .overflowingContentWidgets > div { z-index: 600 !important; /* @rebornix: larger than the editor title bar */ } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .monaco-list-rows { +.notebookOverlay .cell-list-container .monaco-list-rows { min-height: 100%; overflow: visible !important; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container { +.notebookOverlay .cell-list-container { position: relative; } -.monaco-workbench .part.editor > .content .notebook-editor.global-drag-active .webview { +.notebookOverlay.global-drag-active .webview { pointer-events: none; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container .webview-cover { +.notebookOverlay .cell-list-container .webview-cover { position: absolute; top: 0; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { cursor: default; overflow: visible !important; width: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image { position: absolute; top: -500px; z-index: 1000; @@ -62,55 +58,55 @@ padding-top: 8px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .notebook-cell-focus-indicator { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .notebook-cell-focus-indicator { top: 8px !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .notebook-cell-focus-indicator { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .notebook-cell-focus-indicator { bottom: 8px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .output { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .output { display: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image > .monaco-toolbar { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image > .monaco-toolbar { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-statusbar-container { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-statusbar-container { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-part { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-part { width: calc(100% - 32px); /* minus left gutter */ } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-container > div > div { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-editor-container > div > div { /* Rendered code content - show a single unwrapped line */ height: 20px; overflow: hidden; white-space: pre-wrap; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .cell.markdown { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image.markdown-cell-row .cell.markdown { white-space: nowrap; overflow: hidden; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell { display: flex; } -.monaco-workbench .part.editor > .content .notebook-editor .notebook-content-widgets { +.notebookOverlay .notebook-content-widgets { position: absolute; top: 0; left: 0; width: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor .output { +.notebookOverlay .output { padding-left: 8px; padding-right: 8px; user-select: text; @@ -119,22 +115,22 @@ box-sizing: border-box; } -.monaco-workbench .part.editor > .content .notebook-editor .output p { +.notebookOverlay .output p { white-space: initial; overflow-x: auto; margin: 0px; } -.monaco-workbench .part.editor > .content .notebook-editor .output > div.foreground { +.notebookOverlay .output > div.foreground { padding: 8px; box-sizing: border-box; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-drag-image .output .multi-mimetype-output { +.notebookOverlay .cell-drag-image .output .multi-mimetype-output { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor .output .multi-mimetype-output { +.notebookOverlay .output .multi-mimetype-output { position: absolute; top: 4px; left: -32px; @@ -143,27 +139,27 @@ cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error_message { +.notebookOverlay .output .error_message { color: red; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error > div { +.notebookOverlay .output .error > div { white-space: normal; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error pre.traceback { +.notebookOverlay .output .error pre.traceback { margin: 8px 0; } -.monaco-workbench .part.editor > .content .notebook-editor .output .error .traceback > span { +.notebookOverlay .output .error .traceback > span { display: block; } -.monaco-workbench .part.editor > .content .notebook-editor .output .display img { +.notebookOverlay .output .display img { max-width: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu { position: absolute; left: 0; top: 28px; @@ -174,33 +170,29 @@ } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .menu, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .menu { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .menu, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .menu { visibility: visible; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover { outline: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.selected, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused { outline: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu:hover { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu.mouseover, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .menu:hover { cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element { - padding-top: 16px; -} - -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { visibility: hidden; display: inline-block; position: absolute; @@ -211,7 +203,7 @@ z-index: 30; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item { width: 24px; height: 24px; display: flex; @@ -219,55 +211,55 @@ margin: 1px 2px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item .action-label { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar .action-item .action-label { display: flex; align-items: center; margin: auto; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container { +.notebookOverlay .cell-statusbar-container { height: 21px; font-size: 12px; display: flex; position: relative; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-status-left { +.notebookOverlay .cell-statusbar-container .cell-status-left { display: flex; flex-grow: 1; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-status-right { +.notebookOverlay .cell-statusbar-container .cell-status-right { padding-right: 12px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker { +.notebookOverlay .cell-statusbar-container .cell-language-picker { padding: 0px 6px; cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:hover { +.notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: rgba(255, 255, 255, 0.6); } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:hover { +.notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: rgba(255, 255, 255, 0.9); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:hover { +.vs-dark .notebookOverlay .cell-statusbar-container .cell-language-picker:hover { background-color: rgba(255, 255, 255, 0.15); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-language-picker:active { +.vs-dark .notebookOverlay .cell-statusbar-container .cell-language-picker:active { background-color: rgba(255, 255, 255, 0.2); } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-status-message { +.notebookOverlay .cell-statusbar-container .cell-status-message { display: flex; align-items: center; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-run-status { +.notebookOverlay .cell-statusbar-container .cell-run-status { height: 100%; display: flex; align-items: center; @@ -276,19 +268,19 @@ margin-right: 2px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .codicon { +.notebookOverlay .cell-statusbar-container .codicon { font-size: 14px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-run-status .codicon-check { +.notebookOverlay .cell-statusbar-container .cell-run-status .codicon-check { color: #89D185; } -.vs .monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container .cell-run-status .codicon-check { +.vs .notebookOverlay .cell-statusbar-container .cell-run-status .codicon-check { color: #388A34; } -.monaco-workbench .part.editor > .content .notebook-editor .cell-status-placeholder { +.notebookOverlay .cell-status-placeholder { position: absolute; left: 18px; color: #ccc9; @@ -298,32 +290,32 @@ top: 0px; } -.vs .monaco-workbench .part.editor > .content .notebook-editor .cell-status-placeholder { +.vs .notebookOverlay .cell-status-placeholder { color: #616161e6; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container { position: relative; height: 22px; flex-shrink: 0; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar { margin-top: 8px; visibility: hidden; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .codicon { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .monaco-toolbar .codicon { margin-right: 8px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell.runnable .run-button-container .monaco-toolbar, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell.runnable .run-button-container .monaco-toolbar { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell.runnable .run-button-container .monaco-toolbar, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell.runnable .run-button-container .monaco-toolbar, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell.runnable .run-button-container .monaco-toolbar { visibility: visible; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .execution-count-label { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell .run-button-container .execution-count-label { position: absolute; top: 2px; font-size: 10px; @@ -337,33 +329,33 @@ opacity: .6; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell .run-button-container .execution-count-label, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell .run-button-container .execution-count-label, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell .run-button-container .execution-count-label { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row:hover .cell .run-button-container .execution-count-label, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-output-hover .cell .run-button-container .execution-count-label, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused .cell .run-button-container .execution-count-label { visibility: hidden; } -.monaco-workbench .part.editor > .content .notebook-editor .cell .cell-editor-part { +.notebookOverlay .cell .cell-editor-part { position: relative; } -.monaco-workbench .part.editor > .content .notebook-editor .cell .monaco-progress-container { +.notebookOverlay .cell .monaco-progress-container { top: -5px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.focused > .monaco-toolbar, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.cell-output-hover > .monaco-toolbar, -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions:hover > .monaco-toolbar { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.focused > .monaco-toolbar, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions.cell-output-hover > .monaco-toolbar, +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-has-toolbar-actions:hover > .monaco-toolbar { visibility: visible; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list:not(.element-focused):focus:before { +.notebookOverlay > .cell-list-container > .monaco-list:not(.element-focused):focus:before { outline: none !important; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .notebook-cell-focus-indicator { +.notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator { display: block; content: ' '; position: absolute; @@ -377,13 +369,13 @@ cursor: pointer; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row:hover .notebook-cell-focus-indicator, -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator, -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.focused .notebook-cell-focus-indicator { +.notebookOverlay .monaco-list .monaco-list-row:hover .notebook-cell-focus-indicator, +.notebookOverlay .monaco-list .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator, +.notebookOverlay .monaco-list .monaco-list-row.focused .notebook-cell-focus-indicator { visibility: visible; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-editor-focus .cell-editor-part:before { +.notebookOverlay .monaco-list-row.cell-editor-focus .cell-editor-part:before { z-index: 20; content: ""; right: 0px; @@ -396,31 +388,31 @@ pointer-events: none; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator-top { +.notebookOverlay .monaco-list .monaco-list-row .cell-insertion-indicator-top { top: -15px; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator-bottom { +.notebookOverlay .monaco-list .monaco-list-row .cell-insertion-indicator-bottom { bottom: 13px; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-dragover-top .cell-insertion-indicator-top, -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-dragover-bottom .cell-insertion-indicator-bottom { +.notebookOverlay .monaco-list .monaco-list-row.cell-dragover-top .cell-insertion-indicator-top, +.notebookOverlay .monaco-list .monaco-list-row.cell-dragover-bottom .cell-insertion-indicator-bottom { opacity: 1; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator { +.notebookOverlay .monaco-list .monaco-list-row .cell-insertion-indicator { opacity: 0; transition: opacity 0.2s ease-in-out; position: absolute; height: 2px; } -.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row.cell-dragging { +.notebookOverlay .monaco-list .monaco-list-row.cell-dragging { opacity: 0.5; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container { position: absolute; display: flex; opacity: 0; @@ -429,28 +421,28 @@ padding: 0; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-bottom-toolbar-container { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.cell-drag-image .cell-bottom-toolbar-container { display: none; } -.monaco-workbench .part.editor > .content .notebook-editor.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within, -.monaco-workbench .part.editor > .content .notebook-editor.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { +.notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:focus-within, +.notebookOverlay.notebook-editor-editable > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container:hover { opacity: 1; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator { height: 1px; flex-grow: 1; align-self: center; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator-short { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .seperator-short { height: 1px; width: 16px; align-self: center; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .button { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container .button { display: flex; margin: 0 8px; padding: 0 8px; @@ -461,7 +453,7 @@ font-size: 12px; } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { +.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row .cell-bottom-toolbar-container span.codicon { text-align: center; font-size: 14px; color: inherit; @@ -470,34 +462,34 @@ /* markdown */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown img { +.notebookOverlay .cell.markdown img { max-width: 100%; max-height: 100%; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a { +.notebookOverlay .cell.markdown a { text-decoration: none; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a:hover { +.notebookOverlay .cell.markdown a:hover { text-decoration: underline; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a:focus, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown input:focus, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown select:focus, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown textarea:focus { +.notebookOverlay .cell.markdown a:focus, +.notebookOverlay .cell.markdown input:focus, +.notebookOverlay .cell.markdown select:focus, +.notebookOverlay .cell.markdown textarea:focus { outline: 1px solid -webkit-focus-ring-color; outline-offset: -1px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr { +.notebookOverlay .cell.markdown hr { border: 0; height: 2px; border-bottom: 2px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 { +.notebookOverlay .cell.markdown h1 { padding-bottom: 0.3em; line-height: 1.2; border-bottom-width: 1px; @@ -505,116 +497,116 @@ border-color: rgba(255, 255, 255, 0.18); } -.vs .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 { +.vs .notebookOverlay .cell.markdown h1 { border-color: rgba(0, 0, 0, 0.18); } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h2, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h3 { +.notebookOverlay .cell.markdown h1, +.notebookOverlay .cell.markdown h2, +.notebookOverlay .cell.markdown h3 { font-weight: normal; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div { +.notebookOverlay .cell.markdown div { width: 100%; } /* Adjust margin of first item in markdown cell */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div *:first-child { +.notebookOverlay .cell.markdown div *:first-child { margin-top: 4px; } /* h1 tags don't need top margin */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div h1:first-child { +.notebookOverlay .cell.markdown div h1:first-child { margin-top: 0; } /* Removes bottom margin when only one item exists in markdown cell */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div *:only-child, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div *:last-child { +.notebookOverlay .cell.markdown div *:only-child, +.notebookOverlay .cell.markdown div *:last-child { margin-bottom: 0; } /* makes all markdown cells consistent */ -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown div { +.notebookOverlay .cell.markdown div { min-height: 32px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table { +.notebookOverlay .cell.markdown table { border-collapse: collapse; border-spacing: 0; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table th, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table td { +.notebookOverlay .cell.markdown table th, +.notebookOverlay .cell.markdown table td { border: 1px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { +.notebookOverlay .cell.markdown table > thead > tr > th { text-align: left; border-bottom: 1px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > td, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > th, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { +.notebookOverlay .cell.markdown table > thead > tr > th, +.notebookOverlay .cell.markdown table > thead > tr > td, +.notebookOverlay .cell.markdown table > tbody > tr > th, +.notebookOverlay .cell.markdown table > tbody > tr > td { padding: 5px 10px; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr + tr > td { +.notebookOverlay .cell.markdown table > tbody > tr + tr > td { border-top: 1px solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown blockquote { +.notebookOverlay .cell.markdown blockquote { margin: 0 7px 0 5px; padding: 0 16px 0 10px; border-left-width: 5px; border-left-style: solid; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown code { +.notebookOverlay .cell.markdown code { font-family: Menlo, Monaco, Consolas, "Droid Sans Mono", "Courier New", monospace, "Droid Sans Fallback"; font-size: 1em; line-height: 1.357em; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown body.wordWrap pre { +.notebookOverlay .cell.markdown body.wordWrap pre { white-space: pre-wrap; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre:not(.hljs), -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre.hljs code > div { +.notebookOverlay .cell.markdown pre:not(.hljs), +.notebookOverlay .cell.markdown pre.hljs code > div { padding: 16px; border-radius: 3px; overflow: auto; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre code { +.notebookOverlay .cell.markdown pre code { color: var(--vscode-editor-foreground); tab-size: 4; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block { +.notebookOverlay .cell.markdown .latex-block { display: block; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex { +.notebookOverlay .cell.markdown .latex { vertical-align: middle; display: inline-block; } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex img, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block img { +.notebookOverlay .cell.markdown .latex img, +.notebookOverlay .cell.markdown .latex-block img { filter: brightness(0) invert(0) } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex img, -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown .latex-block img { +.vs-dark .notebookOverlay .cell.markdown .latex img, +.vs-dark .notebookOverlay .cell.markdown .latex-block img { filter: brightness(0) invert(1) } -.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container .notebook-folding-indicator { +.notebookOverlay > .cell-list-container .notebook-folding-indicator { position: absolute; top: 8px; left: 6px; @@ -623,38 +615,38 @@ /** Theming */ -/* .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { +/* .notebookOverlay .cell.markdown pre { background-color: rgba(220, 220, 220, 0.4); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { +.vs-dark .notebookOverlay .cell.markdown pre { background-color: rgba(10, 10, 10, 0.4); } -.hc-black .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown pre { +.hc-black .notebookOverlay .cell.markdown pre { background-color: rgb(0, 0, 0); } -.hc-black .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1 { +.hc-black .notebookOverlay .cell.markdown h1 { border-color: rgb(0, 0, 0); } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { +.notebookOverlay .cell.markdown table > thead > tr > th { border-color: rgba(0, 0, 0, 0.18); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > thead > tr > th { +.vs-dark .notebookOverlay .cell.markdown table > thead > tr > th { border-color: rgba(255, 255, 255, 0.18); } -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr, -.monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { +.notebookOverlay .cell.markdown h1, +.notebookOverlay .cell.markdown hr, +.notebookOverlay .cell.markdown table > tbody > tr > td { border-color: rgba(0, 0, 0, 0.18); } -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown h1, -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown hr, -.vs-dark .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown table > tbody > tr > td { +.vs-dark .notebookOverlay .cell.markdown h1, +.vs-dark .notebookOverlay .cell.markdown hr, +.vs-dark .notebookOverlay .cell.markdown table > tbody > tr > td { border-color: rgba(255, 255, 255, 0.18); } */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts index eb035c05aca..26916bceee7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts @@ -24,7 +24,7 @@ import { Registry } from 'vs/platform/registry/common/platform'; import { EditorDescriptor, Extensions as EditorExtensions, IEditorRegistry } from 'vs/workbench/browser/editor'; import { Extensions as WorkbenchExtensions, IWorkbenchContribution, IWorkbenchContributionsRegistry } from 'vs/workbench/common/contributions'; import { EditorInput, Extensions as EditorInputExtensions, IEditorInput, IEditorInputFactory, IEditorInputFactoryRegistry } from 'vs/workbench/common/editor'; -import { NotebookEditor, NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; +import { NotebookEditor } from 'vs/workbench/contrib/notebook/browser/notebookEditor'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; import { NotebookService } from 'vs/workbench/contrib/notebook/browser/notebookServiceImpl'; @@ -51,6 +51,7 @@ import 'vs/workbench/contrib/notebook/browser/contrib/marker/markerProvider'; import 'vs/workbench/contrib/notebook/browser/view/output/transforms/streamTransform'; import 'vs/workbench/contrib/notebook/browser/view/output/transforms/errorTransform'; import 'vs/workbench/contrib/notebook/browser/view/output/transforms/richTransform'; +import { NotebookEditorOptions } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; /*--------------------------------------------------------------------------------------------- */ diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index d6826fa8be0..acff0ca2503 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -3,144 +3,40 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { getZoomLevel } from 'vs/base/browser/browser'; import * as DOM from 'vs/base/browser/dom'; -import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; -import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { Color, RGBA } from 'vs/base/common/color'; -import { onUnexpectedError } from 'vs/base/common/errors'; +import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { combinedDisposable, DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; -import 'vs/css!./media/notebook'; -import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; -import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; -import { IPosition, Position } from 'vs/editor/common/core/position'; -import { Range } from 'vs/editor/common/core/range'; -import { ICompositeCodeEditor, IEditor } from 'vs/editor/common/editorCommon'; -import { IReadonlyTextBuffer } from 'vs/editor/common/model'; -import * as nls from 'vs/nls'; -import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; -import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { MutableDisposable } from 'vs/base/common/lifecycle'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { IStorageService } from 'vs/platform/storage/common/storage'; import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; -import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; -import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { IThemeService } from 'vs/platform/theme/common/themeService'; import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor'; import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor'; import { EditorOptions, IEditorCloseEvent, IEditorMemento } from 'vs/workbench/common/editor'; -import { CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_BOTTOM_PADDING, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants'; -import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, IEditableCellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; -import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; import { NotebookEditorInput } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput'; -import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; -import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; -import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; -import { CellDragAndDropController, CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; -import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; -import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; -import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; -import { CellKind, CellUri, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; -import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; -import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; -import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; -import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; +import { INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { NotebookEditorWidget } from 'vs/workbench/contrib/notebook/browser/notebookEditorWidget'; -const $ = DOM.$; const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState'; -export class NotebookEditorOptions extends EditorOptions { - - readonly cellOptions?: IResourceEditorInput; - - constructor(options: Partial) { - super(); - this.overwrite(options); - this.cellOptions = options.cellOptions; - } - - with(options: Partial): NotebookEditorOptions { - return new NotebookEditorOptions({ ...this, ...options }); - } -} - -export class NotebookCodeEditors implements ICompositeCodeEditor { - - private readonly _disposables = new DisposableStore(); - private readonly _onDidChangeActiveEditor = new Emitter(); - readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; - - constructor( - private _list: INotebookCellList, - private _renderedEditors: Map - ) { - _list.onDidChangeFocus(_e => this._onDidChangeActiveEditor.fire(this), undefined, this._disposables); - } - - dispose(): void { - this._onDidChangeActiveEditor.dispose(); - this._disposables.dispose(); - } - - get activeCodeEditor(): IEditor | undefined { - const [focused] = this._list.getFocusedElements(); - return this._renderedEditors.get(focused); - } -} - -export class NotebookEditor extends BaseEditor implements INotebookEditor { +export class NotebookEditor extends BaseEditor { static readonly ID: string = 'workbench.editor.notebook'; - private _rootElement!: HTMLElement; - private body!: HTMLElement; - private titleBar: HTMLElement | null = null; - private webview: BackLayerWebView | null = null; - private webviewTransparentCover: HTMLElement | null = null; - private list: INotebookCellList | undefined; - private control: ICompositeCodeEditor | undefined; - private renderedEditors: Map = new Map(); - private eventDispatcher: NotebookEventDispatcher | undefined; - private notebookViewModel: NotebookViewModel | undefined; - private localStore: DisposableStore = this._register(new DisposableStore()); private editorMemento: IEditorMemento; private readonly groupListener = this._register(new MutableDisposable()); - private fontInfo: BareFontInfo | undefined; - private dimension: DOM.Dimension | null = null; - private editorFocus: IContextKey | null = null; - private editorEditable: IContextKey | null = null; - private editorRunnable: IContextKey | null = null; - private editorExecutingNotebook: IContextKey | null = null; - private outputRenderer: OutputRenderer; - protected readonly _contributions: { [key: string]: INotebookEditorContribution; }; - private scrollBeyondLastLine: boolean; + private _widget: NotebookEditorWidget; constructor( @ITelemetryService telemetryService: ITelemetryService, @IThemeService themeService: IThemeService, @IInstantiationService private readonly instantiationService: IInstantiationService, @IStorageService storageService: IStorageService, - @INotebookService private notebookService: INotebookService, - @IEditorGroupsService editorGroupService: IEditorGroupsService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService - ) { + @IEditorGroupsService editorGroupService: IEditorGroupsService) { super(NotebookEditor.ID, telemetryService, themeService, storageService); + this._widget = this.instantiationService.createInstance(NotebookEditorWidget); this.editorMemento = this.getEditorMemento(editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); - this.outputRenderer = new OutputRenderer(this, this.instantiationService); - this._contributions = {}; - this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); - - this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration('editor.scrollBeyondLastLine')) { - this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); - if (this.dimension) { - this.layout(this.dimension); - } - } - }); } private readonly _onDidChangeModel = new Emitter(); @@ -148,12 +44,12 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { set viewModel(newModel: NotebookViewModel | undefined) { - this.notebookViewModel = newModel; + this._widget.viewModel = newModel; this._onDidChangeModel.fire(); } get viewModel() { - return this.notebookViewModel; + return this._widget.viewModel; } get minimumWidth(): number { return 375; } @@ -171,207 +67,21 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { return true; } - private updateEditorFocus() { - // Note - focus going to the webview will fire 'blur', but the webview element will be - // a descendent of the notebook editor root. - this.editorFocus?.set(DOM.isAncestor(document.activeElement, this.getDomNode())); - } protected createEditor(parent: HTMLElement): void { - this._rootElement = DOM.append(parent, $('.notebook-editor')); - this.createBody(this._rootElement); - this.generateFontInfo(); - this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService); - this.editorFocus.set(true); - this._register(this.onDidFocus(() => this.updateEditorFocus())); - this._register(this.onDidBlur(() => this.updateEditorFocus())); - - this.editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.contextKeyService); - this.editorEditable.set(true); - this.editorRunnable = NOTEBOOK_EDITOR_RUNNABLE.bindTo(this.contextKeyService); - this.editorRunnable.set(true); - this.editorExecutingNotebook = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.contextKeyService); - - const contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); - - for (const desc of contributions) { - try { - const contribution = this.instantiationService.createInstance(desc.ctor, this); - this._contributions[desc.id] = contribution; - } catch (err) { - onUnexpectedError(err); - } - } + this._widget.createEditor(parent); } - populateEditorTitlebar() { - for (let element: HTMLElement | null = this._rootElement.parentElement; element; element = element.parentElement) { - if (DOM.hasClass(element, 'editor-group-container')) { - // elemnet is editor group container - for (let i = 0; i < element.childElementCount; i++) { - const child = element.childNodes.item(i) as HTMLElement; - - if (DOM.hasClass(child, 'title')) { - this.titleBar = child; - break; - } - } - break; - } - } - } - - clearEditorTitlebarZindex() { - if (this.titleBar === null) { - this.populateEditorTitlebar(); - } - - if (this.titleBar) { - this.titleBar.style.zIndex = 'auto'; - } - } - - increaseEditorTitlebarZindex() { - if (this.titleBar === null) { - this.populateEditorTitlebar(); - } - - if (this.titleBar) { - this.titleBar.style.zIndex = '500'; - } - } - - private generateFontInfo(): void { - const editorOptions = this.configurationService.getValue('editor'); - this.fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); - } - - private createBody(parent: HTMLElement): void { - this.body = document.createElement('div'); - DOM.addClass(this.body, 'cell-list-container'); - this.createCellList(); - DOM.append(parent, this.body); - } - - private createCellList(): void { - DOM.addClass(this.body, 'cell-list-container'); - - const dndController = this._register(new CellDragAndDropController(this)); - const renders = [ - this.instantiationService.createInstance(CodeCellRenderer, this, this.renderedEditors, dndController), - this.instantiationService.createInstance(MarkdownCellRenderer, this.contextKeyService, this, dndController, this.renderedEditors), - ]; - - this.list = this.instantiationService.createInstance( - NotebookCellList, - 'NotebookCellList', - this.body, - this.instantiationService.createInstance(NotebookCellListDelegate), - renders, - this.contextKeyService, - { - setRowLineHeight: false, - setRowHeight: false, - supportDynamicHeights: true, - horizontalScrolling: false, - keyboardSupport: false, - mouseSupport: true, - multipleSelectionSupport: false, - enableKeyboardNavigation: true, - additionalScrollHeight: 0, - transformOptimization: false, - styleController: (_suffix: string) => { return this.list!; }, - overrideStyles: { - listBackground: editorBackground, - listActiveSelectionBackground: editorBackground, - listActiveSelectionForeground: foreground, - listFocusAndSelectionBackground: editorBackground, - listFocusAndSelectionForeground: foreground, - listFocusBackground: editorBackground, - listFocusForeground: foreground, - listHoverForeground: foreground, - listHoverBackground: editorBackground, - listHoverOutline: focusBorder, - listFocusOutline: focusBorder, - listInactiveSelectionBackground: editorBackground, - listInactiveSelectionForeground: foreground, - listInactiveFocusBackground: editorBackground, - listInactiveFocusOutline: editorBackground, - }, - accessibilityProvider: { - getAriaLabel() { return null; }, - getWidgetAriaLabel() { - return nls.localize('notebookTreeAriaLabel', "Notebook"); - } - } - }, - ); - - this.control = new NotebookCodeEditors(this.list, this.renderedEditors); - this.webview = this.instantiationService.createInstance(BackLayerWebView, this); - this.webview.webview.onDidBlur(() => this.updateEditorFocus()); - this.webview.webview.onDidFocus(() => this.updateEditorFocus()); - this._register(this.webview.onMessage(message => { - if (this.viewModel) { - this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.viewModel.uri, message); - } - })); - this.list.rowsContainer.appendChild(this.webview.element); - - this._register(this.list); - this._register(combinedDisposable(...renders)); - - // transparent cover - this.webviewTransparentCover = DOM.append(this.list.rowsContainer, $('.webview-cover')); - this.webviewTransparentCover.style.display = 'none'; - - this._register(DOM.addStandardDisposableGenericMouseDownListner(this._rootElement, (e: StandardMouseEvent) => { - if (DOM.hasClass(e.target, 'slider') && this.webviewTransparentCover) { - this.webviewTransparentCover.style.display = 'block'; - } - })); - - this._register(DOM.addStandardDisposableGenericMouseUpListner(this._rootElement, (e: StandardMouseEvent) => { - if (this.webviewTransparentCover) { - // no matter when - this.webviewTransparentCover.style.display = 'none'; - } - })); - - this._register(this.list.onMouseDown(e => { - if (e.element) { - this._onMouseDown.fire({ event: e.browserEvent, target: e.element }); - } - })); - - this._register(this.list.onMouseUp(e => { - if (e.element) { - this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); - } - })); - + get editorWidget() { + return this._widget; } getDomNode() { - return this._rootElement; + return this._widget.getShadowDomNode(); } getControl() { - return this.control; - } - - getInnerWebview(): Webview | undefined { - return this.webview?.webview; - } - - setVisible(visible: boolean, group?: IEditorGroup): void { - if (visible) { - this.increaseEditorTitlebarZindex(); - } else { - this.clearEditorTitlebarZindex(); - } - - super.setVisible(visible, group); + return this._widget.getControl(); } onWillHide() { @@ -379,15 +89,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { this.saveEditorViewState(this.input); } - this.editorFocus?.set(false); - if (this.webview) { - this.localStore.clear(); - this.list?.rowsContainer.removeChild(this.webview?.element); - this.webview?.dispose(); - this.webview = null; - } - - this.list?.clear(); + this._widget.onWillHide(); super.onHide(); } @@ -403,15 +105,13 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } if (editor === this.input) { - this.clearEditorTitlebarZindex(); this.saveEditorViewState(editor); } } focus() { super.focus(); - this.editorFocus?.set(true); - this.list?.domFocus(); + this._widget.focus(); } async setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise { @@ -422,225 +122,17 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { await super.setInput(input, options, token); const model = await input.resolve(); - if (this.notebookViewModel === undefined || !this.notebookViewModel.equal(model.notebook) || this.webview === null) { - this.detachModel(); - await this.attachModel(input, model); - } - - // reveal cell if editor options tell to do so - if (options instanceof NotebookEditorOptions && options.cellOptions) { - const cellOptions = options.cellOptions; - const cell = this.notebookViewModel!.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); - if (cell) { - this.selectElement(cell); - this.revealInCenterIfOutsideViewport(cell); - const editor = this.renderedEditors.get(cell)!; - if (editor) { - if (cellOptions.options?.selection) { - const { selection } = cellOptions.options; - editor.setSelection({ - ...selection, - endLineNumber: selection.endLineNumber || selection.startLineNumber, - endColumn: selection.endColumn || selection.startColumn - }); - } - if (!cellOptions.options?.preserveFocus) { - editor.focus(); - } - } - } - } + const viewState = this.loadTextEditorViewState(input); + this._widget.setModel(model, viewState, options); } clearInput(): void { super.clearInput(); } - private detachModel() { - this.localStore.clear(); - this.list?.detachViewModel(); - this.viewModel?.dispose(); - // avoid event - this.notebookViewModel = undefined; - this.webview?.clearInsets(); - this.webview?.clearPreloadsCache(); - this.list?.clear(); - } - - private updateForMetadata(): void { - this.editorEditable?.set(!!this.viewModel!.metadata?.editable); - this.editorRunnable?.set(!!this.viewModel!.metadata?.runnable); - DOM.toggleClass(this.getDomNode(), 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); - } - - private async attachModel(input: NotebookEditorInput, model: NotebookEditorModel) { - if (!this.webview) { - this.webview = this.instantiationService.createInstance(BackLayerWebView, this); - this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element); - } - - await this.webview.waitForInitialization(); - - this.eventDispatcher = new NotebookEventDispatcher(); - this.viewModel = this.instantiationService.createInstance(NotebookViewModel, input.viewType!, model.notebook, this.eventDispatcher, this.getLayoutInfo()); - this.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); - - this.updateForMetadata(); - this.localStore.add(this.eventDispatcher.onDidChangeMetadata((e) => { - this.updateForMetadata(); - })); - - // restore view states, including contributions - const viewState = this.loadTextEditorViewState(input); - - { - // restore view state - this.viewModel.restoreEditorViewState(viewState); - - // contribution state restore - - const contributionsState = viewState?.contributionsState || {}; - const keys = Object.keys(this._contributions); - for (let i = 0, len = keys.length; i < len; i++) { - const id = keys[i]; - const contribution = this._contributions[id]; - if (typeof contribution.restoreViewState === 'function') { - contribution.restoreViewState(contributionsState[id]); - } - } - } - - this.webview?.updateRendererPreloads(this.viewModel.renderers); - - this.localStore.add(this.list!.onWillScroll(e => { - this.webview!.updateViewScrollTop(-e.scrollTop, []); - this.webviewTransparentCover!.style.top = `${e.scrollTop}px`; - })); - - this.localStore.add(this.list!.onDidChangeContentHeight(() => { - DOM.scheduleAtNextAnimationFrame(() => { - const scrollTop = this.list?.scrollTop || 0; - const scrollHeight = this.list?.scrollHeight || 0; - this.webview!.element.style.height = `${scrollHeight}px`; - - if (this.webview?.insetMapping) { - let updateItems: { cell: CodeCellViewModel, output: IOutput, cellTop: number }[] = []; - let removedItems: IOutput[] = []; - this.webview?.insetMapping.forEach((value, key) => { - const cell = value.cell; - const viewIndex = this.list?.getViewIndex(cell); - - if (viewIndex === undefined) { - return; - } - - if (cell.outputs.indexOf(key) < 0) { - // output is already gone - removedItems.push(key); - } - - const cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; - if (this.webview!.shouldUpdateInset(cell, key, cellTop)) { - updateItems.push({ - cell: cell, - output: key, - cellTop: cellTop - }); - } - }); - - removedItems.forEach(output => this.webview?.removeInset(output)); - - if (updateItems.length) { - this.webview?.updateViewScrollTop(-scrollTop, updateItems); - } - } - }); - })); - - this.list!.attachViewModel(this.viewModel); - this.localStore.add(this.list!.onDidRemoveOutput(output => { - this.removeInset(output); - })); - this.localStore.add(this.list!.onDidHideOutput(output => { - this.hideInset(output); - })); - - this.list!.layout(); - - // restore list state at last, it must be after list layout - this.restoreListViewState(viewState); - } - - private restoreListViewState(viewState: INotebookEditorViewState | undefined): void { - if (viewState?.scrollPosition !== undefined) { - this.list!.scrollTop = viewState!.scrollPosition.top; - this.list!.scrollLeft = viewState!.scrollPosition.left; - } else { - this.list!.scrollTop = 0; - this.list!.scrollLeft = 0; - } - - const focusIdx = typeof viewState?.focus === 'number' ? viewState.focus : 0; - if (focusIdx < this.list!.length) { - this.list!.setFocus([focusIdx]); - this.list!.setSelection([focusIdx]); - } else if (this.list!.length > 0) { - this.list!.setFocus([0]); - } - - if (viewState?.editorFocused) { - this.list?.focusView(); - const cell = this.notebookViewModel?.viewCells[focusIdx]; - if (cell) { - cell.focusMode = CellFocusMode.Editor; - } - } - } - private saveEditorViewState(input: NotebookEditorInput): void { - if (this.group && this.notebookViewModel) { - const state = this.notebookViewModel.geteEditorViewState(); - if (this.list) { - state.scrollPosition = { left: this.list.scrollLeft, top: this.list.scrollTop }; - let cellHeights: { [key: number]: number } = {}; - for (let i = 0; i < this.viewModel!.length; i++) { - const elm = this.viewModel!.viewCells[i] as CellViewModel; - if (elm.cellKind === CellKind.Code) { - cellHeights[i] = elm.layoutInfo.totalHeight; - } else { - cellHeights[i] = 0; - } - } - - state.cellTotalHeights = cellHeights; - - const focus = this.list.getFocus()[0]; - if (typeof focus === 'number') { - const element = this.notebookViewModel!.viewCells[focus]; - const itemDOM = this.list?.domElementOfElement(element!); - let editorFocused = false; - if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { - editorFocused = true; - } - - state.editorFocused = editorFocused; - state.focus = focus; - } - } - - // Save contribution view states - const contributionsState: { [key: string]: any } = {}; - - const keys = Object.keys(this._contributions); - for (const id of keys) { - const contribution = this._contributions[id]; - if (typeof contribution.saveViewState === 'function') { - contributionsState[id] = contribution.saveViewState(); - } - } - - state.contributionsState = contributionsState; + if (this.group) { + const state = this._widget.getEditorViewState(); this.editorMemento.saveEditorState(this.group, input.resource, state); } } @@ -654,19 +146,7 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } layout(dimension: DOM.Dimension): void { - this.dimension = new DOM.Dimension(dimension.width, dimension.height); - DOM.toggleClass(this._rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600); - DOM.toggleClass(this._rootElement, 'narrow-width', dimension.width < 600); - DOM.size(this.body, dimension.width, dimension.height); - this.list?.updateOptions({ additionalScrollHeight: this.scrollBeyondLastLine ? dimension.height : 0 }); - this.list?.layout(dimension.height, dimension.width); - - if (this.webviewTransparentCover) { - this.webviewTransparentCover.style.height = `${dimension.height}px`; - this.webviewTransparentCover.style.width = `${dimension.width}px`; - } - - this.eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + this._widget.layout(dimension); } protected saveState(): void { @@ -681,556 +161,10 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { //#region Editor Features - selectElement(cell: ICellViewModel) { - this.list?.selectElement(cell); - // this.viewModel!.selectionHandles = [cell.handle]; - } - - revealInView(cell: ICellViewModel) { - this.list?.revealElementInView(cell); - } - - revealInCenterIfOutsideViewport(cell: ICellViewModel) { - this.list?.revealElementInCenterIfOutsideViewport(cell); - } - - revealInCenter(cell: ICellViewModel) { - this.list?.revealElementInCenter(cell); - } - - revealLineInView(cell: ICellViewModel, line: number): void { - this.list?.revealElementLineInView(cell, line); - } - - revealLineInCenter(cell: ICellViewModel, line: number) { - this.list?.revealElementLineInCenter(cell, line); - } - - revealLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number) { - this.list?.revealElementLineInCenterIfOutsideViewport(cell, line); - } - - revealRangeInView(cell: ICellViewModel, range: Range): void { - this.list?.revealElementRangeInView(cell, range); - } - - revealRangeInCenter(cell: ICellViewModel, range: Range): void { - this.list?.revealElementRangeInCenter(cell, range); - } - - revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void { - this.list?.revealElementRangeInCenterIfOutsideViewport(cell, range); - } - - setCellSelection(cell: ICellViewModel, range: Range): void { - this.list?.setCellSelection(cell, range); - } - - changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any { - return this.notebookViewModel?.changeDecorations(callback); - } - - setHiddenAreas(_ranges: ICellRange[]): boolean { - return this.list!.setHiddenAreas(_ranges, true); - } - - //#endregion - - //#region Mouse Events - private readonly _onMouseUp: Emitter = this._register(new Emitter()); - public readonly onMouseUp: Event = this._onMouseUp.event; - - private readonly _onMouseDown: Emitter = this._register(new Emitter()); - public readonly onMouseDown: Event = this._onMouseDown.event; - - //#endregion - - //#region Cell operations - async layoutNotebookCell(cell: ICellViewModel, height: number): Promise { - const viewIndex = this.list!.getViewIndex(cell); - if (viewIndex === undefined) { - // the cell is hidden - return; - } - - let relayout = (cell: ICellViewModel, height: number) => { - this.list?.updateElementHeight2(cell, height); - }; - - let r: () => void; - DOM.scheduleAtNextAnimationFrame(() => { - relayout(cell, height); - r(); - }); - - return new Promise(resolve => { r = resolve; }); - } - - insertNotebookCell(cell: ICellViewModel | undefined, type: CellKind, direction: 'above' | 'below' = 'above', initialText: string = '', ui: boolean = false): CellViewModel | null { - if (!this.notebookViewModel!.metadata.editable) { - return null; - } - - const newLanguages = this.notebookViewModel!.languages; - const language = (type === CellKind.Code && newLanguages && newLanguages.length) ? newLanguages[0] : 'markdown'; - const index = cell ? this.notebookViewModel!.getCellIndex(cell) : 0; - const nextIndex = ui ? this.notebookViewModel!.getNextVisibleCellIndex(index) : index + 1; - const insertIndex = cell ? - (direction === 'above' ? index : nextIndex) : - index; - const newCell = this.notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, true); - return newCell; - } - - private pushIfAbsent(positions: IPosition[], p: IPosition) { - const last = positions.length > 0 ? positions[positions.length - 1] : undefined; - if (!last || last.lineNumber !== p.lineNumber || last.column !== p.column) { - positions.push(p); - } - } - - /** - * Add split point at the beginning and the end; - * Move end of line split points to the beginning of the next line; - * Avoid duplicate split points - */ - private splitPointsToBoundaries(splitPoints: IPosition[], textBuffer: IReadonlyTextBuffer): IPosition[] | null { - const boundaries: IPosition[] = []; - const lineCnt = textBuffer.getLineCount(); - const getLineLen = (lineNumber: number) => { - return textBuffer.getLineLength(lineNumber); - }; - - // split points need to be sorted - splitPoints = splitPoints.sort((l, r) => { - const lineDiff = l.lineNumber - r.lineNumber; - const columnDiff = l.column - r.column; - return lineDiff !== 0 ? lineDiff : columnDiff; - }); - - // eat-up any split point at the beginning, i.e. we ignore the split point at the very beginning - this.pushIfAbsent(boundaries, new Position(1, 1)); - - for (let sp of splitPoints) { - if (getLineLen(sp.lineNumber) + 1 === sp.column && sp.lineNumber < lineCnt) { - sp = new Position(sp.lineNumber + 1, 1); - } - this.pushIfAbsent(boundaries, sp); - } - - // eat-up any split point at the beginning, i.e. we ignore the split point at the very end - this.pushIfAbsent(boundaries, new Position(lineCnt, getLineLen(lineCnt) + 1)); - - // if we only have two then they describe the whole range and nothing needs to be split - return boundaries.length > 2 ? boundaries : null; - } - - private computeCellLinesContents(cell: IEditableCellViewModel, splitPoints: IPosition[]): string[] | null { - const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, cell.textBuffer); - if (!rangeBoundaries) { - return null; - } - const newLineModels: string[] = []; - for (let i = 1; i < rangeBoundaries.length; i++) { - const start = rangeBoundaries[i - 1]; - const end = rangeBoundaries[i]; - - newLineModels.push(cell.textModel.getValueInRange(new Range(start.lineNumber, start.column, end.lineNumber, end.column))); - } - - return newLineModels; - } - - async splitNotebookCell(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return null; - } - - let splitPoints = cell.getSelectionsStartPosition(); - if (splitPoints && splitPoints.length > 0) { - await cell.resolveTextModel(); - - if (!cell.hasModel()) { - return null; - } - - let newLinesContents = this.computeCellLinesContents(cell, splitPoints); - if (newLinesContents) { - - // update the contents of the first cell - cell.textModel.applyEdits([ - { range: cell.textModel.getFullModelRange(), text: newLinesContents[0] } - ], true); - - // create new cells based on the new text models - const language = cell.model.language; - const kind = cell.cellKind; - let insertIndex = this.notebookViewModel!.getCellIndex(cell) + 1; - const newCells = []; - for (let j = 1; j < newLinesContents.length; j++, insertIndex++) { - newCells.push(this.notebookViewModel!.createCell(insertIndex, newLinesContents[j], language, kind, true)); - } - return newCells; - } - } - - return null; - } - - async joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return null; - } - - if (constraint && cell.cellKind !== constraint) { - return null; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - if (index === 0 && direction === 'above') { - return null; - } - - if (index === this.notebookViewModel!.length - 1 && direction === 'below') { - return null; - } - - if (direction === 'above') { - const above = this.notebookViewModel!.viewCells[index - 1]; - if (constraint && above.cellKind !== constraint) { - return null; - } - - await above.resolveTextModel(); - if (!above.hasModel()) { - return null; - } - - const insertContent = cell.getText(); - const aboveCellLineCount = above.textModel.getLineCount(); - const aboveCellLastLineEndColumn = above.textModel.getLineLength(aboveCellLineCount); - above.textModel.applyEdits([ - { range: new Range(aboveCellLineCount, aboveCellLastLineEndColumn + 1, aboveCellLineCount, aboveCellLastLineEndColumn + 1), text: insertContent } - ]); - - await this.deleteNotebookCell(cell); - return above; - } else { - const below = this.notebookViewModel!.viewCells[index + 1]; - if (constraint && below.cellKind !== constraint) { - return null; - } - - await cell.resolveTextModel(); - if (!cell.hasModel()) { - return null; - } - - const insertContent = below.getText(); - - const cellLineCount = cell.textModel.getLineCount(); - const cellLastLineEndColumn = cell.textModel.getLineLength(cellLineCount); - cell.textModel.applyEdits([ - { range: new Range(cellLineCount, cellLastLineEndColumn + 1, cellLineCount, cellLastLineEndColumn + 1), text: insertContent } - ]); - - await this.deleteNotebookCell(below); - return cell; - } - } - - async deleteNotebookCell(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - this.notebookViewModel!.deleteCell(index, true); - return true; - } - - async moveCellDown(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - if (index === this.notebookViewModel!.length - 1) { - return false; - } - - const newIdx = index + 1; - return this.moveCellToIndex(index, newIdx); - } - - async moveCellUp(cell: ICellViewModel): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - const index = this.notebookViewModel!.getCellIndex(cell); - if (index === 0) { - return false; - } - - const newIdx = index - 1; - return this.moveCellToIndex(index, newIdx); - } - - async moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { - if (!this.notebookViewModel!.metadata.editable) { - return false; - } - - if (cell === relativeToCell) { - return false; - } - - const originalIdx = this.notebookViewModel!.getCellIndex(cell); - const relativeToIndex = this.notebookViewModel!.getCellIndex(relativeToCell); - - let newIdx = direction === 'above' ? relativeToIndex : relativeToIndex + 1; - if (originalIdx < newIdx) { - newIdx--; - } - - return this.moveCellToIndex(originalIdx, newIdx); - } - - private async moveCellToIndex(index: number, newIdx: number): Promise { - if (index === newIdx) { - return false; - } - - if (!this.notebookViewModel!.moveCellToIdx(index, newIdx, true)) { - throw new Error('Notebook Editor move cell, index out of range'); - } - - let r: (val: boolean) => void; - DOM.scheduleAtNextAnimationFrame(() => { - this.list?.revealElementInView(this.notebookViewModel!.viewCells[newIdx]); - r(true); - }); - - return new Promise(resolve => { r = resolve; }); - } - - editNotebookCell(cell: CellViewModel): void { - if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).editable) { - return; - } - - cell.editState = CellEditState.Editing; - - this.renderedEditors.get(cell)?.focus(); - } - - saveNotebookCell(cell: ICellViewModel): void { - cell.editState = CellEditState.Preview; - } - - getActiveCell() { - let elements = this.list?.getFocusedElements(); - - if (elements && elements.length) { - return elements[0]; - } - - return undefined; - } - - cancelNotebookExecution(): void { - if (!this.notebookViewModel!.currentTokenSource) { - throw new Error('Notebook is not executing'); - } - - - this.notebookViewModel!.currentTokenSource.cancel(); - this.notebookViewModel!.currentTokenSource = undefined; - } - - async executeNotebook(): Promise { - if (!this.notebookViewModel!.metadata.runnable) { - return; - } - - // return this.progressService.showWhile(this._executeNotebook()); - return this._executeNotebook(); - } - - async _executeNotebook(): Promise { - if (this.notebookViewModel!.currentTokenSource) { - return; - } - - const tokenSource = new CancellationTokenSource(); - try { - this.editorExecutingNotebook!.set(true); - this.notebookViewModel!.currentTokenSource = tokenSource; - - for (let cell of this.notebookViewModel!.viewCells) { - if (cell.cellKind === CellKind.Code) { - await this._executeNotebookCell(cell, tokenSource); - } - } - } finally { - this.editorExecutingNotebook!.set(false); - this.notebookViewModel!.currentTokenSource = undefined; - tokenSource.dispose(); - } - } - - cancelNotebookCellExecution(cell: ICellViewModel): void { - if (!cell.currentTokenSource) { - throw new Error('Cell is not executing'); - } - - cell.currentTokenSource.cancel(); - cell.currentTokenSource = undefined; - } - - async executeNotebookCell(cell: ICellViewModel): Promise { - if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).runnable) { - return; - } - - const tokenSource = new CancellationTokenSource(); - try { - this._executeNotebookCell(cell, tokenSource); - } finally { - tokenSource.dispose(); - } - } - - private async _executeNotebookCell(cell: ICellViewModel, tokenSource: CancellationTokenSource): Promise { - try { - cell.currentTokenSource = tokenSource; - const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; - if (provider) { - const viewType = provider.id; - const notebookUri = CellUri.parse(cell.uri)?.notebook; - if (notebookUri) { - return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle, tokenSource.token); - } - } - } finally { - cell.currentTokenSource = undefined; - } - } - - focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { - if (focusItem === 'editor') { - this.selectElement(cell); - this.list?.focusView(); - - cell.editState = CellEditState.Editing; - cell.focusMode = CellFocusMode.Editor; - this.revealInCenterIfOutsideViewport(cell); - } else if (focusItem === 'output') { - this.selectElement(cell); - this.list?.focusView(); - - if (!this.webview) { - return; - } - this.webview.focusOutput(cell.id); - - cell.editState = CellEditState.Preview; - cell.focusMode = CellFocusMode.Container; - this.revealInCenterIfOutsideViewport(cell); - } else { - let itemDOM = this.list?.domElementOfElement(cell); - if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { - (document.activeElement as HTMLElement).blur(); - } - - cell.editState = CellEditState.Preview; - cell.focusMode = CellFocusMode.Container; - - this.selectElement(cell); - this.revealInCenterIfOutsideViewport(cell); - this.list?.focusView(); - } - } - - //#endregion - - //#region MISC - - getLayoutInfo(): NotebookLayoutInfo { - if (!this.list) { - throw new Error('Editor is not initalized successfully'); - } - - return { - width: this.dimension!.width, - height: this.dimension!.height, - fontInfo: this.fontInfo! - }; - } - - triggerScroll(event: IMouseWheelEvent) { - this.list?.triggerScrollFromMouseWheelEvent(event); - } - - createInset(cell: CodeCellViewModel, output: IOutput, shadowContent: string, offset: number) { - if (!this.webview) { - return; - } - - let preloads = this.notebookViewModel!.renderers; - - if (!this.webview!.insetMapping.has(output)) { - let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; - this.webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); - } else { - let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; - let scrollTop = this.list?.scrollTop || 0; - - this.webview!.updateViewScrollTop(-scrollTop, [{ cell: cell, output: output, cellTop: cellTop }]); - } - } - - removeInset(output: IOutput) { - if (!this.webview) { - return; - } - - this.webview!.removeInset(output); - } - - hideInset(output: IOutput) { - if (!this.webview) { - return; - } - - this.webview!.hideInset(output); - } - - getOutputRenderer(): OutputRenderer { - return this.outputRenderer; - } - - postMessage(message: any) { - this.webview?.webview.sendMessage(message); - } - - //#endregion - - //#region Editor Contributions - public getContribution(id: string): T { - return (this._contributions[id] || null); - } - //#endregion dispose() { - const keys = Object.keys(this._contributions); - for (let i = 0, len = keys.length; i < len; i++) { - const contributionId = keys[i]; - this._contributions[contributionId].dispose(); - } - + this._widget.dispose(); super.dispose(); } @@ -1241,111 +175,3 @@ export class NotebookEditor extends BaseEditor implements INotebookEditor { } } -const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground'; - -export const focusedCellIndicator = registerColor('notebook.focusedCellIndicator', { - light: new Color(new RGBA(102, 175, 224)), - dark: new Color(new RGBA(12, 125, 157)), - hc: new Color(new RGBA(0, 73, 122)) -}, nls.localize('notebook.focusedCellIndicator', "The color of the focused notebook cell indicator.")); - -export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { - dark: new Color(new RGBA(255, 255, 255, 0.06)), - light: new Color(new RGBA(237, 239, 249)), - hc: null -} - , nls.localize('notebook.outputContainerBackgroundColor', "The Color of the notebook output container background.")); - -// TODO currently also used for toolbar border, if we keep all of this, pick a generic name -export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeperator', { - dark: Color.fromHex('#808080').transparent(0.35), - light: Color.fromHex('#808080').transparent(0.35), - hc: contrastBorder -}, nls.localize('cellToolbarSeperator', "The color of seperator in Cell bottom toolbar")); - - -registerThemingParticipant((theme, collector) => { - const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); - if (color) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .monaco-editor-background, - .monaco-workbench .part.editor > .content .notebook-editor .cell .margin-view-overlays, - .monaco-workbench .part.editor > .content .notebook-editor .cell .cell-statusbar-container { background: ${color}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-drag-image .cell-editor-container > div { background: ${color} !important; }`); - } - const link = theme.getColor(textLinkForeground); - if (link) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output a, - .monaco-workbench .part.editor > .content .notebook-editor .cell.markdown a { color: ${link};} `); - } - const activeLink = theme.getColor(textLinkActiveForeground); - if (activeLink) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output a:hover, - .monaco-workbench .part.editor > .content .notebook-editor .cell .output a:active { color: ${activeLink}; }`); - } - const shortcut = theme.getColor(textPreformatForeground); - if (shortcut) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor code, - .monaco-workbench .part.editor > .content .notebook-editor .shortcut { color: ${shortcut}; }`); - } - const border = theme.getColor(contrastBorder); - if (border) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-editor { border-color: ${border}; }`); - } - const quoteBackground = theme.getColor(textBlockQuoteBackground); - if (quoteBackground) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor blockquote { background: ${quoteBackground}; }`); - } - const quoteBorder = theme.getColor(textBlockQuoteBorder); - if (quoteBorder) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor blockquote { border-color: ${quoteBorder}; }`); - } - - const containerBackground = theme.getColor(notebookOutputContainerColor); - if (containerBackground) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { background-color: ${containerBackground}; }`); - } - - const editorBackgroundColor = theme.getColor(editorBackground); - if (editorBackgroundColor) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-statusbar-container { border-top: solid 1px ${editorBackgroundColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > .monaco-toolbar { background-color: ${editorBackgroundColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); - } - - const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); - if (cellToolbarSeperator) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container .seperator { background-color: ${cellToolbarSeperator} }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container .seperator-short { background-color: ${cellToolbarSeperator} }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row > .monaco-toolbar { border: solid 1px ${cellToolbarSeperator}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row:hover .notebook-cell-focus-indicator, - .monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator { border-color: ${cellToolbarSeperator}; }`); - } - - const focusedCellIndicatorColor = theme.getColor(focusedCellIndicator); - if (focusedCellIndicatorColor) { - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.focused .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row .cell-insertion-indicator { background-color: ${focusedCellIndicatorColor}; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list-row.cell-editor-focus .cell-editor-part:before { outline: solid 1px ${focusedCellIndicatorColor}; }`); - } - - // const widgetShadowColor = theme.getColor(widgetShadow); - // if (widgetShadowColor) { - // collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { - // box-shadow: 0 0 8px 4px ${widgetShadowColor} - // }`) - // } - - // Cell Margin - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { padding-top: ${EDITOR_TOP_MARGIN}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .output { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-bottom-toolbar-container { width: calc(100% - ${CELL_MARGIN * 2 + CELL_RUN_GUTTER}px); margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); - - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .markdown-cell-row .cell .cell-editor-part { margin-left: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell .run-button-container { width: ${CELL_RUN_GUTTER}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .cell-insertion-indicator { left: ${CELL_MARGIN + CELL_RUN_GUTTER}px; right: ${CELL_MARGIN}px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .cell-drag-image .cell-editor-container > div { padding: ${EDITOR_TOP_PADDING}px 16px ${EDITOR_BOTTOM_PADDING}px 16px; }`); - collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor .monaco-list .monaco-list-row .notebook-cell-focus-indicator { left: ${CELL_MARGIN}px; }`); -}); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts new file mode 100644 index 00000000000..032e881d233 --- /dev/null +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditorWidget.ts @@ -0,0 +1,1313 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { getZoomLevel } from 'vs/base/browser/browser'; +import * as DOM from 'vs/base/browser/dom'; +import { IMouseWheelEvent, StandardMouseEvent } from 'vs/base/browser/mouseEvent'; +import { CancellationTokenSource } from 'vs/base/common/cancellation'; +import { Color, RGBA } from 'vs/base/common/color'; +import { onUnexpectedError } from 'vs/base/common/errors'; +import { Emitter, Event } from 'vs/base/common/event'; +import { combinedDisposable, DisposableStore, Disposable } from 'vs/base/common/lifecycle'; +import 'vs/css!./media/notebook'; +import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; +import { IEditorOptions } from 'vs/editor/common/config/editorOptions'; +import { BareFontInfo } from 'vs/editor/common/config/fontInfo'; +import { IPosition, Position } from 'vs/editor/common/core/position'; +import { Range } from 'vs/editor/common/core/range'; +import { ICompositeCodeEditor, IEditor } from 'vs/editor/common/editorCommon'; +import { IReadonlyTextBuffer } from 'vs/editor/common/model'; +import * as nls from 'vs/nls'; +import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; +import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; +import { IResourceEditorInput } from 'vs/platform/editor/common/editor'; +import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; +import { IStorageService, StorageScope } from 'vs/platform/storage/common/storage'; +import { contrastBorder, editorBackground, focusBorder, foreground, registerColor, textBlockQuoteBackground, textBlockQuoteBorder, textLinkActiveForeground, textLinkForeground, textPreformatForeground } from 'vs/platform/theme/common/colorRegistry'; +import { registerThemingParticipant } from 'vs/platform/theme/common/themeService'; +import { EditorMemento } from 'vs/workbench/browser/parts/editor/baseEditor'; +import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor'; +import { CELL_MARGIN, CELL_RUN_GUTTER, EDITOR_BOTTOM_PADDING, EDITOR_TOP_MARGIN, EDITOR_TOP_PADDING, SCROLLABLE_ELEMENT_PADDING_TOP } from 'vs/workbench/contrib/notebook/browser/constants'; +import { CellEditState, CellFocusMode, ICellRange, ICellViewModel, IEditableCellViewModel, INotebookCellList, INotebookEditor, INotebookEditorContribution, INotebookEditorMouseEvent, NotebookLayoutInfo, NOTEBOOK_EDITOR_EDITABLE, NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK, NOTEBOOK_EDITOR_FOCUSED, NOTEBOOK_EDITOR_RUNNABLE } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; +import { NotebookEditorExtensionsRegistry } from 'vs/workbench/contrib/notebook/browser/notebookEditorExtensions'; +import { NotebookCellList } from 'vs/workbench/contrib/notebook/browser/view/notebookCellList'; +import { OutputRenderer } from 'vs/workbench/contrib/notebook/browser/view/output/outputRenderer'; +import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/view/renderers/backLayerWebView'; +import { CellDragAndDropController, CodeCellRenderer, MarkdownCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellRenderer'; +import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel'; +import { NotebookEventDispatcher, NotebookLayoutChangedEvent } from 'vs/workbench/contrib/notebook/browser/viewModel/eventDispatcher'; +import { CellViewModel, IModelDecorationsChangeAccessor, INotebookEditorViewState, NotebookViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel'; +import { CellKind, CellUri, IOutput } from 'vs/workbench/contrib/notebook/common/notebookCommon'; +import { NotebookEditorModel } from 'vs/workbench/contrib/notebook/common/notebookEditorModel'; +import { INotebookService } from 'vs/workbench/contrib/notebook/common/notebookService'; +import { Webview } from 'vs/workbench/contrib/webview/browser/webview'; +import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils'; +import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; +import { ILayoutService } from 'vs/platform/layout/browser/layoutService'; +import { generateUuid } from 'vs/base/common/uuid'; +import { Memento, MementoObject } from 'vs/workbench/common/memento'; + +const $ = DOM.$; + +export class NotebookEditorOptions extends EditorOptions { + + readonly cellOptions?: IResourceEditorInput; + + constructor(options: Partial) { + super(); + this.overwrite(options); + this.cellOptions = options.cellOptions; + } + + with(options: Partial): NotebookEditorOptions { + return new NotebookEditorOptions({ ...this, ...options }); + } +} + +export class NotebookCodeEditors implements ICompositeCodeEditor { + + private readonly _disposables = new DisposableStore(); + private readonly _onDidChangeActiveEditor = new Emitter(); + readonly onDidChangeActiveEditor: Event = this._onDidChangeActiveEditor.event; + + constructor( + private _list: INotebookCellList, + private _renderedEditors: Map + ) { + _list.onDidChangeFocus(_e => this._onDidChangeActiveEditor.fire(this), undefined, this._disposables); + } + + dispose(): void { + this._onDidChangeActiveEditor.dispose(); + this._disposables.dispose(); + } + + get activeCodeEditor(): IEditor | undefined { + const [focused] = this._list.getFocusedElements(); + return this._renderedEditors.get(focused); + } +} + +export class NotebookEditorWidget extends Disposable implements INotebookEditor { + static readonly ID: string = 'workbench.editor.notebook'; + private static readonly EDITOR_MEMENTOS = new Map>(); + private _rootElement!: HTMLElement; + private overlayContainer!: HTMLElement; + private body!: HTMLElement; + private webview: BackLayerWebView | null = null; + private webviewTransparentCover: HTMLElement | null = null; + private list: INotebookCellList | undefined; + private control: ICompositeCodeEditor | undefined; + private renderedEditors: Map = new Map(); + private eventDispatcher: NotebookEventDispatcher | undefined; + private notebookViewModel: NotebookViewModel | undefined; + private localStore: DisposableStore = this._register(new DisposableStore()); + // private readonly groupListener = this._register(new MutableDisposable()); + private fontInfo: BareFontInfo | undefined; + private dimension: DOM.Dimension | null = null; + private editorFocus: IContextKey | null = null; + private editorEditable: IContextKey | null = null; + private editorRunnable: IContextKey | null = null; + private editorExecutingNotebook: IContextKey | null = null; + private outputRenderer: OutputRenderer; + protected readonly _contributions: { [key: string]: INotebookEditorContribution; }; + private scrollBeyondLastLine: boolean; + private readonly memento: Memento; + + + constructor( + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IStorageService storageService: IStorageService, + @INotebookService private notebookService: INotebookService, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @ILayoutService private readonly _layoutService: ILayoutService + ) { + super(); + this.memento = new Memento(NotebookEditorWidget.ID, storageService); + + this.outputRenderer = new OutputRenderer(this, this.instantiationService); + this._contributions = {}; + this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); + + this.configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration('editor.scrollBeyondLastLine')) { + this.scrollBeyondLastLine = this.configurationService.getValue('editor.scrollBeyondLastLine'); + if (this.dimension) { + this.layout(this.dimension); + } + } + }); + } + + private readonly _onDidChangeModel = new Emitter(); + readonly onDidChangeModel: Event = this._onDidChangeModel.event; + + + set viewModel(newModel: NotebookViewModel | undefined) { + this.notebookViewModel = newModel; + this._onDidChangeModel.fire(); + } + + get viewModel() { + return this.notebookViewModel; + } + + get minimumWidth(): number { return 375; } + get maximumWidth(): number { return Number.POSITIVE_INFINITY; } + + // these setters need to exist because this extends from BaseEditor + set minimumWidth(value: number) { /*noop*/ } + set maximumWidth(value: number) { /*noop*/ } + + + //#region Editor Core + + protected getEditorMemento(editorGroupService: IEditorGroupsService, key: string, limit: number = 10): IEditorMemento { + const mementoKey = `${this.getId()}${key}`; + + let editorMemento = NotebookEditorWidget.EDITOR_MEMENTOS.get(mementoKey); + if (!editorMemento) { + editorMemento = new EditorMemento(this.getId(), key, this.getMemento(StorageScope.WORKSPACE), limit, editorGroupService); + NotebookEditorWidget.EDITOR_MEMENTOS.set(mementoKey, editorMemento); + } + + return editorMemento; + } + + protected getMemento(scope: StorageScope): MementoObject { + return this.memento.getMemento(scope); + } + + + getId(): string { + return NotebookEditorWidget.ID; + } + + + public get isNotebookEditor() { + return true; + } + + private updateEditorFocus() { + // Note - focus going to the webview will fire 'blur', but the webview element will be + // a descendent of the notebook editor root. + this.editorFocus?.set(DOM.isAncestor(document.activeElement, this.overlayContainer)); + } + + public createEditor(parent: HTMLElement): void { + this._rootElement = DOM.append(parent, $('.notebook-editor')); + + this.overlayContainer = document.createElement('div'); + const id = generateUuid(); + this.overlayContainer.id = `notebook-${id}`; + this.overlayContainer.className = 'notebookOverlay'; + DOM.addClass(this.overlayContainer, 'notebook-editor'); + this.overlayContainer.style.visibility = 'hidden'; + + this._layoutService.container.appendChild(this.overlayContainer); + this.createBody(this.overlayContainer); + this.generateFontInfo(); + this.editorFocus = NOTEBOOK_EDITOR_FOCUSED.bindTo(this.contextKeyService); + this.editorFocus.set(true); + // this._register(this.onDidFocus(() => this.updateEditorFocus())); + // this._register(this.onDidBlur(() => this.updateEditorFocus())); + + this.editorEditable = NOTEBOOK_EDITOR_EDITABLE.bindTo(this.contextKeyService); + this.editorEditable.set(true); + this.editorRunnable = NOTEBOOK_EDITOR_RUNNABLE.bindTo(this.contextKeyService); + this.editorRunnable.set(true); + this.editorExecutingNotebook = NOTEBOOK_EDITOR_EXECUTING_NOTEBOOK.bindTo(this.contextKeyService); + + const contributions = NotebookEditorExtensionsRegistry.getEditorContributions(); + + for (const desc of contributions) { + try { + const contribution = this.instantiationService.createInstance(desc.ctor, this); + this._contributions[desc.id] = contribution; + } catch (err) { + onUnexpectedError(err); + } + } + } + + private generateFontInfo(): void { + const editorOptions = this.configurationService.getValue('editor'); + this.fontInfo = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()); + } + + private createBody(parent: HTMLElement): void { + this.body = document.createElement('div'); + DOM.addClass(this.body, 'cell-list-container'); + this.createCellList(); + DOM.append(parent, this.body); + } + + private createCellList(): void { + DOM.addClass(this.body, 'cell-list-container'); + + const dndController = this._register(new CellDragAndDropController(this)); + const renders = [ + this.instantiationService.createInstance(CodeCellRenderer, this, this.renderedEditors, dndController), + this.instantiationService.createInstance(MarkdownCellRenderer, this.contextKeyService, this, dndController, this.renderedEditors), + ]; + + this.list = this.instantiationService.createInstance( + NotebookCellList, + 'NotebookCellList', + this.body, + this.instantiationService.createInstance(NotebookCellListDelegate), + renders, + this.contextKeyService, + { + setRowLineHeight: false, + setRowHeight: false, + supportDynamicHeights: true, + horizontalScrolling: false, + keyboardSupport: false, + mouseSupport: true, + multipleSelectionSupport: false, + enableKeyboardNavigation: true, + additionalScrollHeight: 0, + transformOptimization: false, + styleController: (_suffix: string) => { return this.list!; }, + overrideStyles: { + listBackground: editorBackground, + listActiveSelectionBackground: editorBackground, + listActiveSelectionForeground: foreground, + listFocusAndSelectionBackground: editorBackground, + listFocusAndSelectionForeground: foreground, + listFocusBackground: editorBackground, + listFocusForeground: foreground, + listHoverForeground: foreground, + listHoverBackground: editorBackground, + listHoverOutline: focusBorder, + listFocusOutline: focusBorder, + listInactiveSelectionBackground: editorBackground, + listInactiveSelectionForeground: foreground, + listInactiveFocusBackground: editorBackground, + listInactiveFocusOutline: editorBackground, + }, + accessibilityProvider: { + getAriaLabel() { return null; }, + getWidgetAriaLabel() { + return nls.localize('notebookTreeAriaLabel', "Notebook"); + } + } + }, + ); + + this.control = new NotebookCodeEditors(this.list, this.renderedEditors); + this.webview = this.instantiationService.createInstance(BackLayerWebView, this); + this.webview.webview.onDidBlur(() => this.updateEditorFocus()); + this.webview.webview.onDidFocus(() => this.updateEditorFocus()); + this._register(this.webview.onMessage(message => { + if (this.viewModel) { + this.notebookService.onDidReceiveMessage(this.viewModel.viewType, this.viewModel.uri, message); + } + })); + this.list.rowsContainer.appendChild(this.webview.element); + + this._register(this.list); + this._register(combinedDisposable(...renders)); + + // transparent cover + this.webviewTransparentCover = DOM.append(this.list.rowsContainer, $('.webview-cover')); + this.webviewTransparentCover.style.display = 'none'; + + this._register(DOM.addStandardDisposableGenericMouseDownListner(this.overlayContainer, (e: StandardMouseEvent) => { + if (DOM.hasClass(e.target, 'slider') && this.webviewTransparentCover) { + this.webviewTransparentCover.style.display = 'block'; + } + })); + + this._register(DOM.addStandardDisposableGenericMouseUpListner(this.overlayContainer, () => { + if (this.webviewTransparentCover) { + // no matter when + this.webviewTransparentCover.style.display = 'none'; + } + })); + + this._register(this.list.onMouseDown(e => { + if (e.element) { + this._onMouseDown.fire({ event: e.browserEvent, target: e.element }); + } + })); + + this._register(this.list.onMouseUp(e => { + if (e.element) { + this._onMouseUp.fire({ event: e.browserEvent, target: e.element }); + } + })); + + } + + getShadowDomNode() { + return this._rootElement; + } + + getDomNode() { + return this.overlayContainer; + } + + getControl() { + return this.control; + } + + onWillHide() { + this.editorFocus?.set(false); + this.overlayContainer.style.visibility = 'hidden'; + this.overlayContainer.style.display = 'none'; + } + + getInnerWebview(): Webview | undefined { + return this.webview?.webview; + } + + + focus() { + this.editorFocus?.set(true); + this.list?.domFocus(); + } + + async setModel(model: NotebookEditorModel, viewState: INotebookEditorViewState | undefined, options: EditorOptions | undefined): Promise { + if (this.notebookViewModel === undefined || !this.notebookViewModel.equal(model.notebook) || this.webview === null) { + this.detachModel(); + await this.attachModel(model, viewState); + } + + // reveal cell if editor options tell to do so + if (options instanceof NotebookEditorOptions && options.cellOptions) { + const cellOptions = options.cellOptions; + const cell = this.notebookViewModel!.viewCells.find(cell => cell.uri.toString() === cellOptions.resource.toString()); + if (cell) { + this.selectElement(cell); + this.revealInCenterIfOutsideViewport(cell); + const editor = this.renderedEditors.get(cell)!; + if (editor) { + if (cellOptions.options?.selection) { + const { selection } = cellOptions.options; + editor.setSelection({ + ...selection, + endLineNumber: selection.endLineNumber || selection.startLineNumber, + endColumn: selection.endColumn || selection.startColumn + }); + } + if (!cellOptions.options?.preserveFocus) { + editor.focus(); + } + } + } + } + } + + private detachModel() { + this.localStore.clear(); + this.list?.detachViewModel(); + this.viewModel?.dispose(); + // avoid event + this.notebookViewModel = undefined; + this.webview?.clearInsets(); + this.webview?.clearPreloadsCache(); + this.list?.clear(); + } + + private updateForMetadata(): void { + this.editorEditable?.set(!!this.viewModel!.metadata?.editable); + this.editorRunnable?.set(!!this.viewModel!.metadata?.runnable); + DOM.toggleClass(this.overlayContainer, 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); + DOM.toggleClass(this.getDomNode(), 'notebook-editor-editable', !!this.viewModel!.metadata?.editable); + } + + private async attachModel(model: NotebookEditorModel, viewState: INotebookEditorViewState | undefined) { + if (!this.webview) { + this.webview = this.instantiationService.createInstance(BackLayerWebView, this); + this.list?.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element); + } + + await this.webview.waitForInitialization(); + + this.eventDispatcher = new NotebookEventDispatcher(); + this.viewModel = this.instantiationService.createInstance(NotebookViewModel, model.viewType, model.notebook, this.eventDispatcher, this.getLayoutInfo()); + this.eventDispatcher.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + + this.updateForMetadata(); + this.localStore.add(this.eventDispatcher.onDidChangeMetadata(() => { + this.updateForMetadata(); + })); + + // restore view states, including contributions + + { + // restore view state + this.viewModel.restoreEditorViewState(viewState); + + // contribution state restore + + const contributionsState = viewState?.contributionsState || {}; + const keys = Object.keys(this._contributions); + for (let i = 0, len = keys.length; i < len; i++) { + const id = keys[i]; + const contribution = this._contributions[id]; + if (typeof contribution.restoreViewState === 'function') { + contribution.restoreViewState(contributionsState[id]); + } + } + } + + this.webview?.updateRendererPreloads(this.viewModel.renderers); + + this.localStore.add(this.list!.onWillScroll(e => { + this.webview!.updateViewScrollTop(-e.scrollTop, []); + this.webviewTransparentCover!.style.top = `${e.scrollTop}px`; + })); + + this.localStore.add(this.list!.onDidChangeContentHeight(() => { + DOM.scheduleAtNextAnimationFrame(() => { + const scrollTop = this.list?.scrollTop || 0; + const scrollHeight = this.list?.scrollHeight || 0; + this.webview!.element.style.height = `${scrollHeight}px`; + + if (this.webview?.insetMapping) { + let updateItems: { cell: CodeCellViewModel, output: IOutput, cellTop: number }[] = []; + let removedItems: IOutput[] = []; + this.webview?.insetMapping.forEach((value, key) => { + const cell = value.cell; + const viewIndex = this.list?.getViewIndex(cell); + + if (viewIndex === undefined) { + return; + } + + if (cell.outputs.indexOf(key) < 0) { + // output is already gone + removedItems.push(key); + } + + const cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; + if (this.webview!.shouldUpdateInset(cell, key, cellTop)) { + updateItems.push({ + cell: cell, + output: key, + cellTop: cellTop + }); + } + }); + + removedItems.forEach(output => this.webview?.removeInset(output)); + + if (updateItems.length) { + this.webview?.updateViewScrollTop(-scrollTop, updateItems); + } + } + }); + })); + + this.list!.attachViewModel(this.viewModel); + this.localStore.add(this.list!.onDidRemoveOutput(output => { + this.removeInset(output); + })); + this.localStore.add(this.list!.onDidHideOutput(output => { + this.hideInset(output); + })); + + this.list!.layout(); + + // restore list state at last, it must be after list layout + this.restoreListViewState(viewState); + } + + private restoreListViewState(viewState: INotebookEditorViewState | undefined): void { + if (viewState?.scrollPosition !== undefined) { + this.list!.scrollTop = viewState!.scrollPosition.top; + this.list!.scrollLeft = viewState!.scrollPosition.left; + } else { + this.list!.scrollTop = 0; + this.list!.scrollLeft = 0; + } + + const focusIdx = typeof viewState?.focus === 'number' ? viewState.focus : 0; + if (focusIdx < this.list!.length) { + this.list!.setFocus([focusIdx]); + this.list!.setSelection([focusIdx]); + } else if (this.list!.length > 0) { + this.list!.setFocus([0]); + } + + if (viewState?.editorFocused) { + this.list?.focusView(); + const cell = this.notebookViewModel?.viewCells[focusIdx]; + if (cell) { + cell.focusMode = CellFocusMode.Editor; + } + } + } + + getEditorViewState() { + const state = this.notebookViewModel!.geteEditorViewState(); + if (this.list) { + state.scrollPosition = { left: this.list.scrollLeft, top: this.list.scrollTop }; + let cellHeights: { [key: number]: number } = {}; + for (let i = 0; i < this.viewModel!.length; i++) { + const elm = this.viewModel!.viewCells[i] as CellViewModel; + if (elm.cellKind === CellKind.Code) { + cellHeights[i] = elm.layoutInfo.totalHeight; + } else { + cellHeights[i] = 0; + } + } + + state.cellTotalHeights = cellHeights; + + const focus = this.list.getFocus()[0]; + if (typeof focus === 'number') { + const element = this.notebookViewModel!.viewCells[focus]; + const itemDOM = this.list?.domElementOfElement(element!); + let editorFocused = false; + if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { + editorFocused = true; + } + + state.editorFocused = editorFocused; + state.focus = focus; + } + } + + // Save contribution view states + const contributionsState: { [key: string]: any } = {}; + + const keys = Object.keys(this._contributions); + for (const id of keys) { + const contribution = this._contributions[id]; + if (typeof contribution.saveViewState === 'function') { + contributionsState[id] = contribution.saveViewState(); + } + } + + state.contributionsState = contributionsState; + return state; + } + + // private saveEditorViewState(input: NotebookEditorInput): void { + // if (this.group && this.notebookViewModel) { + // } + // } + + // private loadTextEditorViewState(): INotebookEditorViewState | undefined { + // return this.editorMemento.loadEditorState(this.group, input.resource); + // } + + layout(dimension: DOM.Dimension): void { + this.dimension = new DOM.Dimension(dimension.width, dimension.height); + DOM.toggleClass(this._rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600); + DOM.toggleClass(this._rootElement, 'narrow-width', dimension.width < 600); + DOM.size(this.body, dimension.width, dimension.height); + this.list?.updateOptions({ additionalScrollHeight: this.scrollBeyondLastLine ? dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP : 0 }); + this.list?.layout(dimension.height - SCROLLABLE_ELEMENT_PADDING_TOP, dimension.width); + + this.overlayContainer.style.visibility = 'visible'; + this.overlayContainer.style.display = 'block'; + const containerRect = this._rootElement.getBoundingClientRect(); + this.overlayContainer.style.position = 'absolute'; + this.overlayContainer.style.top = `${containerRect.top}px`; + this.overlayContainer.style.left = `${containerRect.left}px`; + this.overlayContainer.style.width = `${dimension ? dimension.width : containerRect.width}px`; + this.overlayContainer.style.height = `${dimension ? dimension.height : containerRect.height}px`; + + if (this.webviewTransparentCover) { + this.webviewTransparentCover.style.height = `${dimension.height}px`; + this.webviewTransparentCover.style.width = `${dimension.width}px`; + } + + this.eventDispatcher?.emit([new NotebookLayoutChangedEvent({ width: true, fontInfo: true }, this.getLayoutInfo())]); + } + + // protected saveState(): void { + // if (this.input instanceof NotebookEditorInput) { + // this.saveEditorViewState(this.input); + // } + + // super.saveState(); + // } + + //#endregion + + //#region Editor Features + + selectElement(cell: ICellViewModel) { + this.list?.selectElement(cell); + // this.viewModel!.selectionHandles = [cell.handle]; + } + + revealInView(cell: ICellViewModel) { + this.list?.revealElementInView(cell); + } + + revealInCenterIfOutsideViewport(cell: ICellViewModel) { + this.list?.revealElementInCenterIfOutsideViewport(cell); + } + + revealInCenter(cell: ICellViewModel) { + this.list?.revealElementInCenter(cell); + } + + revealLineInView(cell: ICellViewModel, line: number): void { + this.list?.revealElementLineInView(cell, line); + } + + revealLineInCenter(cell: ICellViewModel, line: number) { + this.list?.revealElementLineInCenter(cell, line); + } + + revealLineInCenterIfOutsideViewport(cell: ICellViewModel, line: number) { + this.list?.revealElementLineInCenterIfOutsideViewport(cell, line); + } + + revealRangeInView(cell: ICellViewModel, range: Range): void { + this.list?.revealElementRangeInView(cell, range); + } + + revealRangeInCenter(cell: ICellViewModel, range: Range): void { + this.list?.revealElementRangeInCenter(cell, range); + } + + revealRangeInCenterIfOutsideViewport(cell: ICellViewModel, range: Range): void { + this.list?.revealElementRangeInCenterIfOutsideViewport(cell, range); + } + + setCellSelection(cell: ICellViewModel, range: Range): void { + this.list?.setCellSelection(cell, range); + } + + changeDecorations(callback: (changeAccessor: IModelDecorationsChangeAccessor) => any): any { + return this.notebookViewModel?.changeDecorations(callback); + } + + setHiddenAreas(_ranges: ICellRange[]): boolean { + return this.list!.setHiddenAreas(_ranges, true); + } + + //#endregion + + //#region Mouse Events + private readonly _onMouseUp: Emitter = this._register(new Emitter()); + public readonly onMouseUp: Event = this._onMouseUp.event; + + private readonly _onMouseDown: Emitter = this._register(new Emitter()); + public readonly onMouseDown: Event = this._onMouseDown.event; + + //#endregion + + //#region Cell operations + async layoutNotebookCell(cell: ICellViewModel, height: number): Promise { + const viewIndex = this.list!.getViewIndex(cell); + if (viewIndex === undefined) { + // the cell is hidden + return; + } + + let relayout = (cell: ICellViewModel, height: number) => { + this.list?.updateElementHeight2(cell, height); + }; + + let r: () => void; + DOM.scheduleAtNextAnimationFrame(() => { + relayout(cell, height); + r(); + }); + + return new Promise(resolve => { r = resolve; }); + } + + insertNotebookCell(cell: ICellViewModel | undefined, type: CellKind, direction: 'above' | 'below' = 'above', initialText: string = '', ui: boolean = false): CellViewModel | null { + if (!this.notebookViewModel!.metadata.editable) { + return null; + } + + const newLanguages = this.notebookViewModel!.languages; + const language = (type === CellKind.Code && newLanguages && newLanguages.length) ? newLanguages[0] : 'markdown'; + const index = cell ? this.notebookViewModel!.getCellIndex(cell) : 0; + const nextIndex = ui ? this.notebookViewModel!.getNextVisibleCellIndex(index) : index + 1; + const insertIndex = cell ? + (direction === 'above' ? index : nextIndex) : + index; + const newCell = this.notebookViewModel!.createCell(insertIndex, initialText.split(/\r?\n/g), language, type, true); + return newCell; + } + + private pushIfAbsent(positions: IPosition[], p: IPosition) { + const last = positions.length > 0 ? positions[positions.length - 1] : undefined; + if (!last || last.lineNumber !== p.lineNumber || last.column !== p.column) { + positions.push(p); + } + } + + /** + * Add split point at the beginning and the end; + * Move end of line split points to the beginning of the next line; + * Avoid duplicate split points + */ + private splitPointsToBoundaries(splitPoints: IPosition[], textBuffer: IReadonlyTextBuffer): IPosition[] | null { + const boundaries: IPosition[] = []; + const lineCnt = textBuffer.getLineCount(); + const getLineLen = (lineNumber: number) => { + return textBuffer.getLineLength(lineNumber); + }; + + // split points need to be sorted + splitPoints = splitPoints.sort((l, r) => { + const lineDiff = l.lineNumber - r.lineNumber; + const columnDiff = l.column - r.column; + return lineDiff !== 0 ? lineDiff : columnDiff; + }); + + // eat-up any split point at the beginning, i.e. we ignore the split point at the very beginning + this.pushIfAbsent(boundaries, new Position(1, 1)); + + for (let sp of splitPoints) { + if (getLineLen(sp.lineNumber) + 1 === sp.column && sp.lineNumber < lineCnt) { + sp = new Position(sp.lineNumber + 1, 1); + } + this.pushIfAbsent(boundaries, sp); + } + + // eat-up any split point at the beginning, i.e. we ignore the split point at the very end + this.pushIfAbsent(boundaries, new Position(lineCnt, getLineLen(lineCnt) + 1)); + + // if we only have two then they describe the whole range and nothing needs to be split + return boundaries.length > 2 ? boundaries : null; + } + + private computeCellLinesContents(cell: IEditableCellViewModel, splitPoints: IPosition[]): string[] | null { + const rangeBoundaries = this.splitPointsToBoundaries(splitPoints, cell.textBuffer); + if (!rangeBoundaries) { + return null; + } + const newLineModels: string[] = []; + for (let i = 1; i < rangeBoundaries.length; i++) { + const start = rangeBoundaries[i - 1]; + const end = rangeBoundaries[i]; + + newLineModels.push(cell.textModel.getValueInRange(new Range(start.lineNumber, start.column, end.lineNumber, end.column))); + } + + return newLineModels; + } + + async splitNotebookCell(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return null; + } + + let splitPoints = cell.getSelectionsStartPosition(); + if (splitPoints && splitPoints.length > 0) { + await cell.resolveTextModel(); + + if (!cell.hasModel()) { + return null; + } + + let newLinesContents = this.computeCellLinesContents(cell, splitPoints); + if (newLinesContents) { + + // update the contents of the first cell + cell.textModel.applyEdits([ + { range: cell.textModel.getFullModelRange(), text: newLinesContents[0] } + ], true); + + // create new cells based on the new text models + const language = cell.model.language; + const kind = cell.cellKind; + let insertIndex = this.notebookViewModel!.getCellIndex(cell) + 1; + const newCells = []; + for (let j = 1; j < newLinesContents.length; j++, insertIndex++) { + newCells.push(this.notebookViewModel!.createCell(insertIndex, newLinesContents[j], language, kind, true)); + } + return newCells; + } + } + + return null; + } + + async joinNotebookCells(cell: ICellViewModel, direction: 'above' | 'below', constraint?: CellKind): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return null; + } + + if (constraint && cell.cellKind !== constraint) { + return null; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + if (index === 0 && direction === 'above') { + return null; + } + + if (index === this.notebookViewModel!.length - 1 && direction === 'below') { + return null; + } + + if (direction === 'above') { + const above = this.notebookViewModel!.viewCells[index - 1]; + if (constraint && above.cellKind !== constraint) { + return null; + } + + await above.resolveTextModel(); + if (!above.hasModel()) { + return null; + } + + const insertContent = cell.getText(); + const aboveCellLineCount = above.textModel.getLineCount(); + const aboveCellLastLineEndColumn = above.textModel.getLineLength(aboveCellLineCount); + above.textModel.applyEdits([ + { range: new Range(aboveCellLineCount, aboveCellLastLineEndColumn + 1, aboveCellLineCount, aboveCellLastLineEndColumn + 1), text: insertContent } + ]); + + await this.deleteNotebookCell(cell); + return above; + } else { + const below = this.notebookViewModel!.viewCells[index + 1]; + if (constraint && below.cellKind !== constraint) { + return null; + } + + await cell.resolveTextModel(); + if (!cell.hasModel()) { + return null; + } + + const insertContent = below.getText(); + + const cellLineCount = cell.textModel.getLineCount(); + const cellLastLineEndColumn = cell.textModel.getLineLength(cellLineCount); + cell.textModel.applyEdits([ + { range: new Range(cellLineCount, cellLastLineEndColumn + 1, cellLineCount, cellLastLineEndColumn + 1), text: insertContent } + ]); + + await this.deleteNotebookCell(below); + return cell; + } + } + + async deleteNotebookCell(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + this.notebookViewModel!.deleteCell(index, true); + return true; + } + + async moveCellDown(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + if (index === this.notebookViewModel!.length - 1) { + return false; + } + + const newIdx = index + 1; + return this.moveCellToIndex(index, newIdx); + } + + async moveCellUp(cell: ICellViewModel): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + const index = this.notebookViewModel!.getCellIndex(cell); + if (index === 0) { + return false; + } + + const newIdx = index - 1; + return this.moveCellToIndex(index, newIdx); + } + + async moveCell(cell: ICellViewModel, relativeToCell: ICellViewModel, direction: 'above' | 'below'): Promise { + if (!this.notebookViewModel!.metadata.editable) { + return false; + } + + if (cell === relativeToCell) { + return false; + } + + const originalIdx = this.notebookViewModel!.getCellIndex(cell); + const relativeToIndex = this.notebookViewModel!.getCellIndex(relativeToCell); + + let newIdx = direction === 'above' ? relativeToIndex : relativeToIndex + 1; + if (originalIdx < newIdx) { + newIdx--; + } + + return this.moveCellToIndex(originalIdx, newIdx); + } + + private async moveCellToIndex(index: number, newIdx: number): Promise { + if (index === newIdx) { + return false; + } + + if (!this.notebookViewModel!.moveCellToIdx(index, newIdx, true)) { + throw new Error('Notebook Editor move cell, index out of range'); + } + + let r: (val: boolean) => void; + DOM.scheduleAtNextAnimationFrame(() => { + this.list?.revealElementInView(this.notebookViewModel!.viewCells[newIdx]); + r(true); + }); + + return new Promise(resolve => { r = resolve; }); + } + + editNotebookCell(cell: CellViewModel): void { + if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).editable) { + return; + } + + cell.editState = CellEditState.Editing; + + this.renderedEditors.get(cell)?.focus(); + } + + saveNotebookCell(cell: ICellViewModel): void { + cell.editState = CellEditState.Preview; + } + + getActiveCell() { + let elements = this.list?.getFocusedElements(); + + if (elements && elements.length) { + return elements[0]; + } + + return undefined; + } + + cancelNotebookExecution(): void { + if (!this.notebookViewModel!.currentTokenSource) { + throw new Error('Notebook is not executing'); + } + + + this.notebookViewModel!.currentTokenSource.cancel(); + this.notebookViewModel!.currentTokenSource = undefined; + } + + async executeNotebook(): Promise { + if (!this.notebookViewModel!.metadata.runnable) { + return; + } + + // return this.progressService.showWhile(this._executeNotebook()); + return this._executeNotebook(); + } + + async _executeNotebook(): Promise { + if (this.notebookViewModel!.currentTokenSource) { + return; + } + + const tokenSource = new CancellationTokenSource(); + try { + this.editorExecutingNotebook!.set(true); + this.notebookViewModel!.currentTokenSource = tokenSource; + + for (let cell of this.notebookViewModel!.viewCells) { + if (cell.cellKind === CellKind.Code) { + await this._executeNotebookCell(cell, tokenSource); + } + } + } finally { + this.editorExecutingNotebook!.set(false); + this.notebookViewModel!.currentTokenSource = undefined; + tokenSource.dispose(); + } + } + + cancelNotebookCellExecution(cell: ICellViewModel): void { + if (!cell.currentTokenSource) { + throw new Error('Cell is not executing'); + } + + cell.currentTokenSource.cancel(); + cell.currentTokenSource = undefined; + } + + async executeNotebookCell(cell: ICellViewModel): Promise { + if (!cell.getEvaluatedMetadata(this.notebookViewModel!.metadata).runnable) { + return; + } + + const tokenSource = new CancellationTokenSource(); + try { + this._executeNotebookCell(cell, tokenSource); + } finally { + tokenSource.dispose(); + } + } + + private async _executeNotebookCell(cell: ICellViewModel, tokenSource: CancellationTokenSource): Promise { + try { + cell.currentTokenSource = tokenSource; + const provider = this.notebookService.getContributedNotebookProviders(this.viewModel!.uri)[0]; + if (provider) { + const viewType = provider.id; + const notebookUri = CellUri.parse(cell.uri)?.notebook; + if (notebookUri) { + return await this.notebookService.executeNotebookCell(viewType, notebookUri, cell.handle, tokenSource.token); + } + } + } finally { + cell.currentTokenSource = undefined; + } + } + + focusNotebookCell(cell: ICellViewModel, focusItem: 'editor' | 'container' | 'output') { + if (focusItem === 'editor') { + this.selectElement(cell); + this.list?.focusView(); + + cell.editState = CellEditState.Editing; + cell.focusMode = CellFocusMode.Editor; + this.revealInCenterIfOutsideViewport(cell); + } else if (focusItem === 'output') { + this.selectElement(cell); + this.list?.focusView(); + + if (!this.webview) { + return; + } + this.webview.focusOutput(cell.id); + + cell.editState = CellEditState.Preview; + cell.focusMode = CellFocusMode.Container; + this.revealInCenterIfOutsideViewport(cell); + } else { + let itemDOM = this.list?.domElementOfElement(cell); + if (document.activeElement && itemDOM && itemDOM.contains(document.activeElement)) { + (document.activeElement as HTMLElement).blur(); + } + + cell.editState = CellEditState.Preview; + cell.focusMode = CellFocusMode.Container; + + this.selectElement(cell); + this.revealInCenterIfOutsideViewport(cell); + this.list?.focusView(); + } + } + + //#endregion + + //#region MISC + + getLayoutInfo(): NotebookLayoutInfo { + if (!this.list) { + throw new Error('Editor is not initalized successfully'); + } + + return { + width: this.dimension!.width, + height: this.dimension!.height, + fontInfo: this.fontInfo! + }; + } + + triggerScroll(event: IMouseWheelEvent) { + this.list?.triggerScrollFromMouseWheelEvent(event); + } + + createInset(cell: CodeCellViewModel, output: IOutput, shadowContent: string, offset: number) { + if (!this.webview) { + return; + } + + let preloads = this.notebookViewModel!.renderers; + + if (!this.webview!.insetMapping.has(output)) { + let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; + this.webview!.createInset(cell, output, cellTop, offset, shadowContent, preloads); + } else { + let cellTop = this.list?.getAbsoluteTopOfElement(cell) || 0; + let scrollTop = this.list?.scrollTop || 0; + + this.webview!.updateViewScrollTop(-scrollTop, [{ cell: cell, output: output, cellTop: cellTop }]); + } + } + + removeInset(output: IOutput) { + if (!this.webview) { + return; + } + + this.webview!.removeInset(output); + } + + hideInset(output: IOutput) { + if (!this.webview) { + return; + } + + this.webview!.hideInset(output); + } + + getOutputRenderer(): OutputRenderer { + return this.outputRenderer; + } + + postMessage(message: any) { + this.webview?.webview.sendMessage(message); + } + + //#endregion + + //#region Editor Contributions + public getContribution(id: string): T { + return (this._contributions[id] || null); + } + + //#endregion + + dispose() { + const keys = Object.keys(this._contributions); + for (let i = 0, len = keys.length; i < len; i++) { + const contributionId = keys[i]; + this._contributions[contributionId].dispose(); + } + + this._layoutService.container.removeChild(this.overlayContainer); + + super.dispose(); + } + + toJSON(): any { + return { + notebookHandle: this.viewModel?.handle + }; + } +} + +const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground'; + +export const focusedCellIndicator = registerColor('notebook.focusedCellIndicator', { + light: new Color(new RGBA(102, 175, 224)), + dark: new Color(new RGBA(12, 125, 157)), + hc: new Color(new RGBA(0, 73, 122)) +}, nls.localize('notebook.focusedCellIndicator', "The color of the focused notebook cell indicator.")); + +export const notebookOutputContainerColor = registerColor('notebook.outputContainerBackgroundColor', { + dark: new Color(new RGBA(255, 255, 255, 0.06)), + light: new Color(new RGBA(237, 239, 249)), + hc: null +} + , nls.localize('notebook.outputContainerBackgroundColor', "The Color of the notebook output container background.")); + +// TODO currently also used for toolbar border, if we keep all of this, pick a generic name +export const CELL_TOOLBAR_SEPERATOR = registerColor('notebook.cellToolbarSeperator', { + dark: Color.fromHex('#808080').transparent(0.35), + light: Color.fromHex('#808080').transparent(0.35), + hc: contrastBorder +}, nls.localize('cellToolbarSeperator', "The color of seperator in Cell bottom toolbar")); + + +registerThemingParticipant((theme, collector) => { + collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element { + padding-top: ${SCROLLABLE_ELEMENT_PADDING_TOP}px; + box-sizing: border-box; + }`); + + const color = getExtraColor(theme, embeddedEditorBackground, { dark: 'rgba(0, 0, 0, .4)', extra_dark: 'rgba(200, 235, 255, .064)', light: '#f4f4f4', hc: null }); + if (color) { + collector.addRule(`.notebookOverlay .cell .monaco-editor-background, + .notebookOverlay .cell .margin-view-overlays, + .notebookOverlay .cell .cell-statusbar-container { background: ${color}; }`); + collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { background: ${color} !important; }`); + } + const link = theme.getColor(textLinkForeground); + if (link) { + collector.addRule(`.notebookOverlay .output a, + .notebookOverlay .cell.markdown a { color: ${link};} `); + } + const activeLink = theme.getColor(textLinkActiveForeground); + if (activeLink) { + collector.addRule(`.notebookOverlay .output a:hover, + .notebookOverlay .cell .output a:active { color: ${activeLink}; }`); + } + const shortcut = theme.getColor(textPreformatForeground); + if (shortcut) { + collector.addRule(`.notebookOverlay code, + .notebookOverlay .shortcut { color: ${shortcut}; }`); + } + const border = theme.getColor(contrastBorder); + if (border) { + collector.addRule(`.notebookOverlay .monaco-editor { border-color: ${border}; }`); + } + const quoteBackground = theme.getColor(textBlockQuoteBackground); + if (quoteBackground) { + collector.addRule(`.notebookOverlay blockquote { background: ${quoteBackground}; }`); + } + const quoteBorder = theme.getColor(textBlockQuoteBorder); + if (quoteBorder) { + collector.addRule(`.notebookOverlay blockquote { border-color: ${quoteBorder}; }`); + } + + const containerBackground = theme.getColor(notebookOutputContainerColor); + if (containerBackground) { + collector.addRule(`.notebookOverlay .output { background-color: ${containerBackground}; }`); + } + + const editorBackgroundColor = theme.getColor(editorBackground); + if (editorBackgroundColor) { + collector.addRule(`.notebookOverlay .cell-statusbar-container { border-top: solid 1px ${editorBackgroundColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row > .monaco-toolbar { background-color: ${editorBackgroundColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row.cell-drag-image { background-color: ${editorBackgroundColor}; }`); + } + + const cellToolbarSeperator = theme.getColor(CELL_TOOLBAR_SEPERATOR); + if (cellToolbarSeperator) { + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .seperator { background-color: ${cellToolbarSeperator} }`); + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container .seperator-short { background-color: ${cellToolbarSeperator} }`); + collector.addRule(`.notebookOverlay .monaco-list-row > .monaco-toolbar { border: solid 1px ${cellToolbarSeperator}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row:hover .notebook-cell-focus-indicator, + .notebookOverlay .monaco-list-row.cell-output-hover .notebook-cell-focus-indicator { border-color: ${cellToolbarSeperator}; }`); + } + + const focusedCellIndicatorColor = theme.getColor(focusedCellIndicator); + if (focusedCellIndicatorColor) { + collector.addRule(`.notebookOverlay .monaco-list-row.focused .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row .notebook-cell-focus-indicator { border-color: ${focusedCellIndicatorColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row .cell-insertion-indicator { background-color: ${focusedCellIndicatorColor}; }`); + collector.addRule(`.notebookOverlay .monaco-list-row.cell-editor-focus .cell-editor-part:before { outline: solid 1px ${focusedCellIndicatorColor}; }`); + } + + // const widgetShadowColor = theme.getColor(widgetShadow); + // if (widgetShadowColor) { + // collector.addRule(`.notebookOverlay > .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-toolbar { + // box-shadow: 0 0 8px 4px ${widgetShadowColor} + // }`) + // } + + // Cell Margin + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row { padding-top: ${EDITOR_TOP_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .output { margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); + collector.addRule(`.notebookOverlay .cell-bottom-toolbar-container { width: calc(100% - ${CELL_MARGIN * 2 + CELL_RUN_GUTTER}px); margin: 0px ${CELL_MARGIN}px 0px ${CELL_MARGIN + CELL_RUN_GUTTER}px }`); + + collector.addRule(`.notebookOverlay .markdown-cell-row .cell .cell-editor-part { margin-left: ${CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay .cell-list-container > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > div.cell.markdown { padding-left: ${CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay .cell .run-button-container { width: ${CELL_RUN_GUTTER}px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .cell-insertion-indicator { left: ${CELL_MARGIN + CELL_RUN_GUTTER}px; right: ${CELL_MARGIN}px; }`); + collector.addRule(`.notebookOverlay .cell-drag-image .cell-editor-container > div { padding: ${EDITOR_TOP_PADDING}px 16px ${EDITOR_BOTTOM_PADDING}px 16px; }`); + collector.addRule(`.notebookOverlay .monaco-list .monaco-list-row .notebook-cell-focus-indicator { left: ${CELL_MARGIN}px; }`); +});