notebookEditor.ts 17.0 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, ICell, NotebookEditorModel } from 'vs/workbench/contrib/notebook/browser/notebookEditorInput';
P
Peng Lyu 已提交
13 14 15 16
import { EditorOptions } from 'vs/workbench/common/editor';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
P
Peng Lyu 已提交
17
import { CodeEditorWidget, ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
P
Peng Lyu 已提交
18 19 20 21 22
import * as marked from 'vs/base/common/marked/marked';
import { IModelService } from 'vs/editor/common/services/modelService';
import { URI } from 'vs/base/common/uri';
import { IModeService } from 'vs/editor/common/services/modeService';
import { deepClone } from 'vs/base/common/objects';
P
Peng Lyu 已提交
23
import { IEditorOptions } from 'vs/editor/common/config/editorOptions';
P
Peng Lyu 已提交
24
import { getExtraColor } from 'vs/workbench/contrib/welcome/walkThrough/common/walkThroughUtils';
P
Peng Lyu 已提交
25 26 27
import { textLinkForeground, textLinkActiveForeground, focusBorder, textPreformatForeground, contrastBorder, textBlockQuoteBackground, textBlockQuoteBorder, editorBackground, foreground } from 'vs/platform/theme/common/colorRegistry';
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
import { WorkbenchList } from 'vs/platform/list/browser/listService';
28 29
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
import { getZoomLevel } from 'vs/base/browser/browser';
P
Peng Lyu 已提交
30 31 32 33 34 35
import { EditorExtensionsRegistry } from 'vs/editor/browser/editorExtensions';
import { MenuPreventer } from 'vs/workbench/contrib/codeEditor/browser/menuPreventer';
import { SelectionClipboardContributionID } from 'vs/workbench/contrib/codeEditor/browser/selectionClipboard';
import { SuggestController } from 'vs/editor/contrib/suggest/suggestController';
import { SnippetController2 } from 'vs/editor/contrib/snippet/snippetController2';
import { TabCompletionController } from 'vs/workbench/contrib/snippets/browser/tabCompletion';
P
Peng Lyu 已提交
36 37 38
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { Action } from 'vs/base/common/actions';
import { IDisposable } from 'vs/base/common/lifecycle';
P
Peng Lyu 已提交
39 40 41

const $ = DOM.$;

P
Peng Lyu 已提交
42 43 44 45 46

interface NotebookHandler {
	insertEmptyNotebookCell(cell: ICell, direction: 'above' | 'below'): void;
}

P
Peng Lyu 已提交
47 48
interface CellRenderTemplate {
	cellContainer: HTMLElement;
P
Peng Lyu 已提交
49
	menuContainer: HTMLElement;
50
	renderer?: marked.Renderer; // TODO this can be cached
51
	editor?: CodeEditorWidget;
P
Peng Lyu 已提交
52 53
}

54
export class NotebookCellListDelegate implements IListVirtualDelegate<ICell> {
55
	private _lineHeight: number;
P
Peng Lyu 已提交
56 57 58
	constructor(
		@IConfigurationService private readonly configurationService: IConfigurationService
	) {
59 60 61
		const editorOptions = this.configurationService.getValue<IEditorOptions>('editor');

		this._lineHeight = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()).lineHeight;
P
Peng Lyu 已提交
62 63
	}

64 65
	getHeight(element: ICell): number {
		if (element.cell_type === 'markdown') {
P
Peng Lyu 已提交
66 67
			return 100;
		} else {
P
Peng Lyu 已提交
68
			return Math.max(element.source.length + 1, 4) * this._lineHeight + 16;
P
Peng Lyu 已提交
69 70 71
		}
	}

72
	hasDynamicHeight(element: ICell): boolean {
73
		return element.cell_type === 'markdown';
P
Peng Lyu 已提交
74 75
	}

76 77
	getTemplateId(element: ICell): string {
		if (element.cell_type === 'markdown') {
P
Peng Lyu 已提交
78 79 80 81 82 83 84
			return MarkdownCellRenderer.TEMPLATE_ID;
		} else {
			return CodeCellRenderer.TEMPLATE_ID;
		}
	}
}

85
export class MarkdownCellRenderer implements IListRenderer<ICell, CellRenderTemplate> {
P
Peng Lyu 已提交
86
	static readonly TEMPLATE_ID = 'markdown_cell';
P
Peng Lyu 已提交
87 88 89 90 91 92 93 94
	private disposables: Map<ICell, IDisposable> = new Map();

	constructor(
		private handler: NotebookHandler,
		@IContextMenuService private readonly contextMenuService: IContextMenuService
	) {

	}
P
Peng Lyu 已提交
95 96 97 98 99 100 101 102 103 104 105

	get templateId() {
		return MarkdownCellRenderer.TEMPLATE_ID;
	}

	renderTemplate(container: HTMLElement): CellRenderTemplate {
		const innerContent = document.createElement('div');
		DOM.addClasses(innerContent, 'cell', 'markdown');
		const renderer = new marked.Renderer();
		container.appendChild(innerContent);

P
Peng Lyu 已提交
106 107 108 109
		const action = document.createElement('div');
		DOM.addClasses(action, 'menu', 'codicon-settings-gear', 'codicon');
		container.appendChild(action);

P
Peng Lyu 已提交
110 111
		return {
			cellContainer: innerContent,
P
Peng Lyu 已提交
112
			menuContainer: action,
P
Peng Lyu 已提交
113 114 115 116
			renderer: renderer
		};
	}

117 118
	renderElement(element: ICell, index: number, templateData: CellRenderTemplate, height: number | undefined): void {
		templateData.cellContainer.innerHTML = marked(element.source.join(''), { renderer: templateData.renderer });
P
Peng Lyu 已提交
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175

		let listener = DOM.addStandardDisposableListener(templateData.menuContainer, 'mousedown', e => {
			const { top, height } = DOM.getDomNodePagePosition(templateData.menuContainer);
			// let pad = Math.floor(lineHeight / 3);
			e.preventDefault();

			const actions: Action[] = [];
			const insertAbove = new Action(
				'workbench.notebook.md.insertCellAbove',
				'Insert Cell Above',
				undefined,
				true,
				async () => {
					this.handler.insertEmptyNotebookCell(element, 'above');
				}
			);

			const insertBelow = new Action(
				'workbench.notebook.md.insertCellBelow',
				'Insert Cell Below',
				undefined,
				true,
				async () => {
					this.handler.insertEmptyNotebookCell(element, 'below');
				}
			);

			actions.push(insertAbove);
			actions.push(insertBelow);

			const showContextMenu = (x: number, y: number) => {
				this.contextMenuService.showContextMenu({
					getAnchor: () => {
						return {
							x,
							y
						};
					},
					getActions: () => {
						return actions;
					},
					autoSelectFirstItem: true
				});
			};

			showContextMenu(e.posx, top + height);
		});
		this.disposables.set(element, listener);
	}

	disposeElement(element: ICell, index: number, templateData: CellRenderTemplate, height: number | undefined): void {
		let disposable = this.disposables.get(element);

		if (disposable) {
			disposable.dispose();
			this.disposables.delete(element);
		}
P
Peng Lyu 已提交
176 177
	}

P
Peng Lyu 已提交
178

P
Peng Lyu 已提交
179 180 181 182 183
	disposeTemplate(templateData: CellRenderTemplate): void {
		// throw nerendererw Error('Method not implemented.');
	}
}

184
export class CodeCellRenderer implements IListRenderer<ICell, CellRenderTemplate> {
P
Peng Lyu 已提交
185 186
	static readonly TEMPLATE_ID = 'code_cell';
	private editorOptions: IEditorOptions;
P
Peng Lyu 已提交
187
	private widgetOptions: ICodeEditorWidgetOptions;
P
Peng Lyu 已提交
188
	private disposables: Map<ICell, IDisposable> = new Map();
P
Peng Lyu 已提交
189 190

	constructor(
P
Peng Lyu 已提交
191 192
		private handler: NotebookHandler,
		@IContextMenuService private readonly contextMenuService: IContextMenuService,
P
Peng Lyu 已提交
193 194 195 196 197 198
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IModelService private readonly modelService: IModelService,
		@IModeService private readonly modeService: IModeService
	) {
		const language = 'python';
P
Peng Lyu 已提交
199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
		const editorOptions = deepClone(this.configurationService.getValue<IEditorOptions>('editor', { overrideIdentifier: language }));
		this.editorOptions = {
			...editorOptions,
			scrollBeyondLastLine: false,
			scrollbar: {
				verticalScrollbarSize: 14,
				horizontal: 'auto',
				useShadows: true,
				verticalHasArrows: false,
				horizontalHasArrows: false
			},
			overviewRulerLanes: 3,
			fixedOverflowWidgets: true,
			lineNumbersMinChars: 1,
			minimap: { enabled: false },
		};

P
Peng Lyu 已提交
216
		this.widgetOptions = this.getSimpleCodeEditorWidgetOptions();
P
Peng Lyu 已提交
217 218 219 220 221 222 223 224 225 226
	}

	get templateId() {
		return CodeCellRenderer.TEMPLATE_ID;
	}

	renderTemplate(container: HTMLElement): CellRenderTemplate {
		const innerContent = document.createElement('div');
		DOM.addClasses(innerContent, 'cell', 'code');
		container.appendChild(innerContent);
227 228 229 230 231 232 233
		const editor = this.instantiationService.createInstance(CodeEditorWidget, innerContent, {
			...this.editorOptions,
			dimension: {
				width: 0,
				height: 0
			}
		}, this.widgetOptions);
P
Peng Lyu 已提交
234 235 236
		const action = document.createElement('div');
		DOM.addClasses(action, 'menu', 'codicon-settings-gear', 'codicon');
		container.appendChild(action);
P
Peng Lyu 已提交
237 238

		return {
239
			cellContainer: innerContent,
P
Peng Lyu 已提交
240
			menuContainer: action,
241
			editor
P
Peng Lyu 已提交
242 243 244
		};
	}

245
	renderElement(element: ICell, index: number, templateData: CellRenderTemplate, height: number | undefined): void {
P
Peng Lyu 已提交
246 247
		const innerContent = templateData.cellContainer;
		const width = innerContent.clientWidth;
248 249 250 251
		const lineNum = element.source.length;
		const totalHeight = Math.max(lineNum + 1, 4) * 21;
		const resource = URI.parse(`notebookcell-${index}-${Date.now()}.py`);
		const model = this.modelService.createModel(element.source.join(''), this.modeService.createByFilepathOrFirstLine(resource), resource, false);
252 253 254 255 256
		templateData.editor?.setModel(model);
		templateData.editor?.layout(
			{
				width: width,
				height: totalHeight
P
Peng Lyu 已提交
257
			}
258
		);
P
Peng Lyu 已提交
259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310

		let listener = DOM.addStandardDisposableListener(templateData.menuContainer, 'mousedown', e => {
			const { top, height } = DOM.getDomNodePagePosition(templateData.menuContainer);
			// let pad = Math.floor(lineHeight / 3);
			e.preventDefault();

			const actions: Action[] = [];
			const insertAbove = new Action(
				'workbench.notebook.code.insertCellAbove',
				'Insert Cell Above',
				undefined,
				true,
				async () => {
					// await this._clipboardService.writeText(diff.originalContent[currentLineNumberOffset]);
					this.handler.insertEmptyNotebookCell(element, 'above');
				}
			);

			const insertBelow = new Action(
				'workbench.notebook.code.insertCellBelow',
				'Insert Cell Below',
				undefined,
				true,
				async () => {
					// await this._clipboardService.writeText(diff.originalContent[currentLineNumberOffset]);
					this.handler.insertEmptyNotebookCell(element, 'below');
				}
			);

			actions.push(insertAbove);
			actions.push(insertBelow);

			const showContextMenu = (x: number, y: number) => {
				this.contextMenuService.showContextMenu({
					getAnchor: () => {
						return {
							x,
							y
						};
					},
					getActions: () => {
						return actions;
					},
					autoSelectFirstItem: true
				});
			};

			showContextMenu(e.posx, top + height);
		});

		this.disposables.set(element, listener);

P
Peng Lyu 已提交
311 312 313 314 315
	}

	disposeTemplate(templateData: CellRenderTemplate): void {
		// throw nerendererw Error('Method not implemented.');
	}
P
Peng Lyu 已提交
316

P
Peng Lyu 已提交
317 318 319 320 321 322 323 324 325 326

	disposeElement(element: ICell, index: number, templateData: CellRenderTemplate, height: number | undefined): void {
		let disposable = this.disposables.get(element);

		if (disposable) {
			disposable.dispose();
			this.disposables.delete(element);
		}
	}

P
Peng Lyu 已提交
327 328 329 330 331 332 333 334 335 336 337 338
	getSimpleCodeEditorWidgetOptions(): ICodeEditorWidgetOptions {
		return {
			isSimpleWidget: false,
			contributions: EditorExtensionsRegistry.getSomeEditorContributions([
				MenuPreventer.ID,
				SelectionClipboardContributionID,
				SuggestController.ID,
				SnippetController2.ID,
				TabCompletionController.ID
			])
		};
	}
P
Peng Lyu 已提交
339 340
}

P
Peng Lyu 已提交
341 342

export class NotebookEditor extends BaseEditor implements NotebookHandler {
P
Peng Lyu 已提交
343 344 345
	static readonly ID: string = 'workbench.editor.notebook';
	private rootElement!: HTMLElement;
	private body!: HTMLElement;
346
	private list: WorkbenchList<ICell> | undefined;
P
Peng Lyu 已提交
347
	private model: NotebookEditorModel | undefined;
P
Peng Lyu 已提交
348 349 350 351 352

	constructor(
		@ITelemetryService telemetryService: ITelemetryService,
		@IThemeService themeService: IThemeService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
P
Peng Lyu 已提交
353
		@IStorageService storageService: IStorageService
P
Peng Lyu 已提交
354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
	) {
		super(NotebookEditor.ID, telemetryService, themeService, storageService);
	}
	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 {
		this.body = document.createElement('div'); //DOM.append(parent, $('.notebook-body'));
P
Peng Lyu 已提交
372 373 374
		DOM.addClass(this.body, 'cell-list-container');
		this.createCellList();
		DOM.append(parent, this.body);
P
Peng Lyu 已提交
375 376
	}

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

P
Peng Lyu 已提交
380
		const renders = [
P
Peng Lyu 已提交
381 382
			this.instantiationService.createInstance(MarkdownCellRenderer, this),
			this.instantiationService.createInstance(CodeCellRenderer, this)
P
Peng Lyu 已提交
383 384
		];

385
		this.list = this.instantiationService.createInstance<typeof WorkbenchList, WorkbenchList<ICell>>(
P
Peng Lyu 已提交
386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407
			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,
408 409 410 411 412 413
					listHoverOutline: editorBackground,
					listFocusOutline: editorBackground,
					listInactiveSelectionBackground: editorBackground,
					listInactiveSelectionForeground: foreground,
					listInactiveFocusBackground: editorBackground,
					listInactiveFocusOutline: editorBackground,
P
Peng Lyu 已提交
414 415 416 417
				}
			}
		);
	}
P
Peng Lyu 已提交
418

P
Peng Lyu 已提交
419
	setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
420 421 422 423 424
		return super.setInput(input, options, token)
			.then(() => {
				return input.resolve();
			})
			.then(model => {
P
Peng Lyu 已提交
425
				this.model = model;
426 427 428
				this.list?.splice(0, 0, model.getNookbook().cells);
				this.list?.layout();
			});
P
Peng Lyu 已提交
429 430
	}

P
Peng Lyu 已提交
431 432 433 434 435 436 437 438 439 440 441 442 443 444
	insertEmptyNotebookCell(cell: ICell, direction: 'above' | 'below') {
		let newCell: ICell = {
			source: [],
			cell_type: 'code'
		};

		let index = this.model!.getNookbook().cells.indexOf(cell);

		const insertIndex = direction === 'above' ? index : index + 1;

		this.model!.getNookbook().cells.splice(insertIndex, 0, newCell);
		this.list?.splice(insertIndex, 0, [newCell]);
	}

P
Peng Lyu 已提交
445 446 447
	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);
448
		DOM.size(this.body, dimension.width - 20, dimension.height);
449
		this.list?.layout(dimension.height, dimension.width - 20);
P
Peng Lyu 已提交
450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491
	}
}

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 focusColor = theme.getColor(focusBorder);
	if (focusColor) {
		collector.addRule(`.monaco-workbench .part.editor > .content .notebook-editor a:focus { outline-color: ${focusColor}; }`);
	}
	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}; }`);
	}
});