async function webviewPreloads()

in patched-vscode/src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts [91:1007]


async function webviewPreloads(ctx: PreloadContext) {

	/* eslint-disable no-restricted-globals, no-restricted-syntax */

	// The use of global `window` should be fine in this context, even
	// with aux windows. This code is running from within an `iframe`
	// where there is only one `window` object anyway.

	const userAgent = navigator.userAgent;
	const isChrome = (userAgent.indexOf('Chrome') >= 0);
	const textEncoder = new TextEncoder();
	const textDecoder = new TextDecoder();

	function promiseWithResolvers<T>(): { promise: Promise<T>; resolve: (value: T | PromiseLike<T>) => void; reject: (err?: any) => void } {
		let resolve: (value: T | PromiseLike<T>) => void;
		let reject: (reason?: any) => void;
		const promise = new Promise<T>((res, rej) => {
			resolve = res;
			reject = rej;
		});
		return { promise, resolve: resolve!, reject: reject! };
	}

	let currentOptions = ctx.options;
	const isWorkspaceTrusted = ctx.isWorkspaceTrusted;
	let currentRenderOptions = ctx.renderOptions;
	const settingChange: EmitterLike<RenderOptions> = createEmitter<RenderOptions>();

	const acquireVsCodeApi = globalThis.acquireVsCodeApi;
	const vscode = acquireVsCodeApi();
	delete (globalThis as any).acquireVsCodeApi;

	const tokenizationStyle = new CSSStyleSheet();
	tokenizationStyle.replaceSync(ctx.style.tokenizationCss);

	const runWhenIdle: (callback: (idle: IdleDeadline) => void, timeout?: number) => IDisposable = (typeof requestIdleCallback !== 'function' || typeof cancelIdleCallback !== 'function')
		? (runner) => {
			setTimeout(() => {
				if (disposed) {
					return;
				}
				const end = Date.now() + 15; // one frame at 64fps
				runner(Object.freeze({
					didTimeout: true,
					timeRemaining() {
						return Math.max(0, end - Date.now());
					}
				}));
			});
			let disposed = false;
			return {
				dispose() {
					if (disposed) {
						return;
					}
					disposed = true;
				}
			};
		}
		: (runner, timeout?) => {
			const handle: number = requestIdleCallback(runner, typeof timeout === 'number' ? { timeout } : undefined);
			let disposed = false;
			return {
				dispose() {
					if (disposed) {
						return;
					}
					disposed = true;
					cancelIdleCallback(handle);
				}
			};
		};
	function getOutputContainer(event: FocusEvent | MouseEvent) {
		for (const node of event.composedPath()) {
			if (node instanceof HTMLElement && node.classList.contains('output')) {
				return {
					id: node.id
				};
			}
		}
		return;
	}
	let lastFocusedOutput: { id: string } | undefined = undefined;
	const handleOutputFocusOut = (event: FocusEvent) => {
		const outputFocus = event && getOutputContainer(event);
		if (!outputFocus) {
			return;
		}
		// Possible we're tabbing through the elements of the same output.
		// Lets see if focus is set back to the same output.
		lastFocusedOutput = undefined;
		setTimeout(() => {
			if (lastFocusedOutput?.id === outputFocus.id) {
				return;
			}
			postNotebookMessage<webviewMessages.IOutputBlurMessage>('outputBlur', outputFocus);
		}, 0);
	};

	// check if an input element is focused within the output element
	const checkOutputInputFocus = (e: FocusEvent) => {
		lastFocusedOutput = getOutputContainer(e);
		const activeElement = window.document.activeElement;
		if (!activeElement) {
			return;
		}

		const id = lastFocusedOutput?.id;
		if (id && (activeElement.tagName === 'INPUT' || activeElement.tagName === 'TEXTAREA' || activeElement.tagName === 'SELECT')) {
			postNotebookMessage<webviewMessages.IOutputInputFocusMessage>('outputInputFocus', { inputFocused: true, id });

			activeElement.addEventListener('blur', () => {
				postNotebookMessage<webviewMessages.IOutputInputFocusMessage>('outputInputFocus', { inputFocused: false, id });
			}, { once: true });
		}
	};

	const handleInnerClick = (event: MouseEvent) => {
		if (!event || !event.view || !event.view.document) {
			return;
		}

		const outputFocus = lastFocusedOutput = getOutputContainer(event);
		for (const node of event.composedPath()) {
			if (node instanceof HTMLAnchorElement && node.href) {
				if (node.href.startsWith('blob:')) {
					if (outputFocus) {
						postNotebookMessage<webviewMessages.IOutputFocusMessage>('outputFocus', outputFocus);
					}

					handleBlobUrlClick(node.href, node.download);
				} else if (node.href.startsWith('data:')) {
					if (outputFocus) {
						postNotebookMessage<webviewMessages.IOutputFocusMessage>('outputFocus', outputFocus);
					}
					handleDataUrl(node.href, node.download);
				} else if (node.getAttribute('href')?.trim().startsWith('#')) {
					// Scrolling to location within current doc

					if (!node.hash) {
						postNotebookMessage<webviewMessages.IScrollToRevealMessage>('scroll-to-reveal', { scrollTop: 0 });
						return;
					}

					const targetId = node.hash.substring(1);

					// Check outer document first
					let scrollTarget: Element | null | undefined = event.view.document.getElementById(targetId);

					if (!scrollTarget) {
						// Fallback to checking preview shadow doms
						for (const preview of event.view.document.querySelectorAll('.preview')) {
							scrollTarget = preview.shadowRoot?.getElementById(targetId);
							if (scrollTarget) {
								break;
							}
						}
					}

					if (scrollTarget) {
						const scrollTop = scrollTarget.getBoundingClientRect().top + event.view.scrollY;
						postNotebookMessage<webviewMessages.IScrollToRevealMessage>('scroll-to-reveal', { scrollTop });
						return;
					}
				} else {
					const href = node.getAttribute('href');
					if (href) {
						if (href.startsWith('command:') && outputFocus) {
							postNotebookMessage<webviewMessages.IOutputFocusMessage>('outputFocus', outputFocus);
						}
						postNotebookMessage<webviewMessages.IClickedLinkMessage>('clicked-link', { href });
					}
				}

				event.preventDefault();
				event.stopPropagation();
				return;
			}
		}

		if (outputFocus) {
			postNotebookMessage<webviewMessages.IOutputFocusMessage>('outputFocus', outputFocus);
		}
	};

	const blurOutput = () => {
		const selection = window.getSelection();
		if (!selection) {
			return;
		}
		selection.removeAllRanges();
	};

	const selectOutputContents = (cellOrOutputId: string) => {
		const selection = window.getSelection();
		if (!selection) {
			return;
		}
		const cellOutputContainer = window.document.getElementById(cellOrOutputId);
		if (!cellOutputContainer) {
			return;
		}
		selection.removeAllRanges();
		const range = document.createRange();
		range.selectNode(cellOutputContainer);
		selection.addRange(range);

	};

	const selectInputContents = (cellOrOutputId: string) => {
		const cellOutputContainer = window.document.getElementById(cellOrOutputId);
		if (!cellOutputContainer) {
			return;
		}
		const activeElement = window.document.activeElement;
		if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') {
			(activeElement as HTMLInputElement).select();
		}
	};

	const onPageUpDownSelectionHandler = (e: KeyboardEvent) => {
		if (!lastFocusedOutput?.id || !e.shiftKey) {
			return;
		}

		// If we're pressing `Shift+Up/Down` then we want to select a line at a time.
		if (e.shiftKey && (e.code === 'ArrowUp' || e.code === 'ArrowDown')) {
			e.stopPropagation(); // We don't want the notebook to handle this, default behavior is what we need.
			return;
		}

		// We want to handle just `Shift + PageUp/PageDown` & `Shift + Cmd + ArrowUp/ArrowDown` (for mac)
		if (!(e.code === 'PageUp' || e.code === 'PageDown') && !(e.metaKey && (e.code === 'ArrowDown' || e.code === 'ArrowUp'))) {
			return;
		}
		const outputContainer = window.document.getElementById(lastFocusedOutput.id);
		const selection = window.getSelection();
		if (!outputContainer || !selection?.anchorNode) {
			return;
		}
		const activeElement = window.document.activeElement;
		if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') {
			// Leave for default behavior.
			return;
		}

		// These should change the scroll position, not adjust the selected cell in the notebook
		e.stopPropagation(); // We don't want the notebook to handle this.
		e.preventDefault(); // We will handle selection.

		const { anchorNode, anchorOffset } = selection;
		const range = document.createRange();
		if (e.code === 'PageDown' || e.code === 'ArrowDown') {
			range.setStart(anchorNode, anchorOffset);
			range.setEnd(outputContainer, 1);
		}
		else {
			range.setStart(outputContainer, 0);
			range.setEnd(anchorNode, anchorOffset);
		}
		selection.removeAllRanges();
		selection.addRange(range);
	};

	const disableNativeSelectAll = (e: KeyboardEvent) => {
		if (!lastFocusedOutput?.id) {
			return;
		}
		const activeElement = window.document.activeElement;
		if (activeElement?.tagName === 'INPUT' || activeElement?.tagName === 'TEXTAREA') {
			// The input element will handle this.
			return;
		}

		if ((e.key === 'a' && e.ctrlKey) || (e.metaKey && e.key === 'a')) {
			e.preventDefault(); // We will handle selection in editor code.
			return;
		}
	};

	const handleDataUrl = async (data: string | ArrayBuffer | null, downloadName: string) => {
		postNotebookMessage<webviewMessages.IClickedDataUrlMessage>('clicked-data-url', {
			data,
			downloadName
		});
	};

	const handleBlobUrlClick = async (url: string, downloadName: string) => {
		try {
			const response = await fetch(url);
			const blob = await response.blob();
			const reader = new FileReader();
			reader.addEventListener('load', () => {
				handleDataUrl(reader.result, downloadName);
			});
			reader.readAsDataURL(blob);
		} catch (e) {
			console.error(e.message);
		}
	};

	window.document.body.addEventListener('click', handleInnerClick);
	window.document.body.addEventListener('focusin', checkOutputInputFocus);
	window.document.body.addEventListener('focusout', handleOutputFocusOut);
	window.document.body.addEventListener('keydown', onPageUpDownSelectionHandler);
	window.document.body.addEventListener('keydown', disableNativeSelectAll);

	interface RendererContext extends rendererApi.RendererContext<unknown> {
		readonly onDidChangeSettings: Event<RenderOptions>;
		readonly settings: RenderOptions;
	}

	interface RendererModule {
		readonly activate: rendererApi.ActivationFunction;
	}

	interface KernelPreloadContext {
		readonly onDidReceiveKernelMessage: Event<unknown>;
		postKernelMessage(data: unknown): void;
	}

	interface KernelPreloadModule {
		activate(ctx: KernelPreloadContext): Promise<void> | void;
	}

	interface IObservedElement {
		id: string;
		output: boolean;
		lastKnownPadding: number;
		lastKnownHeight: number;
		cellId: string;
	}

	function createKernelContext(): KernelPreloadContext {
		return Object.freeze({
			onDidReceiveKernelMessage: onDidReceiveKernelMessage.event,
			postKernelMessage: (data: unknown) => postNotebookMessage('customKernelMessage', { message: data }),
		});
	}

	async function runKernelPreload(url: string): Promise<void> {
		try {
			return await activateModuleKernelPreload(url);
		} catch (e) {
			console.error(e);
			throw e;
		}
	}

	async function activateModuleKernelPreload(url: string) {
		const module: KernelPreloadModule = await __import(url);
		if (!module.activate) {
			console.error(`Notebook preload '${url}' was expected to be a module but it does not export an 'activate' function`);
			return;
		}
		return module.activate(createKernelContext());
	}

	const dimensionUpdater = new class {
		private readonly pending = new Map<string, webviewMessages.DimensionUpdate>();

		updateHeight(id: string, height: number, options: { init?: boolean; isOutput?: boolean }) {
			if (!this.pending.size) {
				setTimeout(() => {
					this.updateImmediately();
				}, 0);
			}
			const update = this.pending.get(id);
			if (update && update.isOutput) {
				this.pending.set(id, {
					id,
					height,
					init: update.init,
					isOutput: update.isOutput,
				});
			} else {
				this.pending.set(id, {
					id,
					height,
					...options,
				});
			}
		}

		updateImmediately() {
			if (!this.pending.size) {
				return;
			}

			postNotebookMessage<webviewMessages.IDimensionMessage>('dimension', {
				updates: Array.from(this.pending.values())
			});
			this.pending.clear();
		}
	};

	const resizeObserver = new class {

		private readonly _observer: ResizeObserver;

		private readonly _observedElements = new WeakMap<Element, IObservedElement>();
		private _outputResizeTimer: any;

		constructor() {
			this._observer = new ResizeObserver(entries => {
				for (const entry of entries) {
					if (!window.document.body.contains(entry.target)) {
						continue;
					}

					const observedElementInfo = this._observedElements.get(entry.target);
					if (!observedElementInfo) {
						continue;
					}

					this.postResizeMessage(observedElementInfo.cellId);

					if (entry.target.id !== observedElementInfo.id) {
						continue;
					}

					if (!entry.contentRect) {
						continue;
					}

					if (!observedElementInfo.output) {
						// markup, update directly
						this.updateHeight(observedElementInfo, entry.target.offsetHeight);
						continue;
					}

					const newHeight = entry.contentRect.height;
					const shouldUpdatePadding =
						(newHeight !== 0 && observedElementInfo.lastKnownPadding === 0) ||
						(newHeight === 0 && observedElementInfo.lastKnownPadding !== 0);

					if (shouldUpdatePadding) {
						// Do not update dimension in resize observer
						window.requestAnimationFrame(() => {
							if (newHeight !== 0) {
								entry.target.style.padding = `${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodePadding}px ${ctx.style.outputNodeLeftPadding}px`;
							} else {
								entry.target.style.padding = `0px`;
							}
							this.updateHeight(observedElementInfo, entry.target.offsetHeight);
						});
					} else {
						this.updateHeight(observedElementInfo, entry.target.offsetHeight);
					}
				}
			});
		}

		private updateHeight(observedElementInfo: IObservedElement, offsetHeight: number) {
			if (observedElementInfo.lastKnownHeight !== offsetHeight) {
				observedElementInfo.lastKnownHeight = offsetHeight;
				dimensionUpdater.updateHeight(observedElementInfo.id, offsetHeight, {
					isOutput: observedElementInfo.output
				});
			}
		}

		public observe(container: Element, id: string, output: boolean, cellId: string) {
			if (this._observedElements.has(container)) {
				return;
			}

			this._observedElements.set(container, { id, output, lastKnownPadding: ctx.style.outputNodePadding, lastKnownHeight: -1, cellId });
			this._observer.observe(container);
		}

		private postResizeMessage(cellId: string) {
			// Debounce this callback to only happen after
			// 250 ms. Don't need resize events that often.
			clearTimeout(this._outputResizeTimer);
			this._outputResizeTimer = setTimeout(() => {
				postNotebookMessage('outputResized', {
					cellId
				});
			}, 250);

		}
	};

	let previousDelta: number | undefined;
	let scrollTimeout: any /* NodeJS.Timeout */ | undefined;
	let scrolledElement: Element | undefined;
	let lastTimeScrolled: number | undefined;
	function flagRecentlyScrolled(node: Element, deltaY?: number) {
		scrolledElement = node;
		if (deltaY === undefined) {
			lastTimeScrolled = Date.now();
			previousDelta = undefined;
			node.setAttribute('recentlyScrolled', 'true');
			clearTimeout(scrollTimeout);
			scrollTimeout = setTimeout(() => { scrolledElement?.removeAttribute('recentlyScrolled'); }, 300);
			return true;
		}

		if (node.hasAttribute('recentlyScrolled')) {
			if (lastTimeScrolled && Date.now() - lastTimeScrolled > 400) {
				// it has been a while since we actually scrolled
				// if scroll velocity increases significantly, it's likely a new scroll event
				if (!!previousDelta && deltaY < 0 && deltaY < previousDelta - 8) {
					clearTimeout(scrollTimeout);
					scrolledElement?.removeAttribute('recentlyScrolled');
					return false;
				} else if (!!previousDelta && deltaY > 0 && deltaY > previousDelta + 8) {
					clearTimeout(scrollTimeout);
					scrolledElement?.removeAttribute('recentlyScrolled');
					return false;
				}

				// the tail end of a smooth scrolling event (from a trackpad) can go on for a while
				// so keep swallowing it, but we can shorten the timeout since the events occur rapidly
				clearTimeout(scrollTimeout);
				scrollTimeout = setTimeout(() => { scrolledElement?.removeAttribute('recentlyScrolled'); }, 50);
			} else {
				clearTimeout(scrollTimeout);
				scrollTimeout = setTimeout(() => { scrolledElement?.removeAttribute('recentlyScrolled'); }, 300);
			}

			previousDelta = deltaY;
			return true;
		}

		return false;
	}

	function eventTargetShouldHandleScroll(event: WheelEvent) {
		for (let node = event.target as Node | null; node; node = node.parentNode) {
			if (!(node instanceof Element) || node.id === 'container' || node.classList.contains('cell_container') || node.classList.contains('markup') || node.classList.contains('output_container')) {
				return false;
			}

			// scroll up
			if (event.deltaY < 0 && node.scrollTop > 0) {
				// there is still some content to scroll
				flagRecentlyScrolled(node);
				return true;
			}

			// scroll down
			if (event.deltaY > 0 && node.scrollTop + node.clientHeight < node.scrollHeight) {
				// per https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight
				// scrollTop is not rounded but scrollHeight and clientHeight are
				// so we need to check if the difference is less than some threshold
				if (node.scrollHeight - node.scrollTop - node.clientHeight < 2) {
					continue;
				}

				// if the node is not scrollable, we can continue. We don't check the computed style always as it's expensive
				if (window.getComputedStyle(node).overflowY === 'hidden' || window.getComputedStyle(node).overflowY === 'visible') {
					continue;
				}

				flagRecentlyScrolled(node);
				return true;
			}

			if (flagRecentlyScrolled(node, event.deltaY)) {
				return true;
			}
		}

		return false;
	}

	const handleWheel = (event: WheelEvent & { wheelDeltaX?: number; wheelDeltaY?: number; wheelDelta?: number }) => {
		if (event.defaultPrevented || eventTargetShouldHandleScroll(event)) {
			return;
		}
		postNotebookMessage<webviewMessages.IWheelMessage>('did-scroll-wheel', {
			payload: {
				deltaMode: event.deltaMode,
				deltaX: event.deltaX,
				deltaY: event.deltaY,
				deltaZ: event.deltaZ,
				// Refs https://github.com/microsoft/vscode/issues/146403#issuecomment-1854538928
				wheelDelta: event.wheelDelta && isChrome ? (event.wheelDelta / window.devicePixelRatio) : event.wheelDelta,
				wheelDeltaX: event.wheelDeltaX && isChrome ? (event.wheelDeltaX / window.devicePixelRatio) : event.wheelDeltaX,
				wheelDeltaY: event.wheelDeltaY && isChrome ? (event.wheelDeltaY / window.devicePixelRatio) : event.wheelDeltaY,
				detail: event.detail,
				shiftKey: event.shiftKey,
				type: event.type
			}
		});
	};

	function focusFirstFocusableOrContainerInOutput(cellOrOutputId: string, alternateId?: string) {
		const cellOutputContainer = window.document.getElementById(cellOrOutputId) ??
			(alternateId ? window.document.getElementById(alternateId) : undefined);
		if (cellOutputContainer) {
			if (cellOutputContainer.contains(window.document.activeElement)) {
				return;
			}
			const id = cellOutputContainer.id;
			let focusableElement = cellOutputContainer.querySelector('[tabindex="0"], [href], button, input, option, select, textarea') as HTMLElement | null;
			if (!focusableElement) {
				focusableElement = cellOutputContainer;
				focusableElement.tabIndex = -1;
				postNotebookMessage<webviewMessages.IOutputInputFocusMessage>('outputInputFocus', { inputFocused: false, id });
			} else {
				const inputFocused = focusableElement.tagName === 'INPUT' || focusableElement.tagName === 'TEXTAREA';
				postNotebookMessage<webviewMessages.IOutputInputFocusMessage>('outputInputFocus', { inputFocused, id });
			}

			lastFocusedOutput = cellOutputContainer;
			postNotebookMessage<webviewMessages.IOutputFocusMessage>('outputFocus', { id: cellOutputContainer.id });
			focusableElement.focus();
		}
	}

	function createFocusSink(cellId: string, focusNext?: boolean) {
		const element = document.createElement('div');
		element.id = `focus-sink-${cellId}`;
		element.tabIndex = 0;
		element.addEventListener('focus', () => {
			postNotebookMessage<webviewMessages.IFocusEditorMessage>('focus-editor', {
				cellId: cellId,
				focusNext
			});
		});

		return element;
	}

	function _internalHighlightRange(range: Range, tagName = 'mark', attributes = {}) {
		// derived from https://github.com/Treora/dom-highlight-range/blob/master/highlight-range.js

		// Return an array of the text nodes in the range. Split the start and end nodes if required.
		function _textNodesInRange(range: Range): Text[] {
			if (!range.startContainer.ownerDocument) {
				return [];
			}

			// If the start or end node is a text node and only partly in the range, split it.
			if (range.startContainer.nodeType === Node.TEXT_NODE && range.startOffset > 0) {
				const startContainer = range.startContainer as Text;
				const endOffset = range.endOffset; // (this may get lost when the splitting the node)
				const createdNode = startContainer.splitText(range.startOffset);
				if (range.endContainer === startContainer) {
					// If the end was in the same container, it will now be in the newly created node.
					range.setEnd(createdNode, endOffset - range.startOffset);
				}

				range.setStart(createdNode, 0);
			}

			if (
				range.endContainer.nodeType === Node.TEXT_NODE
				&& range.endOffset < (range.endContainer as Text).length
			) {
				(range.endContainer as Text).splitText(range.endOffset);
			}

			// Collect the text nodes.
			const walker = range.startContainer.ownerDocument.createTreeWalker(
				range.commonAncestorContainer,
				NodeFilter.SHOW_TEXT,
				node => range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT,
			);

			walker.currentNode = range.startContainer;

			// // Optimise by skipping nodes that are explicitly outside the range.
			// const NodeTypesWithCharacterOffset = [
			//  Node.TEXT_NODE,
			//  Node.PROCESSING_INSTRUCTION_NODE,
			//  Node.COMMENT_NODE,
			// ];
			// if (!NodeTypesWithCharacterOffset.includes(range.startContainer.nodeType)) {
			//   if (range.startOffset < range.startContainer.childNodes.length) {
			//     walker.currentNode = range.startContainer.childNodes[range.startOffset];
			//   } else {
			//     walker.nextSibling(); // TODO verify this is correct.
			//   }
			// }

			const nodes: Text[] = [];
			if (walker.currentNode.nodeType === Node.TEXT_NODE) {
				nodes.push(walker.currentNode as Text);
			}

			while (walker.nextNode() && range.comparePoint(walker.currentNode, 0) !== 1) {
				if (walker.currentNode.nodeType === Node.TEXT_NODE) {
					nodes.push(walker.currentNode as Text);
				}
			}

			return nodes;
		}

		// Replace [node] with <tagName ...attributes>[node]</tagName>
		function wrapNodeInHighlight(node: Text, tagName: string, attributes: any) {
			const highlightElement = node.ownerDocument.createElement(tagName);
			Object.keys(attributes).forEach(key => {
				highlightElement.setAttribute(key, attributes[key]);
			});
			const tempRange = node.ownerDocument.createRange();
			tempRange.selectNode(node);
			tempRange.surroundContents(highlightElement);
			return highlightElement;
		}

		if (range.collapsed) {
			return {
				remove: () => { },
				update: () => { }
			};
		}

		// First put all nodes in an array (splits start and end nodes if needed)
		const nodes = _textNodesInRange(range);

		// Highlight each node
		const highlightElements: Element[] = [];
		for (const nodeIdx in nodes) {
			const highlightElement = wrapNodeInHighlight(nodes[nodeIdx], tagName, attributes);
			highlightElements.push(highlightElement);
		}

		// Remove a highlight element created with wrapNodeInHighlight.
		function _removeHighlight(highlightElement: Element) {
			if (highlightElement.childNodes.length === 1) {
				highlightElement.parentNode?.replaceChild(highlightElement.firstChild!, highlightElement);
			} else {
				// If the highlight somehow contains multiple nodes now, move them all.
				while (highlightElement.firstChild) {
					highlightElement.parentNode?.insertBefore(highlightElement.firstChild, highlightElement);
				}
				highlightElement.remove();
			}
		}

		// Return a function that cleans up the highlightElements.
		function _removeHighlights() {
			// Remove each of the created highlightElements.
			for (const highlightIdx in highlightElements) {
				_removeHighlight(highlightElements[highlightIdx]);
			}
		}

		function _updateHighlight(highlightElement: Element, attributes: any = {}) {
			Object.keys(attributes).forEach(key => {
				highlightElement.setAttribute(key, attributes[key]);
			});
		}

		function updateHighlights(attributes: any) {
			for (const highlightIdx in highlightElements) {
				_updateHighlight(highlightElements[highlightIdx], attributes);
			}
		}

		return {
			remove: _removeHighlights,
			update: updateHighlights
		};
	}

	interface ICommonRange {
		collapsed: boolean;
		commonAncestorContainer: Node;
		endContainer: Node;
		endOffset: number;
		startContainer: Node;
		startOffset: number;

	}

	interface IHighlightResult {
		range: ICommonRange;
		dispose: () => void;
		update: (color: string | undefined, className: string | undefined) => void;
	}

	function selectRange(_range: ICommonRange) {
		const sel = window.getSelection();
		if (sel) {
			try {
				sel.removeAllRanges();
				const r = document.createRange();
				r.setStart(_range.startContainer, _range.startOffset);
				r.setEnd(_range.endContainer, _range.endOffset);
				sel.addRange(r);
			} catch (e) {
				console.log(e);
			}
		}
	}

	function highlightRange(range: Range, useCustom: boolean, tagName = 'mark', attributes = {}): IHighlightResult {
		if (useCustom) {
			const ret = _internalHighlightRange(range, tagName, attributes);
			return {
				range: range,
				dispose: ret.remove,
				update: (color: string | undefined, className: string | undefined) => {
					if (className === undefined) {
						ret.update({
							'style': `background-color: ${color}`
						});
					} else {
						ret.update({
							'class': className
						});
					}
				}
			};
		} else {
			window.document.execCommand('hiliteColor', false, matchColor);
			const cloneRange = window.getSelection()!.getRangeAt(0).cloneRange();
			const _range = {
				collapsed: cloneRange.collapsed,
				commonAncestorContainer: cloneRange.commonAncestorContainer,
				endContainer: cloneRange.endContainer,
				endOffset: cloneRange.endOffset,
				startContainer: cloneRange.startContainer,
				startOffset: cloneRange.startOffset
			};
			return {
				range: _range,
				dispose: () => {
					selectRange(_range);
					try {
						document.designMode = 'On';
						window.document.execCommand('removeFormat', false, undefined);
						document.designMode = 'Off';
						window.getSelection()?.removeAllRanges();
					} catch (e) {
						console.log(e);
					}
				},
				update: (color: string | undefined, className: string | undefined) => {
					selectRange(_range);
					try {
						document.designMode = 'On';
						window.document.execCommand('removeFormat', false, undefined);
						window.document.execCommand('hiliteColor', false, color);
						document.designMode = 'Off';
						window.getSelection()?.removeAllRanges();
					} catch (e) {
						console.log(e);
					}
				}
			};
		}
	}

	function createEmitter<T>(listenerChange: (listeners: Set<Listener<T>>) => void = () => undefined): EmitterLike<T> {
		const listeners = new Set<Listener<T>>();
		return {
			fire(data) {
				for (const listener of [...listeners]) {
					listener.fn.call(listener.thisArg, data);
				}
			},
			event(fn, thisArg, disposables) {
				const listenerObj = { fn, thisArg };
				const disposable: IDisposable = {
					dispose: () => {
						listeners.delete(listenerObj);
						listenerChange(listeners);
					},
				};

				listeners.add(listenerObj);
				listenerChange(listeners);

				if (disposables instanceof Array) {
					disposables.push(disposable);
				} else if (disposables) {
					disposables.add(disposable);
				}

				return disposable;
			},
		};
	}

	function showRenderError(errorText: string, outputNode: HTMLElement, errors: readonly Error[]) {
		outputNode.innerText = errorText;
		const errList = document.createElement('ul');
		for (const result of errors) {
			console.error(result);
			const item = document.createElement('li');
			item.innerText = result.message;
			errList.appendChild(item);
		}
		outputNode.appendChild(errList);
	}

	const outputItemRequests = new class {
		private _requestPool = 0;
		private readonly _requests = new Map</*requestId*/number, { resolve: (x: webviewMessages.OutputItemEntry | undefined) => void }>();

		getOutputItem(outputId: string, mime: string) {
			const requestId = this._requestPool++;

			const { promise, resolve } = promiseWithResolvers<webviewMessages.OutputItemEntry | undefined>();
			this._requests.set(requestId, { resolve });

			postNotebookMessage<webviewMessages.IGetOutputItemMessage>('getOutputItem', { requestId, outputId, mime });
			return promise;
		}

		resolveOutputItem(requestId: number, output: webviewMessages.OutputItemEntry | undefined) {
			const request = this._requests.get(requestId);
			if (!request) {
				return;
			}

			this._requests.delete(requestId);
			request.resolve(output);
		}
	};