notebookEditor.ts 16.1 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 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131
class AbstractCellRenderer {
	constructor(
		private handler: NotebookHandler,
		private contextMenuService: IContextMenuService
	) { }

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

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

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

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

export class MarkdownCellRenderer extends AbstractCellRenderer implements IListRenderer<ICell, CellRenderTemplate> {
P
Peng Lyu 已提交
132
	static readonly TEMPLATE_ID = 'markdown_cell';
133
	private disposables: Map<HTMLElement, IDisposable> = new Map();
P
Peng Lyu 已提交
134 135

	constructor(
136 137
		handler: NotebookHandler,
		@IContextMenuService contextMenuService: IContextMenuService
P
Peng Lyu 已提交
138
	) {
139
		super(handler, contextMenuService);
P
Peng Lyu 已提交
140
	}
P
Peng Lyu 已提交
141 142 143 144 145 146 147 148 149 150 151

	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 已提交
152 153 154 155
		const action = document.createElement('div');
		DOM.addClasses(action, 'menu', 'codicon-settings-gear', 'codicon');
		container.appendChild(action);

P
Peng Lyu 已提交
156 157
		return {
			cellContainer: innerContent,
P
Peng Lyu 已提交
158
			menuContainer: action,
P
Peng Lyu 已提交
159 160 161 162
			renderer: renderer
		};
	}

163 164
	renderElement(element: ICell, index: number, templateData: CellRenderTemplate, height: number | undefined): void {
		templateData.cellContainer.innerHTML = marked(element.source.join(''), { renderer: templateData.renderer });
165 166 167 168 169 170
		let disposable = this.disposables.get(templateData.menuContainer);

		if (disposable) {
			disposable.dispose();
			this.disposables.delete(templateData.menuContainer);
		}
P
Peng Lyu 已提交
171 172 173 174 175

		let listener = DOM.addStandardDisposableListener(templateData.menuContainer, 'mousedown', e => {
			const { top, height } = DOM.getDomNodePagePosition(templateData.menuContainer);
			e.preventDefault();

176
			this.showContextMenu(element, e.posx, top + height);
P
Peng Lyu 已提交
177 178
		});

179
		this.disposables.set(templateData.menuContainer, listener);
P
Peng Lyu 已提交
180 181 182 183 184 185 186
	}

	disposeTemplate(templateData: CellRenderTemplate): void {
		// throw nerendererw Error('Method not implemented.');
	}
}

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

	constructor(
194 195
		handler: NotebookHandler,
		@IContextMenuService contextMenuService: IContextMenuService,
P
Peng Lyu 已提交
196 197 198 199 200
		@IConfigurationService private readonly configurationService: IConfigurationService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
		@IModelService private readonly modelService: IModelService,
		@IModeService private readonly modeService: IModeService
	) {
201 202
		super(handler, contextMenuService);

P
Peng Lyu 已提交
203
		const language = 'python';
P
Peng Lyu 已提交
204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
		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 已提交
221
		this.widgetOptions = this.getSimpleCodeEditorWidgetOptions();
P
Peng Lyu 已提交
222 223 224 225 226 227 228 229 230 231
	}

	get templateId() {
		return CodeCellRenderer.TEMPLATE_ID;
	}

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

		return {
244
			cellContainer: innerContent,
P
Peng Lyu 已提交
245
			menuContainer: action,
246
			editor
P
Peng Lyu 已提交
247 248 249
		};
	}

250
	renderElement(element: ICell, index: number, templateData: CellRenderTemplate, height: number | undefined): void {
P
Peng Lyu 已提交
251 252
		const innerContent = templateData.cellContainer;
		const width = innerContent.clientWidth;
253 254 255 256
		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);
257 258 259 260 261
		templateData.editor?.setModel(model);
		templateData.editor?.layout(
			{
				width: width,
				height: totalHeight
P
Peng Lyu 已提交
262
			}
263
		);
P
Peng Lyu 已提交
264 265 266 267 268

		let listener = DOM.addStandardDisposableListener(templateData.menuContainer, 'mousedown', e => {
			const { top, height } = DOM.getDomNodePagePosition(templateData.menuContainer);
			e.preventDefault();

269
			this.showContextMenu(element, e.posx, top + height);
P
Peng Lyu 已提交
270 271 272 273
		});

		this.disposables.set(element, listener);

P
Peng Lyu 已提交
274 275 276 277 278
	}

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

P
Peng Lyu 已提交
280 281 282 283 284 285 286 287 288 289

	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 已提交
290 291 292 293 294 295 296 297 298 299 300 301
	getSimpleCodeEditorWidgetOptions(): ICodeEditorWidgetOptions {
		return {
			isSimpleWidget: false,
			contributions: EditorExtensionsRegistry.getSomeEditorContributions([
				MenuPreventer.ID,
				SelectionClipboardContributionID,
				SuggestController.ID,
				SnippetController2.ID,
				TabCompletionController.ID
			])
		};
	}
P
Peng Lyu 已提交
302 303
}

P
Peng Lyu 已提交
304 305

export class NotebookEditor extends BaseEditor implements NotebookHandler {
P
Peng Lyu 已提交
306 307 308
	static readonly ID: string = 'workbench.editor.notebook';
	private rootElement!: HTMLElement;
	private body!: HTMLElement;
309
	private list: WorkbenchList<ICell> | undefined;
P
Peng Lyu 已提交
310
	private model: NotebookEditorModel | undefined;
P
Peng Lyu 已提交
311 312 313 314 315

	constructor(
		@ITelemetryService telemetryService: ITelemetryService,
		@IThemeService themeService: IThemeService,
		@IInstantiationService private readonly instantiationService: IInstantiationService,
P
Peng Lyu 已提交
316
		@IStorageService storageService: IStorageService
P
Peng Lyu 已提交
317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334
	) {
		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 已提交
335 336 337
		DOM.addClass(this.body, 'cell-list-container');
		this.createCellList();
		DOM.append(parent, this.body);
P
Peng Lyu 已提交
338 339
	}

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

P
Peng Lyu 已提交
343
		const renders = [
P
Peng Lyu 已提交
344 345
			this.instantiationService.createInstance(MarkdownCellRenderer, this),
			this.instantiationService.createInstance(CodeCellRenderer, this)
P
Peng Lyu 已提交
346 347
		];

348
		this.list = this.instantiationService.createInstance<typeof WorkbenchList, WorkbenchList<ICell>>(
P
Peng Lyu 已提交
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370
			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,
371 372
					listHoverOutline: focusBorder,
					listFocusOutline: focusBorder,
373 374 375 376
					listInactiveSelectionBackground: editorBackground,
					listInactiveSelectionForeground: foreground,
					listInactiveFocusBackground: editorBackground,
					listInactiveFocusOutline: editorBackground,
P
Peng Lyu 已提交
377 378 379 380
				}
			}
		);
	}
P
Peng Lyu 已提交
381

P
Peng Lyu 已提交
382
	setInput(input: NotebookEditorInput, options: EditorOptions | undefined, token: CancellationToken): Promise<void> {
383 384 385 386 387
		return super.setInput(input, options, token)
			.then(() => {
				return input.resolve();
			})
			.then(model => {
P
Peng Lyu 已提交
388
				this.model = model;
389 390 391
				this.list?.splice(0, 0, model.getNookbook().cells);
				this.list?.layout();
			});
P
Peng Lyu 已提交
392 393
	}

P
Peng Lyu 已提交
394 395 396 397 398 399 400 401 402 403 404 405 406 407
	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 已提交
408 409 410
	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);
411
		DOM.size(this.body, dimension.width - 20, dimension.height);
412
		this.list?.layout(dimension.height, dimension.width - 20);
P
Peng Lyu 已提交
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
	}
}

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}; }`);
	}
});