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