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

R
rebornix 已提交
6
import { getZoomLevel } from 'vs/base/browser/browser';
P
Peng Lyu 已提交
7
import * as DOM from 'vs/base/browser/dom';
8
import { domEvent } from 'vs/base/browser/event';
R
rebornix 已提交
9
import { IListRenderer, IListVirtualDelegate } from 'vs/base/browser/ui/list/list';
R
Rob Lourens 已提交
10
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
11
import { ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
12
import { IAction } from 'vs/base/common/actions';
13
import { Delayer } from 'vs/base/common/async';
14
import { renderCodicons } from 'vs/base/common/codicons';
15
import { Color } from 'vs/base/common/color';
16
import { Emitter, Event } from 'vs/base/common/event';
17
import { Disposable, DisposableStore, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
P
Peng Lyu 已提交
18
import { deepClone } from 'vs/base/common/objects';
19
import * as platform from 'vs/base/common/platform';
20
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
R
rebornix 已提交
21
import { CodeEditorWidget } from 'vs/editor/browser/widget/codeEditorWidget';
22
import { EditorOption, EDITOR_FONT_DEFAULTS, IEditorOptions } from 'vs/editor/common/config/editorOptions';
P
Peng Lyu 已提交
23
import { BareFontInfo } from 'vs/editor/common/config/fontInfo';
24
import { Range } from 'vs/editor/common/core/range';
C
Christopher Maynard 已提交
25
import { EditorContextKeys } from 'vs/editor/common/editorContextKeys';
26 27 28 29
import { ITextModel } from 'vs/editor/common/model';
import * as modes from 'vs/editor/common/modes';
import { tokenizeLineToHTML } from 'vs/editor/common/modes/textToHtmlTokenizer';
import { IModeService } from 'vs/editor/common/services/modeService';
30 31
import { MenuEntryActionViewItem, SubmenuEntryActionViewItem } from 'vs/platform/actions/browser/menuEntryActionViewItem';
import { IMenu, MenuItemAction, SubmenuItemAction } from 'vs/platform/actions/common/actions';
R
rebornix 已提交
32
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
33
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
P
Peng Lyu 已提交
34
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
R
rebornix 已提交
35
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
C
Christopher Maynard 已提交
36
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
R
Rob Lourens 已提交
37 38
import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding';
import { INotificationService } from 'vs/platform/notification/common/notification';
R
Rob Lourens 已提交
39
import { BOTTOM_CELL_TOOLBAR_HEIGHT, CELL_BOTTOM_MARGIN, CELL_TOP_MARGIN, EDITOR_BOTTOM_PADDING, EDITOR_TOOLBAR_HEIGHT, EDITOR_TOP_PADDING } from 'vs/workbench/contrib/notebook/browser/constants';
40
import { CancelCellAction, ChangeCellLanguageAction, ExecuteCellAction, INotebookCellActionContext } from 'vs/workbench/contrib/notebook/browser/contrib/coreActions';
R
Rob Lourens 已提交
41
import { BaseCellRenderTemplate, CellCollapseState, CellEditState, CodeCellRenderTemplate, ICellViewModel, INotebookCellList, INotebookEditor, isCodeCellRenderTemplate, MarkdownCellRenderTemplate } from 'vs/workbench/contrib/notebook/browser/notebookBrowser';
42
import { CellContextKeyManager } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellContextKeys';
43
import { CellMenus } from 'vs/workbench/contrib/notebook/browser/view/renderers/cellMenus';
R
rebornix 已提交
44
import { CodeCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/codeCell';
45
import { StatefulMarkdownCell } from 'vs/workbench/contrib/notebook/browser/view/renderers/markdownCell';
46 47 48
import { CodeCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/codeCellViewModel';
import { MarkdownCellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/markdownCellViewModel';
import { CellViewModel } from 'vs/workbench/contrib/notebook/browser/viewModel/notebookViewModel';
49
import { CellKind, NotebookCellMetadata, NotebookCellRunState } from 'vs/workbench/contrib/notebook/common/notebookCommon';
50
import { createAndFillInActionBarActionsWithVerticalSeparators, VerticalSeparator, VerticalSeparatorViewItem } from './cellActionView';
51

R
Rob Lourens 已提交
52 53 54
const $ = DOM.$;

export class NotebookCellListDelegate implements IListVirtualDelegate<CellViewModel> {
55
	private readonly lineHeight: number;
56

P
Peng Lyu 已提交
57 58 59 60
	constructor(
		@IConfigurationService private readonly configurationService: IConfigurationService
	) {
		const editorOptions = this.configurationService.getValue<IEditorOptions>('editor');
61
		this.lineHeight = BareFontInfo.createFromRawSettings(editorOptions, getZoomLevel()).lineHeight;
P
Peng Lyu 已提交
62 63
	}

R
rebornix 已提交
64
	getHeight(element: CellViewModel): number {
65
		return element.getHeight(this.lineHeight);
P
Peng Lyu 已提交
66 67
	}

R
rebornix 已提交
68
	hasDynamicHeight(element: CellViewModel): boolean {
P
Peng Lyu 已提交
69 70 71
		return element.hasDynamicHeight();
	}

R
rebornix 已提交
72
	getTemplateId(element: CellViewModel): string {
R
rebornix 已提交
73
		if (element.cellKind === CellKind.Markdown) {
P
Peng Lyu 已提交
74 75 76 77 78 79 80
			return MarkdownCellRenderer.TEMPLATE_ID;
		} else {
			return CodeCellRenderer.TEMPLATE_ID;
		}
	}
}

81
export class CodiconActionViewItem extends MenuEntryActionViewItem {
R
rebornix 已提交
82 83
	constructor(
		readonly _action: MenuItemAction,
84 85 86
		keybindingService: IKeybindingService,
		notificationService: INotificationService,
		contextMenuService: IContextMenuService
R
rebornix 已提交
87
	) {
88
		super(_action, keybindingService, notificationService, contextMenuService);
R
rebornix 已提交
89 90 91 92 93 94 95 96
	}
	updateLabel(): void {
		if (this.options.label && this.label) {
			this.label.innerHTML = renderCodicons(this._commandAction.label ?? '');
		}
	}
}

97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118
export class CellEditorOptions {

	private static fixedEditorOptions: IEditorOptions = {
		padding: {
			top: EDITOR_TOP_PADDING,
			bottom: EDITOR_BOTTOM_PADDING
		},
		scrollBeyondLastLine: false,
		scrollbar: {
			verticalScrollbarSize: 14,
			horizontal: 'auto',
			useShadows: true,
			verticalHasArrows: false,
			horizontalHasArrows: false,
			alwaysConsumeMouseWheel: false
		},
		renderLineHighlightOnlyWhenFocus: true,
		overviewRulerLanes: 0,
		selectOnLineNumbers: false,
		lineNumbers: 'off',
		lineDecorationsWidth: 0,
		glyphMargin: false,
119
		fixedOverflowWidgets: true,
120
		minimap: { enabled: false },
J
Johannes Rieken 已提交
121
		renderValidationDecorations: 'on'
122 123 124
	};

	private _value: IEditorOptions;
125
	private disposable: IDisposable;
126 127 128 129 130 131

	private readonly _onDidChange = new Emitter<IEditorOptions>();
	readonly onDidChange: Event<IEditorOptions> = this._onDidChange.event;

	constructor(configurationService: IConfigurationService, language: string) {

132
		this.disposable = configurationService.onDidChangeConfiguration(e => {
133 134 135 136 137 138 139 140
			if (e.affectsConfiguration('editor')) {
				this._value = computeEditorOptions();
				this._onDidChange.fire(this.value);
			}
		});

		const computeEditorOptions = () => {
			const editorOptions = deepClone(configurationService.getValue<IEditorOptions>('editor', { overrideIdentifier: language }));
141
			const computed = {
142 143 144
				...editorOptions,
				...CellEditorOptions.fixedEditorOptions
			};
145 146 147 148 149 150

			if (!computed.folding) {
				computed.lineDecorationsWidth = 16;
			}

			return computed;
151 152 153 154 155 156 157
		};

		this._value = computeEditorOptions();
	}

	dispose(): void {
		this._onDidChange.dispose();
158
		this.disposable.dispose();
159 160 161 162 163
	}

	get value(): IEditorOptions {
		return this._value;
	}
164 165 166 167 168 169 170

	setGlyphMargin(gm: boolean): void {
		if (gm !== this._value.glyphMargin) {
			this._value.glyphMargin = gm;
			this._onDidChange.fire(this.value);
		}
	}
171 172
}

173
abstract class AbstractCellRenderer {
174 175
	protected readonly editorOptions: CellEditorOptions;
	protected readonly cellMenus: CellMenus;
P
Peng Lyu 已提交
176 177

	constructor(
178 179 180
		protected readonly instantiationService: IInstantiationService,
		protected readonly notebookEditor: INotebookEditor,
		protected readonly contextMenuService: IContextMenuService,
181
		configurationService: IConfigurationService,
182 183
		private readonly keybindingService: IKeybindingService,
		private readonly notificationService: INotificationService,
184
		protected readonly contextKeyServiceProvider: (container?: HTMLElement) => IContextKeyService,
185
		language: string,
R
Rob Lourens 已提交
186
		protected readonly dndController: CellDragAndDropController
P
Peng Lyu 已提交
187
	) {
188
		this.editorOptions = new CellEditorOptions(configurationService, language);
189
		this.cellMenus = this.instantiationService.createInstance(CellMenus);
190 191 192 193
	}

	dispose() {
		this.editorOptions.dispose();
P
Peng Lyu 已提交
194 195
	}

196
	protected createBetweenCellToolbar(container: HTMLElement, disposables: DisposableStore, contextKeyService: IContextKeyService): ToolBar {
R
rebornix 已提交
197 198 199 200 201 202 203 204 205 206 207
		const toolbar = new ToolBar(container, this.contextMenuService, {
			actionViewItemProvider: action => {
				if (action instanceof MenuItemAction) {
					const item = new CodiconActionViewItem(action, this.keybindingService, this.notificationService, this.contextMenuService);
					return item;
				}

				return undefined;
			}
		});

208 209
		const cellMenu = this.instantiationService.createInstance(CellMenus);
		const menu = disposables.add(cellMenu.getCellInsertionMenu(contextKeyService));
R
rebornix 已提交
210

211
		const actions = this.getCellToolbarActions(menu);
J
João Moreno 已提交
212
		toolbar.setActions(actions.primary, actions.secondary);
R
rebornix 已提交
213

214 215
		return toolbar;
	}
216

217 218
	protected setBetweenCellToolbarContext(templateData: BaseCellRenderTemplate, element: CodeCellViewModel | MarkdownCellViewModel, context: INotebookCellActionContext): void {
		templateData.betweenCellToolbar.context = context;
R
rebornix 已提交
219

220
		const container = templateData.bottomCellContainer;
221 222 223 224
		const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset;
		container.style.top = `${bottomToolbarOffset}px`;

		templateData.elementDisposables.add(element.onDidChangeLayout(() => {
R
rebornix 已提交
225 226
			const bottomToolbarOffset = element.layoutInfo.bottomToolbarOffset;
			container.style.top = `${bottomToolbarOffset}px`;
227
		}));
R
rebornix 已提交
228 229
	}

230 231
	protected createToolbar(container: HTMLElement): ToolBar {
		const toolbar = new ToolBar(container, this.contextMenuService, {
232
			getKeyBinding: action => this.keybindingService.lookupKeybinding(action.id),
233 234
			actionViewItemProvider: action => {
				if (action instanceof MenuItemAction) {
235 236 237
					return this.instantiationService.createInstance(MenuEntryActionViewItem, action);
				} else if (action instanceof SubmenuItemAction) {
					return this.instantiationService.createInstance(SubmenuEntryActionViewItem, action);
238 239
				}

R
rebornix 已提交
240 241 242 243
				if (action.id === VerticalSeparator.ID) {
					return new VerticalSeparatorViewItem(undefined, action);
				}

244 245 246 247 248 249 250
				return undefined;
			}
		});

		return toolbar;
	}

R
Rob Lourens 已提交
251 252 253
	private getCellToolbarActions(menu: IMenu): { primary: IAction[], secondary: IAction[] } {
		const primary: IAction[] = [];
		const secondary: IAction[] = [];
254
		const result = { primary, secondary };
255

R
rebornix 已提交
256
		createAndFillInActionBarActionsWithVerticalSeparators(menu, { shouldForwardArgs: true }, result, g => /^inline/.test(g));
257

258
		return result;
259
	}
260

261
	protected setupCellToolbarActions(templateData: BaseCellRenderTemplate, disposables: DisposableStore): void {
262
		const updateActions = () => {
263
			const actions = this.getCellToolbarActions(templateData.titleMenu);
264

265
			const hadFocus = DOM.isAncestor(document.activeElement, templateData.toolbar.getContainer());
J
João Moreno 已提交
266
			templateData.toolbar.setActions(actions.primary, actions.secondary);
267 268 269
			if (hadFocus) {
				this.notebookEditor.focus();
			}
270

271 272 273
			if (actions.primary.length || actions.secondary.length) {
				templateData.container.classList.add('cell-has-toolbar-actions');
				if (isCodeCellRenderTemplate(templateData)) {
274
					templateData.focusIndicatorLeft.style.top = `${EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN}px`;
275
					templateData.focusIndicatorRight.style.top = `${EDITOR_TOOLBAR_HEIGHT + CELL_TOP_MARGIN}px`;
276 277 278 279
				}
			} else {
				templateData.container.classList.remove('cell-has-toolbar-actions');
				if (isCodeCellRenderTemplate(templateData)) {
280
					templateData.focusIndicatorLeft.style.top = `${CELL_TOP_MARGIN}px`;
281
					templateData.focusIndicatorRight.style.top = `${CELL_TOP_MARGIN}px`;
282 283 284 285 286
				}
			}
		};

		updateActions();
287
		disposables.add(templateData.titleMenu.onDidChange(() => {
288 289 290 291
			if (this.notebookEditor.isDisposed) {
				return;
			}

292 293 294
			updateActions();
		}));
	}
295

R
Rob Lourens 已提交
296
	protected commonRenderTemplate(templateData: BaseCellRenderTemplate): void {
297
		templateData.disposables.add(DOM.addDisposableListener(templateData.container, DOM.EventType.FOCUS, () => {
R
Rob Lourens 已提交
298 299 300
			if (templateData.currentRenderedCell) {
				this.notebookEditor.selectElement(templateData.currentRenderedCell);
			}
301
		}, true));
R
Rob Lourens 已提交
302

303
		this.addExpandListener(templateData);
R
Rob Lourens 已提交
304
	}
305

R
Rob Lourens 已提交
306
	protected commonRenderElement(element: ICellViewModel, index: number, templateData: BaseCellRenderTemplate): void {
307 308 309 310 311 312
		if (element.dragging) {
			templateData.container.classList.add(DRAGGING_CLASS);
		} else {
			templateData.container.classList.remove(DRAGGING_CLASS);
		}
	}
R
Rob Lourens 已提交
313

314 315
	protected addExpandListener(templateData: BaseCellRenderTemplate): void {
		templateData.disposables.add(domEvent(templateData.expandButton, DOM.EventType.CLICK)(() => {
R
Rob Lourens 已提交
316 317 318 319
			if (!templateData.currentRenderedCell) {
				return;
			}

320 321 322 323 324
			if (templateData.currentRenderedCell.collapseState === CellCollapseState.Collapsed) {
				templateData.currentRenderedCell.collapseState = CellCollapseState.Normal;
			} else if (templateData.currentRenderedCell.outputCollapseState === CellCollapseState.Collapsed) {
				templateData.currentRenderedCell.outputCollapseState = CellCollapseState.Normal;
			}
R
Rob Lourens 已提交
325 326
		}));
	}
327 328 329 330 331 332 333 334 335

	protected setupCollapsedPart(container: HTMLElement): { collapsedPart: HTMLElement, expandButton: HTMLElement } {
		const collapsedPart = DOM.append(container, $('.cell.cell-collapsed-part'));
		collapsedPart.innerHTML = renderCodicons('$(unfold)');
		const expandButton = collapsedPart.querySelector('.codicon') as HTMLElement;
		DOM.hide(collapsedPart);

		return { collapsedPart, expandButton };
	}
P
Peng Lyu 已提交
336 337
}

338
export class MarkdownCellRenderer extends AbstractCellRenderer implements IListRenderer<MarkdownCellViewModel, MarkdownCellRenderTemplate> {
P
Peng Lyu 已提交
339 340 341
	static readonly TEMPLATE_ID = 'markdown_cell';

	constructor(
342
		notebookEditor: INotebookEditor,
R
Rob Lourens 已提交
343
		dndController: CellDragAndDropController,
344
		private renderedEditors: Map<ICellViewModel, ICodeEditor | undefined>,
345
		contextKeyServiceProvider: (container?: HTMLElement) => IContextKeyService,
346
		@IInstantiationService instantiationService: IInstantiationService,
P
Peng Lyu 已提交
347
		@IConfigurationService configurationService: IConfigurationService,
348 349 350
		@IContextMenuService contextMenuService: IContextMenuService,
		@IKeybindingService keybindingService: IKeybindingService,
		@INotificationService notificationService: INotificationService,
P
Peng Lyu 已提交
351
	) {
352
		super(instantiationService, notebookEditor, contextMenuService, configurationService, keybindingService, notificationService, contextKeyServiceProvider, 'markdown', dndController);
P
Peng Lyu 已提交
353 354 355 356 357 358
	}

	get templateId() {
		return MarkdownCellRenderer.TEMPLATE_ID;
	}

359
	renderTemplate(container: HTMLElement): MarkdownCellRenderTemplate {
R
Rob Lourens 已提交
360
		container.classList.add('markdown-cell-row');
R
Rob Lourens 已提交
361
		const disposables = new DisposableStore();
R
Rob Lourens 已提交
362
		const contextKeyService = disposables.add(this.contextKeyServiceProvider(container));
R
Rob Lourens 已提交
363
		const toolbar = disposables.add(this.createToolbar(container));
364
		const focusIndicator = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left'));
R
Rob Lourens 已提交
365
		focusIndicator.setAttribute('draggable', 'true');
R
Rob Lourens 已提交
366

R
Rob Lourens 已提交
367
		const codeInnerContent = DOM.append(container, $('.cell.code'));
368
		const editorPart = DOM.append(codeInnerContent, $('.cell-editor-part'));
R
Rob Lourens 已提交
369
		const editorContainer = DOM.append(editorPart, $('.cell-editor-container'));
370
		editorPart.style.display = 'none';
P
Peng Lyu 已提交
371

R
Rob Lourens 已提交
372
		const innerContent = DOM.append(container, $('.cell.markdown'));
R
rebornix 已提交
373
		const foldingIndicator = DOM.append(container, DOM.$('.notebook-folding-indicator'));
R
rebornix 已提交
374

375
		const { collapsedPart, expandButton } = this.setupCollapsedPart(container);
R
Rob Lourens 已提交
376

R
rebornix 已提交
377
		const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container'));
378
		const betweenCellToolbar = disposables.add(this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService));
379

380
		const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart);
381
		const titleMenu = disposables.add(this.cellMenus.getCellTitleMenu(contextKeyService));
382

R
Rob Lourens 已提交
383
		const templateData: MarkdownCellRenderTemplate = {
R
Rob Lourens 已提交
384
			collapsedPart,
385
			expandButton,
386
			contextKeyService,
387
			container,
P
Peng Lyu 已提交
388
			cellContainer: innerContent,
389 390
			editorPart,
			editorContainer,
391
			focusIndicatorLeft: focusIndicator,
R
rebornix 已提交
392
			foldingIndicator,
393
			disposables,
394
			elementDisposables: new DisposableStore(),
395
			toolbar,
396
			betweenCellToolbar,
R
Rob Lourens 已提交
397
			bottomCellContainer,
398 399
			statusBarContainer: statusBar.statusBarContainer,
			languageStatusBarItem: statusBar.languageStatusBarItem,
400
			titleMenu,
R
rebornix 已提交
401
			toJSON: () => { return {}; }
P
Peng Lyu 已提交
402
		};
403
		this.dndController.registerDragHandle(templateData, () => this.getDragImage(templateData));
R
Rob Lourens 已提交
404
		this.commonRenderTemplate(templateData);
405

R
Rob Lourens 已提交
406
		return templateData;
P
Peng Lyu 已提交
407 408
	}

409
	private getDragImage(templateData: MarkdownCellRenderTemplate): HTMLElement {
R
Rob Lourens 已提交
410
		if (templateData.currentRenderedCell!.editState === CellEditState.Editing) {
411
			return this.getEditDragImage(templateData);
R
Rob Lourens 已提交
412
		} else {
413
			return this.getMarkdownDragImage(templateData);
R
Rob Lourens 已提交
414 415 416
		}
	}

417
	private getMarkdownDragImage(templateData: MarkdownCellRenderTemplate): HTMLElement {
418 419
		const dragImageContainer = DOM.$('.cell-drag-image.monaco-list-row.focused.markdown-cell-row');
		dragImageContainer.innerHTML = templateData.container.innerHTML;
R
Rob Lourens 已提交
420 421 422 423 424 425 426 427

		// Remove all rendered content nodes after the
		const markdownContent = dragImageContainer.querySelector('.cell.markdown')!;
		const contentNodes = markdownContent.children[0].children;
		for (let i = contentNodes.length - 1; i >= 1; i--) {
			contentNodes.item(i)!.remove();
		}

428 429 430
		return dragImageContainer;
	}

431
	private getEditDragImage(templateData: MarkdownCellRenderTemplate): HTMLElement {
R
Rob Lourens 已提交
432 433
		return new CodeCellDragImageRenderer().getDragImage(templateData, templateData.currentEditor!, 'markdown');
	}
434

435
	renderElement(element: MarkdownCellViewModel, index: number, templateData: MarkdownCellRenderTemplate, height: number | undefined): void {
436 437
		this.commonRenderElement(element, index, templateData);

R
Rob Lourens 已提交
438
		templateData.currentRenderedCell = element;
R
Rob Lourens 已提交
439
		templateData.currentEditor = undefined;
440
		templateData.editorPart!.style.display = 'none';
R
rebornix 已提交
441 442 443 444 445
		templateData.cellContainer.innerHTML = '';
		let renderedHTML = element.getHTML();
		if (renderedHTML) {
			templateData.cellContainer.appendChild(renderedHTML);
		}
P
Peng Lyu 已提交
446

447 448 449
		if (height === undefined) {
			return;
		}
P
Peng Lyu 已提交
450

451
		const elementDisposables = templateData.elementDisposables;
452

453
		elementDisposables.add(new CellContextKeyManager(templateData.contextKeyService, this.notebookEditor.viewModel?.notebookDocument!, element));
R
rebornix 已提交
454

455
		// render toolbar first
456
		this.setupCellToolbarActions(templateData, elementDisposables);
R
rebornix 已提交
457

458 459 460 461 462 463
		const toolbarContext = <INotebookCellActionContext>{
			cell: element,
			notebookEditor: this.notebookEditor,
			$mid: 12
		};
		templateData.toolbar.context = toolbarContext;
R
rebornix 已提交
464

465
		this.setBetweenCellToolbarContext(templateData, element, toolbarContext);
466

467 468
		const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, templateData.contextKeyService]));
		const markdownCell = scopedInstaService.createInstance(StatefulMarkdownCell, this.notebookEditor, element, templateData, this.editorOptions.value, this.renderedEditors);
469 470
		elementDisposables.add(this.editorOptions.onDidChange(newValue => markdownCell.updateEditorOptions(newValue)));
		elementDisposables.add(markdownCell);
471

472
		templateData.languageStatusBarItem.update(element, this.notebookEditor);
473 474
	}

475 476
	disposeTemplate(templateData: MarkdownCellRenderTemplate): void {
		templateData.disposables.clear();
P
Peng Lyu 已提交
477 478
	}

479 480 481 482 483 484 485
	disposeElement(element: ICellViewModel, _index: number, templateData: MarkdownCellRenderTemplate): void {
		templateData.elementDisposables.clear();
		element.getCellDecorations().forEach(e => {
			if (e.className) {
				templateData.container.classList.remove(e.className);
			}
		});
P
Peng Lyu 已提交
486 487 488
	}
}

489
const DRAGGING_CLASS = 'cell-dragging';
R
Rob Lourens 已提交
490 491
const GLOBAL_DRAG_CLASS = 'global-drag-active';

492 493
type DragImageProvider = () => HTMLElement;

494 495 496 497 498 499 500 501
interface CellDragEvent {
	browserEvent: DragEvent;
	draggedOverCell: ICellViewModel;
	cellTop: number;
	cellHeight: number;
	dragPosRatio: number;
}

R
Rob Lourens 已提交
502
export class CellDragAndDropController extends Disposable {
503
	// TODO@roblourens - should probably use dataTransfer here, but any dataTransfer set makes the editor think I am dropping a file, need
R
Rob Lourens 已提交
504 505 506
	// to figure out how to prevent that
	private currentDraggedCell: ICellViewModel | undefined;

507 508 509 510 511 512 513
	private listInsertionIndicator: HTMLElement;

	private list!: INotebookCellList;

	private isScrolling = false;
	private scrollingDelayer: Delayer<void>;

R
Rob Lourens 已提交
514
	constructor(
515 516
		private readonly notebookEditor: INotebookEditor,
		insertionIndicatorContainer: HTMLElement
R
Rob Lourens 已提交
517 518 519
	) {
		super();

520 521
		this.listInsertionIndicator = DOM.append(insertionIndicatorContainer, $('.cell-list-insertion-indicator'));

R
Rob Lourens 已提交
522 523
		this._register(domEvent(document.body, DOM.EventType.DRAG_START, true)(this.onGlobalDragStart.bind(this)));
		this._register(domEvent(document.body, DOM.EventType.DRAG_END, true)(this.onGlobalDragEnd.bind(this)));
524 525

		const addCellDragListener = (eventType: string, handler: (e: CellDragEvent) => void) => {
R
Rob Lourens 已提交
526 527 528 529 530 531 532 533 534
			this._register(DOM.addDisposableListener(
				notebookEditor.getDomNode(),
				eventType,
				e => {
					const cellDragEvent = this.toCellDragEvent(e);
					if (cellDragEvent) {
						handler(cellDragEvent);
					}
				}));
535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555
		};

		addCellDragListener(DOM.EventType.DRAG_OVER, event => {
			event.browserEvent.preventDefault();
			this.onCellDragover(event);
		});
		addCellDragListener(DOM.EventType.DROP, event => {
			event.browserEvent.preventDefault();
			this.onCellDrop(event);
		});
		addCellDragListener(DOM.EventType.DRAG_LEAVE, event => {
			event.browserEvent.preventDefault();
			this.onCellDragLeave(event);
		});

		this.scrollingDelayer = new Delayer(200);
	}

	setList(value: INotebookCellList) {
		this.list = value;

556 557 558 559 560
		this.list.onWillScroll(e => {
			if (!e.scrollTopChanged) {
				return;
			}

561 562 563 564 565 566 567 568 569 570 571 572
			this.setInsertIndicatorVisibility(false);
			this.isScrolling = true;
			this.scrollingDelayer.trigger(() => {
				this.isScrolling = false;
			});
		});
	}

	private setInsertIndicatorVisibility(visible: boolean) {
		this.listInsertionIndicator.style.opacity = visible ? '1' : '0';
	}

R
Rob Lourens 已提交
573
	private toCellDragEvent(event: DragEvent): CellDragEvent | undefined {
574 575 576
		const targetTop = this.notebookEditor.getDomNode().getBoundingClientRect().top;
		const dragOffset = this.list.scrollTop + event.clientY - targetTop;
		const draggedOverCell = this.list.elementAt(dragOffset);
R
Rob Lourens 已提交
577 578 579 580
		if (!draggedOverCell) {
			return undefined;
		}

581 582 583 584 585 586 587 588 589 590 591 592 593
		const cellTop = this.list.getAbsoluteTopOfElement(draggedOverCell);
		const cellHeight = this.list.elementHeight(draggedOverCell);

		const dragPosInElement = dragOffset - cellTop;
		const dragPosRatio = dragPosInElement / cellHeight;

		return <CellDragEvent>{
			browserEvent: event,
			draggedOverCell,
			cellTop,
			cellHeight,
			dragPosRatio
		};
R
Rob Lourens 已提交
594 595
	}

596 597 598 599
	clearGlobalDragState() {
		this.notebookEditor.getDomNode().classList.remove(GLOBAL_DRAG_CLASS);
	}

R
Rob Lourens 已提交
600 601 602 603 604 605 606
	private onGlobalDragStart() {
		this.notebookEditor.getDomNode().classList.add(GLOBAL_DRAG_CLASS);
	}

	private onGlobalDragEnd() {
		this.notebookEditor.getDomNode().classList.remove(GLOBAL_DRAG_CLASS);
	}
R
Rob Lourens 已提交
607

608
	private onCellDragover(event: CellDragEvent): void {
609 610 611 612
		if (!event.browserEvent.dataTransfer) {
			return;
		}

R
Rob Lourens 已提交
613
		if (!this.currentDraggedCell) {
614 615 616 617
			event.browserEvent.dataTransfer.dropEffect = 'none';
			return;
		}

618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639
		if (this.isScrolling || this.currentDraggedCell === event.draggedOverCell) {
			this.setInsertIndicatorVisibility(false);
			return;
		}

		const dropDirection = this.getDropInsertDirection(event);
		const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight;
		const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop;
		if (insertionIndicatorTop >= 0) {
			this.listInsertionIndicator.style.top = `${insertionIndicatorAbsolutePos - this.list.scrollTop}px`;
			this.setInsertIndicatorVisibility(true);
		} else {
			this.setInsertIndicatorVisibility(false);
		}
	}

	private getDropInsertDirection(event: CellDragEvent): 'above' | 'below' {
		return event.dragPosRatio < 0.5 ? 'above' : 'below';
	}

	private onCellDrop(event: CellDragEvent): void {
		const draggedCell = this.currentDraggedCell!;
640 641 642 643 644 645 646 647 648 649 650 651
		let draggedCells: ICellViewModel[] = [draggedCell];

		if (draggedCell.cellKind === CellKind.Markdown) {
			const currCellIndex = this.notebookEditor.viewModel!.getCellIndex(draggedCell);
			const nextVisibleCellIndex = this.notebookEditor.viewModel!.getNextVisibleCellIndex(currCellIndex);

			if (nextVisibleCellIndex > currCellIndex + 1) {
				// folding ;)
				draggedCells = this.notebookEditor.viewModel!.viewCells.slice(currCellIndex, nextVisibleCellIndex);
			}
		}

652 653 654 655 656 657 658 659 660 661 662 663 664 665
		this.dragCleanup();

		const isCopy = (event.browserEvent.ctrlKey && !platform.isMacintosh) || (event.browserEvent.altKey && platform.isMacintosh);

		const dropDirection = this.getDropInsertDirection(event);
		const insertionIndicatorAbsolutePos = dropDirection === 'above' ? event.cellTop : event.cellTop + event.cellHeight;
		const insertionIndicatorTop = insertionIndicatorAbsolutePos - this.list.scrollTop;
		const editorHeight = this.notebookEditor.getDomNode().getBoundingClientRect().height;
		if (insertionIndicatorTop < 0 || insertionIndicatorTop > editorHeight) {
			// Ignore drop, insertion point is off-screen
			return;
		}

		if (isCopy) {
666
			this.copyCells(draggedCells, event.draggedOverCell, dropDirection);
667
		} else {
668
			this.moveCells(draggedCells, event.draggedOverCell, dropDirection);
669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687
		}
	}

	private onCellDragLeave(event: CellDragEvent): void {
		if (!event.browserEvent.relatedTarget || !DOM.isAncestor(event.browserEvent.relatedTarget as HTMLElement, this.notebookEditor.getDomNode())) {
			this.setInsertIndicatorVisibility(false);
		}
	}

	private dragCleanup(): void {
		if (this.currentDraggedCell) {
			this.currentDraggedCell.dragging = false;
			this.currentDraggedCell = undefined;
		}

		this.setInsertIndicatorVisibility(false);
	}

	registerDragHandle(templateData: BaseCellRenderTemplate, dragImageProvider: DragImageProvider): void {
R
Rob Lourens 已提交
688
		const container = templateData.container;
689
		const dragHandle = templateData.focusIndicatorLeft;
R
Rob Lourens 已提交
690

691
		templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_END)(() => {
692 693
			// Note, templateData may have a different element rendered into it by now
			container.classList.remove(DRAGGING_CLASS);
694
			this.dragCleanup();
R
Rob Lourens 已提交
695 696
		}));

697
		templateData.disposables.add(domEvent(dragHandle, DOM.EventType.DRAG_START)(event => {
R
Rob Lourens 已提交
698 699 700 701
			if (!event.dataTransfer) {
				return;
			}

702 703
			this.currentDraggedCell = templateData.currentRenderedCell!;
			this.currentDraggedCell.dragging = true;
704 705

			const dragImage = dragImageProvider();
R
Rob Lourens 已提交
706 707
			container.parentElement!.appendChild(dragImage);
			event.dataTransfer.setDragImage(dragImage, 0, 0);
708
			setTimeout(() => container.parentElement!.removeChild(dragImage!), 0); // Comment this out to debug drag image layout
R
Rob Lourens 已提交
709

710
			container.classList.add(DRAGGING_CLASS);
711
		}));
R
Rob Lourens 已提交
712
	}
R
Rob Lourens 已提交
713

714
	private async moveCells(draggedCells: ICellViewModel[], ontoCell: ICellViewModel, direction: 'above' | 'below') {
R
rebornix 已提交
715 716 717
		const relativeToIndex = this.notebookEditor!.viewModel!.getCellIndex(ontoCell);
		const newIdx = direction === 'above' ? relativeToIndex : relativeToIndex + 1;

718
		this.notebookEditor.textModel!.pushStackElement('Move Cells');
R
rebornix 已提交
719 720
		for (let i = draggedCells.length - 1; i >= 0; i--) {
			await this.notebookEditor.moveCellToIdx(draggedCells[i], newIdx);
721
		}
R
rebornix 已提交
722

723
		this.notebookEditor.textModel!.pushStackElement('Move Cells');
724 725
	}

726 727 728 729 730
	private copyCells(draggedCells: ICellViewModel[], ontoCell: ICellViewModel, direction: 'above' | 'below') {
		this.notebookEditor.textModel!.pushStackElement('Copy Cells');
		let firstNewCell: ICellViewModel | undefined = undefined;
		let firstNewCellState: CellEditState = CellEditState.Preview;
		for (let i = 0; i < draggedCells.length; i++) {
R
rebornix 已提交
731
			const draggedCell = draggedCells[i];
732 733 734 735 736 737
			const newCell = this.notebookEditor.insertNotebookCell(ontoCell, draggedCell.cellKind, direction, draggedCell.getText());

			if (newCell && !firstNewCell) {
				firstNewCell = newCell;
				firstNewCellState = draggedCell.editState;
			}
R
Rob Lourens 已提交
738
		}
739 740 741

		if (firstNewCell) {
			this.notebookEditor.focusNotebookCell(firstNewCell, firstNewCellState === CellEditState.Editing ? 'editor' : 'container');
R
Rob Lourens 已提交
742
		}
743 744

		this.notebookEditor.textModel!.pushStackElement('Copy Cells');
R
Rob Lourens 已提交
745
	}
746 747
}

748
export class CellLanguageStatusBarItem extends Disposable {
749
	private readonly labelElement: HTMLElement;
750

751 752
	private cell: ICellViewModel | undefined;
	private editor: INotebookEditor | undefined;
753 754 755 756 757 758

	private cellDisposables: DisposableStore;

	constructor(
		readonly container: HTMLElement,
		@IModeService private readonly modeService: IModeService,
759
		@IInstantiationService private readonly instantiationService: IInstantiationService
760 761
	) {
		super();
762
		this.labelElement = DOM.append(container, $('.cell-language-picker'));
763
		this.labelElement.tabIndex = 0;
764

765 766
		this._register(DOM.addDisposableListener(this.labelElement, DOM.EventType.CLICK, () => {
			this.instantiationService.invokeFunction(accessor => {
767
				new ChangeCellLanguageAction().run(accessor, { notebookEditor: this.editor!, cell: this.cell! });
768 769
			});
		}));
770 771 772
		this._register(this.cellDisposables = new DisposableStore());
	}

773
	update(cell: ICellViewModel, editor: INotebookEditor): void {
774
		this.cellDisposables.clear();
775 776
		this.cell = cell;
		this.editor = editor;
777 778

		this.render();
779
		this.cellDisposables.add(this.cell.model.onDidChangeLanguage(() => this.render()));
780 781 782
	}

	private render(): void {
783 784
		const modeId = this.modeService.getModeIdForLanguageName(this.cell!.language) || this.cell!.language;
		this.labelElement.textContent = this.modeService.getLanguageName(modeId) || this.modeService.getLanguageName('plaintext');
785 786 787
	}
}

788 789 790 791 792 793 794 795
class EditorTextRenderer {

	getRichText(editor: ICodeEditor, modelRange: Range): string | null {
		const model = editor.getModel();
		if (!model) {
			return null;
		}

796
		const colorMap = this.getDefaultColorMap();
797 798 799 800 801 802 803 804 805 806 807 808
		const fontInfo = editor.getOptions().get(EditorOption.fontInfo);
		const fontFamily = fontInfo.fontFamily === EDITOR_FONT_DEFAULTS.fontFamily ? fontInfo.fontFamily : `'${fontInfo.fontFamily}', ${EDITOR_FONT_DEFAULTS.fontFamily}`;

		return `<div style="`
			+ `color: ${colorMap[modes.ColorId.DefaultForeground]};`
			+ `background-color: ${colorMap[modes.ColorId.DefaultBackground]};`
			+ `font-family: ${fontFamily};`
			+ `font-weight: ${fontInfo.fontWeight};`
			+ `font-size: ${fontInfo.fontSize}px;`
			+ `line-height: ${fontInfo.lineHeight}px;`
			+ `white-space: pre;`
			+ `">`
809
			+ this.getRichTextLines(model, modelRange, colorMap)
810 811 812
			+ '</div>';
	}

813
	private getRichTextLines(model: ITextModel, modelRange: Range, colorMap: string[]): string {
814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838
		const startLineNumber = modelRange.startLineNumber;
		const startColumn = modelRange.startColumn;
		const endLineNumber = modelRange.endLineNumber;
		const endColumn = modelRange.endColumn;

		const tabSize = model.getOptions().tabSize;

		let result = '';

		for (let lineNumber = startLineNumber; lineNumber <= endLineNumber; lineNumber++) {
			const lineTokens = model.getLineTokens(lineNumber);
			const lineContent = lineTokens.getLineContent();
			const startOffset = (lineNumber === startLineNumber ? startColumn - 1 : 0);
			const endOffset = (lineNumber === endLineNumber ? endColumn - 1 : lineContent.length);

			if (lineContent === '') {
				result += '<br>';
			} else {
				result += tokenizeLineToHTML(lineContent, lineTokens.inflate(), colorMap, startOffset, endOffset, tabSize, platform.isWindows);
			}
		}

		return result;
	}

839
	private getDefaultColorMap(): string[] {
840 841 842 843 844 845 846 847 848 849
		let colorMap = modes.TokenizationRegistry.getColorMap();
		let result: string[] = ['#000000'];
		if (colorMap) {
			for (let i = 1, len = colorMap.length; i < len; i++) {
				result[i] = Color.Format.CSS.formatHex(colorMap[i]);
			}
		}
		return result;
	}
}
R
Rob Lourens 已提交
850

851
class CodeCellDragImageRenderer {
R
Rob Lourens 已提交
852
	getDragImage(templateData: BaseCellRenderTemplate, editor: ICodeEditor, type: 'code' | 'markdown'): HTMLElement {
853
		let dragImage = this.getDragImageImpl(templateData, editor, type);
854
		if (!dragImage) {
855
			// TODO@roblourens I don't think this can happen
856 857 858 859 860 861 862
			dragImage = document.createElement('div');
			dragImage.textContent = '1 cell';
		}

		return dragImage;
	}

863
	private getDragImageImpl(templateData: BaseCellRenderTemplate, editor: ICodeEditor, type: 'code' | 'markdown'): HTMLElement | null {
R
Rob Lourens 已提交
864
		const dragImageContainer = DOM.$(`.cell-drag-image.monaco-list-row.focused.${type}-cell-row`);
865 866 867 868 869 870 871
		dragImageContainer.innerHTML = templateData.container.innerHTML;

		const editorContainer = dragImageContainer.querySelector('.cell-editor-container');
		if (!editorContainer) {
			return null;
		}

R
Rob Lourens 已提交
872
		const richEditorText = new EditorTextRenderer().getRichText(editor, new Range(1, 1, 1, 1000));
873 874 875 876 877 878 879
		if (!richEditorText) {
			return null;
		}

		editorContainer.innerHTML = richEditorText;

		return dragImageContainer;
R
Rob Lourens 已提交
880 881 882
	}
}

883 884 885 886 887
class CellEditorStatusBar {
	readonly cellStatusMessageContainer: HTMLElement;
	readonly cellRunStatusContainer: HTMLElement;
	readonly statusBarContainer: HTMLElement;
	readonly languageStatusBarItem: CellLanguageStatusBarItem;
888
	readonly durationContainer: HTMLElement;
889 890 891 892 893 894 895 896 897

	constructor(
		container: HTMLElement,
		@IInstantiationService instantiationService: IInstantiationService
	) {
		this.statusBarContainer = DOM.append(container, $('.cell-statusbar-container'));
		const leftStatusBarItems = DOM.append(this.statusBarContainer, $('.cell-status-left'));
		const rightStatusBarItems = DOM.append(this.statusBarContainer, $('.cell-status-right'));
		this.cellRunStatusContainer = DOM.append(leftStatusBarItems, $('.cell-run-status'));
898
		this.durationContainer = DOM.append(leftStatusBarItems, $('.cell-run-duration'));
899 900 901 902 903
		this.cellStatusMessageContainer = DOM.append(leftStatusBarItems, $('.cell-status-message'));
		this.languageStatusBarItem = instantiationService.createInstance(CellLanguageStatusBarItem, rightStatusBarItems);
	}
}

904
export class CodeCellRenderer extends AbstractCellRenderer implements IListRenderer<CodeCellViewModel, CodeCellRenderTemplate> {
P
Peng Lyu 已提交
905
	static readonly TEMPLATE_ID = 'code_cell';
R
rebornix 已提交
906

P
Peng Lyu 已提交
907
	constructor(
R
rebornix 已提交
908
		protected notebookEditor: INotebookEditor,
R
rebornix 已提交
909
		private renderedEditors: Map<ICellViewModel, ICodeEditor | undefined>,
R
Rob Lourens 已提交
910
		dndController: CellDragAndDropController,
911
		contextKeyServiceProvider: (container?: HTMLElement) => IContextKeyService,
P
Peng Lyu 已提交
912 913
		@IContextMenuService contextMenuService: IContextMenuService,
		@IConfigurationService configurationService: IConfigurationService,
914
		@IInstantiationService instantiationService: IInstantiationService,
915 916
		@IKeybindingService keybindingService: IKeybindingService,
		@INotificationService notificationService: INotificationService,
P
Peng Lyu 已提交
917
	) {
918
		super(instantiationService, notebookEditor, contextMenuService, configurationService, keybindingService, notificationService, contextKeyServiceProvider, 'python', dndController);
P
Peng Lyu 已提交
919 920 921 922 923 924
	}

	get templateId() {
		return CodeCellRenderer.TEMPLATE_ID;
	}

925
	renderTemplate(container: HTMLElement): CodeCellRenderTemplate {
R
Rob Lourens 已提交
926
		container.classList.add('code-cell-row');
927
		const disposables = new DisposableStore();
R
Rob Lourens 已提交
928
		const contextKeyService = disposables.add(this.contextKeyServiceProvider(container));
929

R
Rob Lourens 已提交
930
		DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-top'));
R
Rob Lourens 已提交
931
		const toolbar = disposables.add(this.createToolbar(container));
932
		const focusIndicator = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-left'));
R
Rob Lourens 已提交
933
		focusIndicator.setAttribute('draggable', 'true');
934

R
Rob Lourens 已提交
935 936 937 938 939
		const cellContainer = DOM.append(container, $('.cell.code'));
		const runButtonContainer = DOM.append(cellContainer, $('.run-button-container'));
		const runToolbar = this.createToolbar(runButtonContainer);
		disposables.add(runToolbar);

940 941
		const executionOrderLabel = DOM.append(runButtonContainer, $('div.execution-count-label'));

942
		// create a special context key service that set the inCompositeEditor-contextkey
R
Rob Lourens 已提交
943
		const editorContextKeyService = disposables.add(this.contextKeyServiceProvider(container));
944 945 946
		const editorInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editorContextKeyService]));
		EditorContextKeys.inCompositeEditor.bindTo(editorContextKeyService).set(true);

R
Rob Lourens 已提交
947 948
		const editorPart = DOM.append(cellContainer, $('.cell-editor-part'));
		const editorContainer = DOM.append(editorPart, $('.cell-editor-container'));
949
		const editor = editorInstaService.createInstance(CodeEditorWidget, editorContainer, {
950
			...this.editorOptions.value,
P
Peng Lyu 已提交
951 952 953 954 955 956
			dimension: {
				width: 0,
				height: 0
			}
		}, {});

957 958
		disposables.add(this.editorOptions.onDidChange(newValue => editor.updateOptions(newValue)));

959
		const { collapsedPart, expandButton } = this.setupCollapsedPart(container);
R
Rob Lourens 已提交
960

R
Rob Lourens 已提交
961
		const progressBar = new ProgressBar(editorPart);
R
Rob Lourens 已提交
962 963 964
		progressBar.hide();
		disposables.add(progressBar);

965
		const statusBar = this.instantiationService.createInstance(CellEditorStatusBar, editorPart);
966
		const timer = new TimerRenderer(statusBar.durationContainer);
967
		const cellRunState = new RunStateRenderer(statusBar.cellRunStatusContainer, runToolbar, this.instantiationService);
R
Rob Lourens 已提交
968 969

		const outputContainer = DOM.append(container, $('.output'));
970 971 972 973

		const focusIndicatorRight = DOM.append(container, DOM.$('.cell-focus-indicator.cell-focus-indicator-side.cell-focus-indicator-right'));
		focusIndicatorRight.setAttribute('draggable', 'true');

974 975
		const focusSinkElement = DOM.append(container, $('.cell-editor-focus-sink'));
		focusSinkElement.setAttribute('tabindex', '0');
976
		const bottomCellContainer = DOM.append(container, $('.cell-bottom-toolbar-container'));
977
		const focusIndicatorBottom = DOM.append(container, $('.cell-focus-indicator.cell-focus-indicator-bottom'));
978
		const betweenCellToolbar = this.createBetweenCellToolbar(bottomCellContainer, disposables, contextKeyService);
979

980 981
		const titleMenu = disposables.add(this.cellMenus.getCellTitleMenu(contextKeyService));

R
Rob Lourens 已提交
982
		const templateData: CodeCellRenderTemplate = {
R
Rob Lourens 已提交
983 984
			editorPart,
			collapsedPart,
985
			expandButton,
986
			contextKeyService,
987 988
			container,
			cellContainer,
989
			statusBarContainer: statusBar.statusBarContainer,
990
			cellRunState,
991 992
			cellStatusMessageContainer: statusBar.cellStatusMessageContainer,
			languageStatusBarItem: statusBar.languageStatusBarItem,
R
Rob Lourens 已提交
993
			progressBar,
994
			focusIndicatorLeft: focusIndicator,
995 996
			focusIndicatorRight,
			focusIndicatorBottom,
997
			toolbar,
998
			betweenCellToolbar,
999
			focusSinkElement,
R
Rob Lourens 已提交
1000
			runToolbar,
R
rebornix 已提交
1001
			runButtonContainer,
1002
			executionOrderLabel,
1003 1004
			outputContainer,
			editor,
1005
			disposables,
1006 1007
			elementDisposables: new DisposableStore(),
			bottomCellContainer,
1008
			timer,
1009
			titleMenu,
R
rebornix 已提交
1010
			toJSON: () => { return {}; }
P
Peng Lyu 已提交
1011
		};
R
Rob Lourens 已提交
1012

1013
		this.dndController.registerDragHandle(templateData, () => new CodeCellDragImageRenderer().getDragImage(templateData, templateData.editor, 'code'));
1014

1015 1016
		disposables.add(DOM.addDisposableListener(focusSinkElement, DOM.EventType.FOCUS, () => {
			if (templateData.currentRenderedCell && (templateData.currentRenderedCell as CodeCellViewModel).outputs.length) {
1017 1018 1019 1020
				this.notebookEditor.focusNotebookCell(templateData.currentRenderedCell, 'output');
			}
		}));

R
Rob Lourens 已提交
1021 1022
		this.commonRenderTemplate(templateData);

R
Rob Lourens 已提交
1023
		return templateData;
P
Peng Lyu 已提交
1024 1025
	}

1026 1027 1028 1029 1030 1031 1032 1033
	private updateForOutputs(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void {
		if (element.outputs.length) {
			DOM.show(templateData.focusSinkElement);
		} else {
			DOM.hide(templateData.focusSinkElement);
		}
	}

1034
	private updateForMetadata(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void {
R
Rob Lourens 已提交
1035 1036
		const metadata = element.getEvaluatedMetadata(this.notebookEditor.viewModel!.notebookDocument.metadata);
		DOM.toggleClass(templateData.cellContainer, 'runnable', !!metadata.runnable);
1037
		this.updateExecutionOrder(metadata, templateData);
R
Rob Lourens 已提交
1038 1039
		templateData.cellStatusMessageContainer.textContent = metadata?.statusMessage || '';

1040
		templateData.cellRunState.renderState(element.metadata?.runState);
1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052

		if (metadata.runState === NotebookCellRunState.Running) {
			if (metadata.runStartTime) {
				templateData.elementDisposables.add(templateData.timer.start(metadata.runStartTime));
			} else {
				templateData.timer.clear();
			}
		} else if (typeof metadata.lastRunDuration === 'number') {
			templateData.timer.show(metadata.lastRunDuration);
		} else {
			templateData.timer.clear();
		}
1053 1054 1055 1056

		if (typeof metadata.breakpointMargin === 'boolean') {
			this.editorOptions.setGlyphMargin(metadata.breakpointMargin);
		}
1057

1058 1059 1060 1061 1062
		if (metadata.runState === NotebookCellRunState.Running) {
			templateData.progressBar.infinite().show(500);
		} else {
			templateData.progressBar.hide();
		}
R
Rob Lourens 已提交
1063 1064
	}

1065 1066 1067 1068
	private updateExecutionOrder(metadata: NotebookCellMetadata, templateData: CodeCellRenderTemplate): void {
		if (metadata.hasExecutionOrder) {
			const executionOrderLabel = typeof metadata.executionOrder === 'number' ?
				`[${metadata.executionOrder}]` :
1069
				'[ ]';
1070
			templateData.executionOrderLabel.innerText = executionOrderLabel;
R
Rob Lourens 已提交
1071 1072 1073 1074 1075
		} else {
			templateData.executionOrderLabel.innerText = '';
		}
	}

1076 1077 1078 1079
	private updateForHover(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void {
		DOM.toggleClass(templateData.container, 'cell-output-hover', element.outputIsHovered);
	}

1080
	private updateForLayout(element: CodeCellViewModel, templateData: CodeCellRenderTemplate): void {
1081
		templateData.focusIndicatorLeft.style.height = `${element.layoutInfo.indicatorHeight}px`;
1082 1083 1084 1085 1086
		templateData.focusIndicatorRight.style.height = `${element.layoutInfo.indicatorHeight}px`;
		templateData.focusIndicatorBottom.style.top = `${element.layoutInfo.totalHeight - BOTTOM_CELL_TOOLBAR_HEIGHT - CELL_BOTTOM_MARGIN}px`;
		templateData.outputContainer.style.top = `${element.layoutInfo.outputContainerOffset}px`;
	}

1087
	renderElement(element: CodeCellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void {
R
rebornix 已提交
1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098
		const removedClassNames: string[] = [];
		templateData.container.classList.forEach(className => {
			if (/^nb\-.*$/.test(className)) {
				removedClassNames.push(className);
			}
		});

		removedClassNames.forEach(className => {
			templateData.container.classList.remove(className);
		});

1099 1100
		this.commonRenderElement(element, index, templateData);

R
Rob Lourens 已提交
1101 1102
		templateData.currentRenderedCell = element;

R
rebornix 已提交
1103 1104 1105 1106
		if (height === undefined) {
			return;
		}

1107
		templateData.outputContainer.innerHTML = '';
1108

1109
		const elementDisposables = templateData.elementDisposables;
R
rebornix 已提交
1110

1111
		elementDisposables.add(this.instantiationService.createInstance(CodeCell, this.notebookEditor, element, templateData));
J
Johannes Rieken 已提交
1112
		this.renderedEditors.set(element, templateData.editor);
1113

1114 1115
		elementDisposables.add(new CellContextKeyManager(templateData.contextKeyService, this.notebookEditor.viewModel?.notebookDocument!, element));

1116
		this.updateForLayout(element, templateData);
1117
		elementDisposables.add(element.onDidChangeLayout(() => {
1118
			this.updateForLayout(element, templateData);
1119 1120
		}));

1121
		templateData.cellRunState.clear();
1122
		this.updateForMetadata(element, templateData);
1123
		this.updateForHover(element, templateData);
1124
		elementDisposables.add(element.onDidChangeState((e) => {
1125
			if (e.metadataChanged) {
1126
				this.updateForMetadata(element, templateData);
1127
			}
1128 1129 1130 1131

			if (e.outputIsHoveredChanged) {
				this.updateForHover(element, templateData);
			}
1132
		}));
1133

1134 1135 1136
		this.updateForOutputs(element, templateData);
		elementDisposables.add(element.onDidChangeOutputs(_e => this.updateForOutputs(element, templateData)));

1137
		this.setupCellToolbarActions(templateData, elementDisposables);
1138 1139 1140 1141 1142 1143 1144

		const toolbarContext = <INotebookCellActionContext>{
			cell: element,
			cellTemplate: templateData,
			notebookEditor: this.notebookEditor,
			$mid: 12
		};
1145
		templateData.toolbar.context = toolbarContext;
1146
		templateData.runToolbar.context = toolbarContext;
R
rebornix 已提交
1147

1148
		this.setBetweenCellToolbarContext(templateData, element, toolbarContext);
1149 1150

		templateData.languageStatusBarItem.update(element, this.notebookEditor);
R
rebornix 已提交
1151 1152
	}

1153
	disposeTemplate(templateData: CodeCellRenderTemplate): void {
1154
		templateData.disposables.clear();
P
Peng Lyu 已提交
1155 1156
	}

1157
	disposeElement(element: ICellViewModel, index: number, templateData: CodeCellRenderTemplate, height: number | undefined): void {
1158
		templateData.elementDisposables.clear();
J
Johannes Rieken 已提交
1159
		this.renderedEditors.delete(element);
P
Peng Lyu 已提交
1160 1161
	}
}
1162 1163 1164 1165 1166 1167

export class TimerRenderer {
	constructor(private readonly container: HTMLElement) {
		DOM.hide(container);
	}

R
Rob Lourens 已提交
1168
	private intervalTimer: number | undefined;
1169 1170 1171 1172 1173 1174 1175 1176 1177

	start(startTime: number): IDisposable {
		this.stop();

		DOM.show(this.container);
		const intervalTimer = setInterval(() => {
			const duration = Date.now() - startTime;
			this.container.textContent = this.formatDuration(duration);
		}, 100);
R
rebornix 已提交
1178
		this.intervalTimer = intervalTimer as unknown as number | undefined;
1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210

		return toDisposable(() => {
			clearInterval(intervalTimer);
		});
	}

	stop() {
		if (this.intervalTimer) {
			clearInterval(this.intervalTimer);
		}
	}

	show(duration: number) {
		this.stop();

		DOM.show(this.container);
		this.container.textContent = this.formatDuration(duration);
	}

	clear() {
		DOM.hide(this.container);
		this.stop();
		this.container.textContent = '';
	}

	private formatDuration(duration: number) {
		const seconds = Math.floor(duration / 1000);
		const tenths = String(duration - seconds * 1000).charAt(0);

		return `${seconds}.${tenths}s`;
	}
}
1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257

export class RunStateRenderer {
	private static readonly MIN_SPINNER_TIME = 200;

	private spinnerTimer: NodeJS.Timeout | undefined;
	private pendingNewState: NotebookCellRunState | undefined;

	constructor(private readonly element: HTMLElement, private readonly runToolbar: ToolBar, private readonly instantiationService: IInstantiationService) {
	}

	clear() {
		if (this.spinnerTimer) {
			clearTimeout(this.spinnerTimer);
		}
	}

	renderState(runState: NotebookCellRunState = NotebookCellRunState.Idle) {
		if (this.spinnerTimer) {
			this.pendingNewState = runState;
			return;
		}

		if (runState === NotebookCellRunState.Running) {
			this.runToolbar.setActions([this.instantiationService.createInstance(CancelCellAction)]);
		} else {
			this.runToolbar.setActions([this.instantiationService.createInstance(ExecuteCellAction)]);
		}

		if (runState === NotebookCellRunState.Success) {
			this.element.innerHTML = renderCodicons('$(check)');
		} else if (runState === NotebookCellRunState.Error) {
			this.element.innerHTML = renderCodicons('$(error)');
		} else if (runState === NotebookCellRunState.Running) {
			this.element.innerHTML = renderCodicons('$(sync~spin)');

			this.spinnerTimer = setTimeout(() => {
				this.spinnerTimer = undefined;
				if (this.pendingNewState) {
					this.renderState(this.pendingNewState);
					this.pendingNewState = undefined;
				}
			}, RunStateRenderer.MIN_SPINNER_TIME);
		} else {
			this.element.innerHTML = '';
		}
	}
}