notebookEditor.ts 14.8 KB
Newer Older
P
Peng Lyu 已提交
1 2 3 4 5 6 7 8 9 10 11
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

import 'vs/css!./notebook';
import * as DOM from 'vs/base/browser/dom';
import { BaseEditor } from 'vs/workbench/browser/parts/editor/baseEditor';
import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { IThemeService, registerThemingParticipant } from 'vs/platform/theme/common/themeService';
import { IStorageService } from 'vs/platform/storage/common/storage';
P
Peng Lyu 已提交
12
import { NotebookEditorInput, NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
R
rebornix 已提交
13
import { EditorOptions, IEditorMemento } from 'vs/workbench/common/editor';
P
Peng Lyu 已提交
14 15 16 17
import { CancellationToken } from 'vs/base/common/cancellation';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { IModelService } from 'vs/editor/common/services/modelService';
import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils';
P
Peng Lyu 已提交
18 19
import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground, contrastBorder, textBlockQuoteBackground, textBlockQuoteBorder, editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
P
Peng Lyu 已提交
20
import { IModeService } from 'vs/editor/common/services/modeService';
P
Peng Lyu 已提交
21
import { NotebookHandler, ViewCell, MarkdownCellRenderer, CodeCellRenderer, NotebookCellListDelegate } from 'vs/workbench/contrib/notebook/browser/cellRenderer';
R
rebornix 已提交
22
import { IEditorGroup, IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService';
P
Peng Lyu 已提交
23
import { IWebviewService } from 'vs/workbench/contrib/webview/browser/webview';
P
Peng Lyu 已提交
24
import { BackLayerWebView } from 'vs/workbench/contrib/notebook/browser/contentWidget';
P
Peng Lyu 已提交
25
import { IMouseWheelEvent } from 'vs/base/browser/mouseEvent';
26
import { DisposableStore } from 'vs/base/common/lifecycle';
R
rebornix 已提交
27
import { INotebookService } from 'vs/workbench/contrib/notebook/browser/notebookService';
P
Peng Lyu 已提交
28 29

const $ = DOM.$;
R
rebornix 已提交
30 31 32 33 34 35
const NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY = 'NotebookEditorViewState';


interface INotebookEditorViewState {
	editingCells: { [key: number]: boolean };
}
P
Peng Lyu 已提交
36

P
Peng Lyu 已提交
37
export class NotebookEditor extends BaseEditor implements NotebookHandler {
P
Peng Lyu 已提交
38 39 40
	static readonly ID: string = 'workbench.editor.notebook';
	private rootElement!: HTMLElement;
	private body!: HTMLElement;
P
Peng Lyu 已提交
41
	private contentWidgets!: HTMLElement;
P
Peng Lyu 已提交
42
	private webview: BackLayerWebView | null = null;
P
Peng Lyu 已提交
43

P
Peng Lyu 已提交
44
	private list: WorkbenchList<ViewCell> | undefined;
P
Peng Lyu 已提交
45
	private model: NotebookEditorModel | undefined;
P
Peng Lyu 已提交
46
	private viewCells: ViewCell[] = [];
47
	private localStore: DisposableStore = new DisposableStore();
R
rebornix 已提交
48
	private editorMemento: IEditorMemento<INotebookEditorViewState>;
P
Peng Lyu 已提交
49 50 51 52 53

	constructor(
		@ITelemetryService telemetryService: ITelemetryService,
		@IThemeService themeService: IThemeService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
P
Peng Lyu 已提交
54 55
		@IModelService private readonly modelService: IModelService,
		@IModeService private readonly modeService: IModeService,
P
Peng Lyu 已提交
56
		@IStorageService storageService: IStorageService,
R
rebornix 已提交
57
		@IWebviewService private webviewService: IWebviewService,
R
rebornix 已提交
58 59
		@INotebookService private notebookService: INotebookService,
		@IEditorGroupsService editorGroupService: IEditorGroupsService
P
Peng Lyu 已提交
60 61
	) {
		super(NotebookEditor.ID, telemetryService, themeService, storageService);
R
rebornix 已提交
62 63

		this.editorMemento = this.getEditorMemento<INotebookEditorViewState>(editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY);
P
Peng Lyu 已提交
64
	}
P
Peng Lyu 已提交
65

P
Peng Lyu 已提交
66 67 68 69 70 71 72 73 74 75 76 77 78 79
	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*/ }


	protected createEditor(parent: HTMLElement): void {
		this.rootElement = DOM.append(parent, $('.notebook-editor'));
		this.createBody(this.rootElement);
	}

	private createBody(parent: HTMLElement): void {
P
Peng Lyu 已提交
80
		this.body = document.createElement('div');
P
Peng Lyu 已提交
81 82 83
		DOM.addClass(this.body, 'cell-list-container');
		this.createCellList();
		DOM.append(parent, this.body);
P
Peng Lyu 已提交
84 85 86 87

		this.contentWidgets = document.createElement('div');
		DOM.addClass(this.contentWidgets, 'notebook-content-widgets');
		DOM.append(this.body, this.contentWidgets);
P
Peng Lyu 已提交
88 89
	}

P
Peng Lyu 已提交
90 91
	private createCellList(): void {
		DOM.addClass(this.body, 'cell-list-container');
P
Peng Lyu 已提交
92

P
Peng Lyu 已提交
93
		const renders = [
P
Peng Lyu 已提交
94 95
			this.instantiationService.createInstance(MarkdownCellRenderer, this),
			this.instantiationService.createInstance(CodeCellRenderer, this)
P
Peng Lyu 已提交
96 97
		];

P
Peng Lyu 已提交
98
		this.list = this.instantiationService.createInstance<typeof WorkbenchList, WorkbenchList<ViewCell>>(
P
Peng Lyu 已提交
99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120
			WorkbenchList,
			'NotebookCellList',
			this.body,
			this.instantiationService.createInstance(NotebookCellListDelegate),
			renders,
			{
				setRowLineHeight: false,
				supportDynamicHeights: true,
				horizontalScrolling: false,
				keyboardSupport: false,
				mouseSupport: false,
				multipleSelectionSupport: false,
				overrideStyles: {
					listBackground: editorBackground,
					listActiveSelectionBackground: editorBackground,
					listActiveSelectionForeground: foreground,
					listFocusAndSelectionBackground: editorBackground,
					listFocusAndSelectionForeground: foreground,
					listFocusBackground: editorBackground,
					listFocusForeground: foreground,
					listHoverForeground: foreground,
					listHoverBackground: editorBackground,
121 122
					listHoverOutline: focusBorder,
					listFocusOutline: focusBorder,
123 124 125 126
					listInactiveSelectionBackground: editorBackground,
					listInactiveSelectionForeground: foreground,
					listInactiveFocusBackground: editorBackground,
					listInactiveFocusOutline: editorBackground,
P
Peng Lyu 已提交
127 128 129
				}
			}
		);
P
Peng Lyu 已提交
130

R
rebornix 已提交
131
		this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this);
P
Peng Lyu 已提交
132
		this.list.view.rowsContainer.appendChild(this.webview.element);
P
Peng Lyu 已提交
133 134 135 136 137
		this._register(this.list);
	}

	triggerWheel(event: IMouseWheelEvent) {
		this.list?.triggerScrollFromMouseWheelEvent(event);
P
Peng Lyu 已提交
138 139
	}

R
rebornix 已提交
140
	createContentWidget(cell: ViewCell, outputIndex: number, shadowContent: string, offset: number) {
141 142 143 144
		if (!this.webview) {
			return;
		}

P
Peng Lyu 已提交
145 146 147
		if (!this.webview!.mapping.has(cell.id)) {
			let index = this.model!.getNotebook().cells.indexOf(cell.cell);
			let top = this.list?.getElementTop(index) || 0;
148
			this.webview!.createContentWidget(cell, offset, shadowContent, top + offset);
R
rebornix 已提交
149 150 151 152 153 154
			this.webview!.outputMapping.set(cell.id + `-${outputIndex}`, true);
		} else if (!this.webview!.outputMapping.has(cell.id + `-${outputIndex}`)) {
			let index = this.model!.getNotebook().cells.indexOf(cell.cell);
			let top = this.list?.getElementTop(index) || 0;
			this.webview!.outputMapping.set(cell.id + `-${outputIndex}`, true);
			this.webview!.createContentWidget(cell, offset, shadowContent, top + offset);
P
Peng Lyu 已提交
155
		} else {
P
Peng Lyu 已提交
156 157 158 159
			let index = this.model!.getNotebook().cells.indexOf(cell.cell);
			let top = this.list?.getElementTop(index) || 0;
			let scrollTop = this.list?.scrollTop || 0;

160
			this.webview!.updateViewScrollTop(-scrollTop, [{ id: cell.id, top: top + offset }]);
P
Peng Lyu 已提交
161
		}
P
Peng Lyu 已提交
162
	}
P
Peng Lyu 已提交
163

P
Peng Lyu 已提交
164 165 166
	disposeViewCell(cell: ViewCell) {
	}

P
Peng Lyu 已提交
167
	onHide() {
R
rebornix 已提交
168 169 170 171 172
		this.viewCells.forEach(cell => {
			if (cell.getText() !== '') {
				cell.isEditing = false;
			}
		});
173 174 175 176 177 178 179 180 181 182

		if (this.webview) {
			this.localStore.clear();
			this.list?.view.rowsContainer.removeChild(this.webview?.element);
			this.webview?.dispose();
			this.webview = null;
		}

		this.list?.splice(0, this.list?.length);
		super.onHide();
P
Peng Lyu 已提交
183 184
	}

185
	setVisible(visible: boolean, group?: IEditorGroup): void {
R
rebornix 已提交
186
		super.setVisible(visible, group);
187
		if (!visible) {
R
rebornix 已提交
188 189 190 191 192
			this.viewCells.forEach(cell => {
				if (cell.getText() !== '') {
					cell.isEditing = false;
				}
			});
193 194 195
		}
	}

P
Peng Lyu 已提交
196
	setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
R
rebornix 已提交
197 198 199 200
		if (this.input instanceof NotebookEditorInput) {
			this.saveTextEditorViewState(this.input);
		}

201 202 203 204 205
		return super.setInput(input, options, token)
			.then(() => {
				return input.resolve();
			})
			.then(model => {
206
				if (this.model !== undefined && this.model.textModel === model.textModel && this.webview !== null) {
P
Peng Lyu 已提交
207 208 209
					return;
				}

210
				this.localStore.clear();
P
Peng Lyu 已提交
211 212 213 214
				this.viewCells.forEach(cell => {
					cell.save();
				});

215 216 217
				if (this.webview) {
					this.webview?.clearContentWidgets();
				} else {
R
rebornix 已提交
218
					this.webview = new BackLayerWebView(this.webviewService, this.notebookService, this);
219 220 221
					this.list?.view.rowsContainer.insertAdjacentElement('afterbegin', this.webview!.element);
				}

P
Peng Lyu 已提交
222
				this.model = model;
R
rebornix 已提交
223 224 225 226
				this.localStore.add(this.model.onDidChangeCells(() => {
					this.updateViewCells();
				}));

R
rebornix 已提交
227
				let viewState = this.loadTextEditorViewState(input);
P
Peng Lyu 已提交
228
				this.viewCells = model.getNotebook().cells.map(cell => {
R
rebornix 已提交
229 230
					const isEditing = viewState && viewState.editingCells[cell.handle];
					return new ViewCell(cell, !!isEditing, this.modelService, this.modeService);
P
Peng Lyu 已提交
231
				});
232 233 234 235 236 237

				const updateScrollPosition = () => {
					let scrollTop = this.list?.scrollTop || 0;
					this.webview!.element.style.top = `${scrollTop}px`;
					let updateItems: { top: number, id: string }[] = [];

R
rebornix 已提交
238
					// const date = new Date();
239 240 241 242 243 244 245 246 247 248 249 250 251 252
					this.webview?.mapping.forEach((item) => {
						let index = this.model!.getNotebook().cells.indexOf(item.cell.cell);
						let top = this.list?.getElementTop(index) || 0;
						let newTop = this.webview!.shouldRenderContentWidget(item.cell.id, top);

						if (newTop !== undefined) {
							updateItems.push({
								top: newTop,
								id: item.cell.id
							});
						}
					});

					if (updateItems.length > 0) {
R
rebornix 已提交
253
						// console.log('----- did scroll ----  ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
254 255 256 257
						this.webview?.updateViewScrollTop(-scrollTop, updateItems);
					}
				};
				this.localStore.add(this.list!.onWillScroll(e => {
R
rebornix 已提交
258 259
					// const date = new Date();
					// console.log('----- will scroll ----  ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
260 261 262 263 264
					this.webview?.updateViewScrollTop(-e.scrollTop, []);
				}));
				this.localStore.add(this.list!.onDidScroll(() => updateScrollPosition()));
				this.localStore.add(this.list!.onDidChangeContentHeight(() => updateScrollPosition()));

P
Peng Lyu 已提交
265
				this.list?.splice(0, this.list?.length, this.viewCells);
266 267
				this.list?.layout();
			});
P
Peng Lyu 已提交
268 269
	}

P
Peng Lyu 已提交
270
	layoutElement(cell: ViewCell, height: number) {
R
rebornix 已提交
271
		let relayout = (cell: ViewCell, height: number) => {
P
Peng Lyu 已提交
272
			let index = this.model!.getNotebook().cells.indexOf(cell.cell);
R
rebornix 已提交
273 274 275
			if (index >= 0) {
				this.list?.updateDynamicHeight(index, cell, height);
			}
R
rebornix 已提交
276 277 278 279 280 281 282 283 284
		};

		if (this.list?.view.isRendering) {
			DOM.scheduleAtNextAnimationFrame(() => {
				relayout(cell, height);
			});
		} else {
			relayout(cell, height);
		}
285 286
	}

R
rebornix 已提交
287
	updateViewCells() {
R
rebornix 已提交
288 289 290 291 292
		if (this.list?.view.isRendering) {
			DOM.scheduleAtNextAnimationFrame(() => {
				this.list?.rerender();
			});
		} else {
R
rebornix 已提交
293
			this.list?.rerender();
R
rebornix 已提交
294
		}
R
rebornix 已提交
295 296
	}

P
Peng Lyu 已提交
297
	insertEmptyNotebookCell(cell: ViewCell, type: 'code' | 'markdown', direction: 'above' | 'below') {
P
Peng Lyu 已提交
298
		let newCell = new ViewCell({
R
rebornix 已提交
299
			handle: -1,
P
Peng Lyu 已提交
300
			cell_type: type,
P
Peng Lyu 已提交
301
			source: [],
P
Peng Lyu 已提交
302
			outputs: []
P
Peng Lyu 已提交
303
		}, false, this.modelService, this.modeService);
P
Peng Lyu 已提交
304

P
Peng Lyu 已提交
305
		let index = this.model!.getNotebook().cells.indexOf(cell.cell);
P
Peng Lyu 已提交
306 307
		const insertIndex = direction === 'above' ? index : index + 1;

P
Peng Lyu 已提交
308 309
		this.viewCells!.splice(insertIndex, 0, newCell);
		this.model!.insertCell(newCell.cell, insertIndex);
P
Peng Lyu 已提交
310
		this.list?.splice(insertIndex, 0, [newCell]);
P
Peng Lyu 已提交
311 312 313 314 315 316 317 318 319 320 321 322

		if (type === 'markdown') {
			newCell.isEditing = true;
		}
	}

	editNotebookCell(cell: ViewCell): void {
		cell.isEditing = true;
	}

	saveNotebookCell(cell: ViewCell): void {
		cell.isEditing = false;
P
Peng Lyu 已提交
323 324
	}

P
Peng Lyu 已提交
325
	deleteNotebookCell(cell: ViewCell) {
P
Peng Lyu 已提交
326
		let index = this.model!.getNotebook().cells.indexOf(cell.cell);
P
Peng Lyu 已提交
327

P
Peng Lyu 已提交
328 329
		this.viewCells!.splice(index, 1);
		this.model!.deleteCell(cell.cell);
P
Peng Lyu 已提交
330 331 332
		this.list?.splice(index, 1);
	}

P
Peng Lyu 已提交
333 334 335
	layout(dimension: DOM.Dimension): void {
		DOM.toggleClass(this.rootElement, 'mid-width', dimension.width < 1000 && dimension.width >= 600);
		DOM.toggleClass(this.rootElement, 'narrow-width', dimension.width < 600);
P
Peng Lyu 已提交
336 337
		DOM.size(this.body, dimension.width - 20, dimension.height);
		this.list?.layout(dimension.height, dimension.width - 20);
P
Peng Lyu 已提交
338
	}
R
rebornix 已提交
339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364

	protected saveState(): void {
		if (this.input instanceof NotebookEditorInput) {
			this.saveTextEditorViewState(this.input);
		}

		super.saveState();
	}

	private saveTextEditorViewState(input: NotebookEditorInput): void {
		if (this.group) {
			let state: { [key: number]: boolean } = {};
			this.viewCells.filter(cell => cell.isEditing).forEach(cell => state[cell.cell.handle] = true);
			this.editorMemento.saveEditorState(this.group, input, {
				editingCells: state
			});
		}
	}

	private loadTextEditorViewState(input: NotebookEditorInput): INotebookEditorViewState | undefined {
		if (this.group) {
			return this.editorMemento.loadEditorState(this.group, input);
		}

		return;
	}
P
Peng Lyu 已提交
365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401
}

const embeddedEditorBackground = 'walkThrough.embeddedEditorBackground';

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 .monaco-editor-background,
			.monaco-workbench .part.editor > .content .notebook-editor .margin-view-overlays { background: ${color}; }`);
	}
	const link = theme.getColor(textLinkForeground);
	if (link) {
		collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor a { color: ${link}; }`);
	}
	const activeLink = theme.getColor(textLinkActiveForeground);
	if (activeLink) {
		collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor a:hover,
			.monaco-workbench .part.editor > .content .notebook-editor 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}; }`);
	}
});