in src/vs/workbench/contrib/notebook/browser/view/renderers/webviewPreloads.ts [70:1387]
async function webviewPreloads(ctx: PreloadContext) {
let currentOptions = ctx.options;
let isWorkspaceTrusted = ctx.isWorkspaceTrusted;
const acquireVsCodeApi = globalThis.acquireVsCodeApi;
const vscode = acquireVsCodeApi();
delete (globalThis as any).acquireVsCodeApi;
const tokenizationStyleElement = document.querySelector('style#vscode-tokenization-styles');
const handleInnerClick = (event: MouseEvent) => {
if (!event || !event.view || !event.view.document) {
return;
}
for (const node of event.composedPath()) {
if (node instanceof HTMLAnchorElement && node.href) {
if (node.href.startsWith('blob:')) {
handleBlobUrlClick(node.href, node.download);
} else if (node.href.startsWith('data:')) {
handleDataUrl(node.href, node.download);
} else if (node.hash && node.getAttribute('href') === node.hash) {
// Scrolling to location within current doc
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) {
postNotebookMessage<webviewMessages.IClickedLinkMessage>('clicked-link', { href });
}
}
event.preventDefault();
event.stopPropagation();
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);
}
};
document.body.addEventListener('click', handleInnerClick);
const preservedScriptAttributes: (keyof HTMLScriptElement)[] = [
'type', 'src', 'nonce', 'noModule', 'async',
];
// derived from https://github.com/jquery/jquery/blob/d0ce00cdfa680f1f0c38460bc51ea14079ae8b07/src/core/DOMEval.js
const domEval = (container: Element) => {
const arr = Array.from(container.getElementsByTagName('script'));
for (let n = 0; n < arr.length; n++) {
const node = arr[n];
const scriptTag = document.createElement('script');
const trustedScript = ttPolicy?.createScript(node.innerText) ?? node.innerText;
scriptTag.text = trustedScript as string;
for (const key of preservedScriptAttributes) {
const val = node[key] || node.getAttribute && node.getAttribute(key);
if (val) {
scriptTag.setAttribute(key, val as any);
}
}
// TODO@connor4312: should script with src not be removed?
container.appendChild(scriptTag).parentNode!.removeChild(scriptTag);
}
};
async function loadScriptSource(url: string, originalUri = url): Promise<string> {
const res = await fetch(url);
const text = await res.text();
if (!res.ok) {
throw new Error(`Unexpected ${res.status} requesting ${originalUri}: ${text || res.statusText}`);
}
return text;
}
interface RendererContext {
getState<T>(): T | undefined;
setState<T>(newState: T): void;
getRenderer(id: string): Promise<any | undefined>;
postMessage?(message: unknown): void;
onDidReceiveMessage?: Event<unknown>;
readonly workspace: { readonly isTrusted: boolean };
}
interface RendererModule {
activate(ctx: RendererContext): Promise<RendererApi | undefined | any> | RendererApi | undefined | any;
}
interface KernelPreloadContext {
readonly onDidReceiveKernelMessage: Event<unknown>;
postKernelMessage(data: unknown): void;
}
interface KernelPreloadModule {
activate(ctx: KernelPreloadContext): Promise<void> | void;
}
function createKernelContext(): KernelPreloadContext {
return {
onDidReceiveKernelMessage: onDidReceiveKernelMessage.event,
postKernelMessage: (data: unknown) => postNotebookMessage('customKernelMessage', { message: data }),
};
}
const invokeSourceWithGlobals = (functionSrc: string, globals: { [name: string]: unknown }) => {
const args = Object.entries(globals);
return new Function(...args.map(([k]) => k), functionSrc)(...args.map(([, v]) => v));
};
const runKernelPreload = async (url: string, originalUri: string): Promise<void> => {
const text = await loadScriptSource(url, originalUri);
const isModule = /\bexport\b.*\bactivate\b/.test(text);
try {
if (isModule) {
const module: KernelPreloadModule = await __import(url);
if (!module.activate) {
console.error(`Notebook preload (${url}) looks like a module but does not export an activate function`);
return;
}
return module.activate(createKernelContext());
} else {
return invokeSourceWithGlobals(text, { ...kernelPreloadGlobals, scriptUrl: url });
}
} catch (e) {
console.error(e);
throw e;
}
};
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, { id: string, output: boolean, lastKnownHeight: number }>();
constructor() {
this._observer = new ResizeObserver(entries => {
for (const entry of entries) {
if (!document.body.contains(entry.target)) {
continue;
}
const observedElementInfo = this._observedElements.get(entry.target);
if (!observedElementInfo) {
continue;
}
if (entry.target.id === observedElementInfo.id && entry.contentRect) {
if (observedElementInfo.output) {
if (entry.contentRect.height !== 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`;
}
}
const offsetHeight = entry.target.offsetHeight;
if (observedElementInfo.lastKnownHeight !== offsetHeight) {
observedElementInfo.lastKnownHeight = offsetHeight;
dimensionUpdater.updateHeight(observedElementInfo.id, offsetHeight, {
isOutput: observedElementInfo.output
});
}
}
}
});
}
public observe(container: Element, id: string, output: boolean) {
if (this._observedElements.has(container)) {
return;
}
this._observedElements.set(container, { id, output, lastKnownHeight: -1 });
this._observer.observe(container);
}
};
function scrollWillGoToParent(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;
}
if (event.deltaY < 0 && node.scrollTop > 0) {
return true;
}
if (event.deltaY > 0 && node.scrollTop + node.clientHeight < node.scrollHeight) {
return true;
}
}
return false;
}
const handleWheel = (event: WheelEvent & { wheelDeltaX?: number, wheelDeltaY?: number, wheelDelta?: number }) => {
if (event.defaultPrevented || scrollWillGoToParent(event)) {
return;
}
postNotebookMessage<webviewMessages.IWheelMessage>('did-scroll-wheel', {
payload: {
deltaMode: event.deltaMode,
deltaX: event.deltaX,
deltaY: event.deltaY,
deltaZ: event.deltaZ,
wheelDelta: event.wheelDelta,
wheelDeltaX: event.wheelDeltaX,
wheelDeltaY: event.wheelDeltaY,
detail: event.detail,
shiftKey: event.shiftKey,
type: event.type
}
});
};
function focusFirstFocusableInCell(cellId: string) {
const cellOutputContainer = document.getElementById(cellId);
if (cellOutputContainer) {
const focusableElement = cellOutputContainer.querySelector('[tabindex="0"], [href], button, input, option, select, textarea') as HTMLElement | null;
focusableElement?.focus();
}
}
function createFocusSink(cellId: string, focusNext?: boolean) {
const element = document.createElement('div');
element.tabIndex = 0;
element.addEventListener('focus', () => {
postNotebookMessage<webviewMessages.IBlurOutputMessage>('focus-editor', {
cellId: cellId,
focusNext
});
});
return element;
}
function addMouseoverListeners(element: HTMLElement, outputId: string): void {
element.addEventListener('mouseenter', () => {
postNotebookMessage<webviewMessages.IMouseEnterMessage>('mouseenter', {
id: outputId,
});
});
element.addEventListener('mouseleave', () => {
postNotebookMessage<webviewMessages.IMouseLeaveMessage>('mouseleave', {
id: outputId,
});
});
}
function isAncestor(testChild: Node | null, testAncestor: Node | null): boolean {
while (testChild) {
if (testChild === testAncestor) {
return true;
}
testChild = testChild.parentNode;
}
return false;
}
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';
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';
document.execCommand('removeFormat', false, undefined);
window.document.execCommand('hiliteColor', false, color);
document.designMode = 'Off';
window.getSelection()?.removeAllRanges();
} catch (e) {
console.log(e);
}
}
};
}
}
class OutputFocusTracker {
private _outputId: string;
private _hasFocus: boolean = false;
private _loosingFocus: boolean = false;
private _element: HTMLElement | Window;
constructor(element: HTMLElement | Window, outputId: string) {
this._element = element;
this._outputId = outputId;
this._hasFocus = isAncestor(document.activeElement, <HTMLElement>element);
this._loosingFocus = false;
element.addEventListener('focus', this._onFocus.bind(this), true);
element.addEventListener('blur', this._onBlur.bind(this), true);
}
private _onFocus() {
this._loosingFocus = false;
if (!this._hasFocus) {
this._hasFocus = true;
postNotebookMessage<webviewMessages.IOutputFocusMessage>('outputFocus', {
id: this._outputId,
});
}
}
private _onBlur() {
if (this._hasFocus) {
this._loosingFocus = true;
window.setTimeout(() => {
if (this._loosingFocus) {
this._loosingFocus = false;
this._hasFocus = false;
postNotebookMessage<webviewMessages.IOutputBlurMessage>('outputBlur', {
id: this._outputId,
});
}
}, 0);
}
}
dispose() {
if (this._element) {
this._element.removeEventListener('focus', this._onFocus, true);
this._element.removeEventListener('blur', this._onBlur, true);
}
}
}
const outputFocusTrackers = new Map<string, OutputFocusTracker>();
function addOutputFocusTracker(element: HTMLElement, outputId: string): void {
if (outputFocusTrackers.has(outputId)) {
outputFocusTrackers.get(outputId)?.dispose();
}
outputFocusTrackers.set(outputId, new OutputFocusTracker(element, outputId));
}
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 showPreloadErrors(outputNode: HTMLElement, ...errors: readonly Error[]) {
outputNode.innerText = `Error loading preloads:`;
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);
}
interface IOutputItem {
readonly id: string;
readonly mime: string;
metadata: unknown;
text(): string;
json(): any;
data(): Uint8Array;
blob(): Blob;
}
class OutputItem implements IOutputItem {
constructor(
public readonly id: string,
public readonly element: HTMLElement,
public readonly mime: string,
public readonly metadata: unknown,
public readonly valueBytes: Uint8Array
) { }
data() {
return this.valueBytes;
}
bytes() { return this.data(); }
text() {
return new TextDecoder().decode(this.valueBytes);
}
json() {
return JSON.parse(this.text());
}
blob() {
return new Blob([this.valueBytes], { type: this.mime });
}
}
const onDidReceiveKernelMessage = createEmitter<unknown>();
const kernelPreloadGlobals = {
acquireVsCodeApi,
onDidReceiveKernelMessage: onDidReceiveKernelMessage.event,
postKernelMessage: (data: unknown) => postNotebookMessage('customKernelMessage', { message: data }),
};
const ttPolicy = window.trustedTypes?.createPolicy('notebookRenderer', {
createHTML: value => value,
createScript: value => value,
});
window.addEventListener('wheel', handleWheel);
interface IFindMatch {
type: 'preview' | 'output',
id: string,
cellId: string,
container: Node,
originalRange: Range,
isShadow: boolean,
highlightResult?: IHighlightResult
}
interface IHighlighter {
highlightCurrentMatch(index: number): void;
unHighlightCurrentMatch(index: number): void;
dispose(): void;
}
let _highlighter: IHighlighter | null = null;
let matchColor = window.getComputedStyle(document.getElementById('_defaultColorPalatte')!).color;
let currentMatchColor = window.getComputedStyle(document.getElementById('_defaultColorPalatte')!).backgroundColor;
class JSHighlighter implements IHighlighter {
private _findMatchIndex = -1;
constructor(
readonly matches: IFindMatch[],
) {
for (let i = matches.length - 1; i >= 0; i--) {
const match = matches[i];
const ret = highlightRange(match.originalRange, true, 'mark', match.isShadow ? {
'style': 'background-color: ' + matchColor + ';',
} : {
'class': 'find-match'
});
match.highlightResult = ret;
}
}
highlightCurrentMatch(index: number) {
const oldMatch = this.matches[this._findMatchIndex];
if (oldMatch) {
oldMatch.highlightResult?.update(matchColor, oldMatch.isShadow ? undefined : 'find-match');
}
const match = this.matches[index];
this._findMatchIndex = index;
const sel = window.getSelection();
if (!!match && !!sel && match.highlightResult) {
let offset = 0;
try {
const outputOffset = document.getElementById(match.id)!.getBoundingClientRect().top;
const tempRange = document.createRange();
tempRange.selectNode(match.highlightResult.range.startContainer);
const rangeOffset = tempRange.getBoundingClientRect().top;
tempRange.detach();
offset = rangeOffset - outputOffset;
} catch (e) {
}
match.highlightResult?.update(currentMatchColor, match.isShadow ? undefined : 'current-find-match');
document.getSelection()?.removeAllRanges();
postNotebookMessage('didFindHighlight', {
offset
});
}
}
unHighlightCurrentMatch(index: number) {
const oldMatch = this.matches[index];
if (oldMatch && oldMatch.highlightResult) {
oldMatch.highlightResult.update(matchColor, oldMatch.isShadow ? undefined : 'find-match');
}
}
dispose() {
document.getSelection()?.removeAllRanges();
this.matches.forEach(match => {
match.highlightResult?.dispose();
});
}
}
class CSSHighlighter implements IHighlighter {
private _matchesHighlight: Highlight;
private _currentMatchesHighlight: Highlight;
private _findMatchIndex = -1;
constructor(
readonly matches: IFindMatch[],
) {
this._matchesHighlight = new Highlight();
this._matchesHighlight.priority = 1;
this._currentMatchesHighlight = new Highlight();
this._currentMatchesHighlight.priority = 2;
for (let i = 0; i < matches.length; i++) {
this._matchesHighlight.add(matches[i].originalRange);
}
CSS.highlights?.set('find-highlight', this._matchesHighlight);
CSS.highlights?.set('current-find-highlight', this._currentMatchesHighlight);
}
highlightCurrentMatch(index: number): void {
this._findMatchIndex = index;
const match = this.matches[this._findMatchIndex];
const range = match.originalRange;
if (match) {
let offset = 0;
try {
const outputOffset = document.getElementById(match.id)!.getBoundingClientRect().top;
const rangeOffset = match.originalRange.getBoundingClientRect().top;
offset = rangeOffset - outputOffset;
postNotebookMessage('didFindHighlight', {
offset
});
} catch (e) {
}
}
this._currentMatchesHighlight.clear();
this._currentMatchesHighlight.add(range);
}
unHighlightCurrentMatch(index: number): void {
this._currentMatchesHighlight.clear();
}
dispose(): void {
document.getSelection()?.removeAllRanges();
this._currentMatchesHighlight.clear();
this._matchesHighlight.clear();
}
}
const find = (query: string, options: { wholeWord?: boolean; caseSensitive?: boolean; includeMarkup: boolean; includeOutput: boolean; }) => {
let find = true;
let matches: IFindMatch[] = [];
let range = document.createRange();
range.selectNodeContents(document.getElementById('findStart')!);
let sel = window.getSelection();
sel?.removeAllRanges();
sel?.addRange(range);
viewModel.toggleDragDropEnabled(false);
try {
document.designMode = 'On';
while (find && matches.length < 500) {
find = (window as any).find(query, /* caseSensitive*/ !!options.caseSensitive,
/* backwards*/ false,
/* wrapAround*/ false,
/* wholeWord */ !!options.wholeWord,
/* searchInFrames*/ true,
false);
if (find) {
const selection = window.getSelection();
if (!selection) {
console.log('no selection');
break;
}
if (options.includeMarkup && selection.rangeCount > 0 && selection.getRangeAt(0).startContainer.nodeType === 1
&& (selection.getRangeAt(0).startContainer as Element).classList.contains('markup')) {
// markdown preview container
const preview = (selection.anchorNode?.firstChild as Element);
const root = preview.shadowRoot as ShadowRoot & { getSelection: () => Selection };
const shadowSelection = root?.getSelection ? root?.getSelection() : null;
if (shadowSelection && shadowSelection.anchorNode) {
matches.push({
type: 'preview',
id: preview.id,
cellId: preview.id,
container: preview,
isShadow: true,
originalRange: shadowSelection.getRangeAt(0)
});
}
}
if (options.includeOutput && selection.rangeCount > 0 && selection.getRangeAt(0).startContainer.nodeType === 1
&& (selection.getRangeAt(0).startContainer as Element).classList.contains('output_container')) {
// output container
const cellId = selection.getRangeAt(0).startContainer.parentElement!.id;
const outputNode = (selection.anchorNode?.firstChild as Element);
const root = outputNode.shadowRoot as ShadowRoot & { getSelection: () => Selection };
const shadowSelection = root?.getSelection ? root?.getSelection() : null;
if (shadowSelection && shadowSelection.anchorNode) {
matches.push({
type: 'output',
id: outputNode.id,
cellId: cellId,
container: outputNode,
isShadow: true,
originalRange: shadowSelection.getRangeAt(0)
});
}
}
const anchorNode = selection?.anchorNode?.parentElement;
if (anchorNode) {
const lastEl: any = matches.length ? matches[matches.length - 1] : null;
if (lastEl && lastEl.container.contains(anchorNode) && options.includeOutput) {
matches.push({
type: lastEl.type,
id: lastEl.id,
cellId: lastEl.cellId,
container: lastEl.container,
isShadow: false,
originalRange: window.getSelection()!.getRangeAt(0)
});
} else {
for (let node = anchorNode as Element | null; node; node = node.parentElement) {
if (!(node instanceof Element)) {
break;
}
if (node.classList.contains('output') && options.includeOutput) {
// inside output
const cellId = node.parentElement?.parentElement?.id;
if (cellId) {
matches.push({
type: 'output',
id: node.id,
cellId: cellId,
container: node,
isShadow: false,
originalRange: window.getSelection()!.getRangeAt(0)
});
}
break;
}
if (node.id === 'container' || node === document.body) {
break;
}
}
}
} else {
break;
}
}
}
} catch (e) {
console.log(e);
}
if (matches.length && CSS.highlights) {
_highlighter = new CSSHighlighter(matches);
} else {
_highlighter = new JSHighlighter(matches);
}
document.getSelection()?.removeAllRanges();
viewModel.toggleDragDropEnabled(currentOptions.dragAndDropEnabled);
postNotebookMessage('didFind', {
matches: matches.map((match, index) => ({
type: match.type,
id: match.id,
cellId: match.cellId,
index
}))
});
};
window.addEventListener('message', async rawEvent => {
const event = rawEvent as ({ data: webviewMessages.ToWebviewMessage; });
switch (event.data.type) {
case 'initializeMarkup':
await Promise.all(event.data.cells.map(info => viewModel.ensureMarkupCell(info)));
dimensionUpdater.updateImmediately();
postNotebookMessage('initializedMarkup', {});
break;
case 'createMarkupCell':
viewModel.ensureMarkupCell(event.data.cell);
break;
case 'showMarkupCell':
viewModel.showMarkupCell(event.data.id, event.data.top, event.data.content);
break;
case 'hideMarkupCells':
for (const id of event.data.ids) {
viewModel.hideMarkupCell(id);
}
break;
case 'unhideMarkupCells':
for (const id of event.data.ids) {
viewModel.unhideMarkupCell(id);
}
break;
case 'deleteMarkupCell':
for (const id of event.data.ids) {
viewModel.deleteMarkupCell(id);
}
break;
case 'updateSelectedMarkupCells':
viewModel.updateSelectedCells(event.data.selectedCellIds);
break;
case 'html': {
const data = event.data;
outputRunner.enqueue(data.outputId, (state) => {
return viewModel.renderOutputCell(data, state);
});
break;
}
case 'view-scroll':
{
// const date = new Date();
// console.log('----- will scroll ---- ', date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds());
event.data.widgets.forEach(widget => {
outputRunner.enqueue(widget.outputId, () => {
viewModel.updateOutputsScroll([widget]);
});
});
viewModel.updateMarkupScrolls(event.data.markupCells);
break;
}
case 'clear':
renderers.clearAll();
viewModel.clearAll();
document.getElementById('container')!.innerText = '';
outputFocusTrackers.forEach(ft => {
ft.dispose();
});
outputFocusTrackers.clear();
break;
case 'clearOutput': {
const { cellId, rendererId, outputId } = event.data;
outputRunner.cancelOutput(outputId);
viewModel.clearOutput(cellId, outputId, rendererId);
break;
}
case 'hideOutput': {
const { cellId, outputId } = event.data;
outputRunner.enqueue(outputId, () => {
viewModel.hideOutput(cellId);
});
break;
}
case 'showOutput': {
const { outputId, cellTop, cellId } = event.data;
outputRunner.enqueue(outputId, () => {
viewModel.showOutput(cellId, outputId, cellTop);
});
break;
}
case 'ack-dimension': {
for (const { cellId, outputId, height } of event.data.updates) {
viewModel.updateOutputHeight(cellId, outputId, height);
}
break;
}
case 'preload': {
const resources = event.data.resources;
for (const { uri, originalUri } of resources) {
kernelPreloads.load(uri, originalUri);
}
break;
}
case 'focus-output':
focusFirstFocusableInCell(event.data.cellId);
break;
case 'decorations': {
let outputContainer = document.getElementById(event.data.cellId);
if (!outputContainer) {
viewModel.ensureOutputCell(event.data.cellId, -100000, true);
outputContainer = document.getElementById(event.data.cellId);
}
outputContainer?.classList.add(...event.data.addedClassNames);
outputContainer?.classList.remove(...event.data.removedClassNames);
break;
}
case 'customKernelMessage':
onDidReceiveKernelMessage.fire(event.data.message);
break;
case 'customRendererMessage':
renderers.getRenderer(event.data.rendererId)?.receiveMessage(event.data.message);
break;
case 'notebookStyles': {
const documentStyle = document.documentElement.style;
for (let i = documentStyle.length - 1; i >= 0; i--) {
const property = documentStyle[i];
// Don't remove properties that the webview might have added separately
if (property && property.startsWith('--notebook-')) {
documentStyle.removeProperty(property);
}
}
// Re-add new properties
for (const [name, value] of Object.entries(event.data.styles)) {
documentStyle.setProperty(`--${name}`, value);
}
break;
}
case 'notebookOptions':
currentOptions = event.data.options;
viewModel.toggleDragDropEnabled(currentOptions.dragAndDropEnabled);
break;
case 'updateWorkspaceTrust': {
isWorkspaceTrusted = event.data.isTrusted;
viewModel.rerender();
break;
}
case 'tokenizedCodeBlock': {
const { codeBlockId, html } = event.data;
MarkupCell.highlightCodeBlock(codeBlockId, html);
break;
}
case 'tokenizedStylesChanged': {
if (tokenizationStyleElement) {
tokenizationStyleElement.textContent = event.data.css;
}
break;
}
case 'find': {
_highlighter?.dispose();
find(event.data.query, event.data.options);
break;
}
case 'findHighlight': {
_highlighter?.highlightCurrentMatch(event.data.index);
break;
}
case 'findUnHighlight': {
_highlighter?.unHighlightCurrentMatch(event.data.index);
break;
}
case 'findStop': {
_highlighter?.dispose();
break;
}
}
});
interface RendererApi {
renderOutputItem: (outputItem: IOutputItem, element: HTMLElement) => void;
disposeOutputItem?: (id?: string) => void;
}
class Renderer {
constructor(
public readonly data: RendererMetadata,
private readonly loadExtension: (id: string) => Promise<void>,
) { }
private _onMessageEvent = createEmitter();
private _loadPromise?: Promise<RendererApi | undefined>;
private _api: RendererApi | undefined;
public get api() { return this._api; }
public load(): Promise<RendererApi | undefined> {
if (!this._loadPromise) {
this._loadPromise = this._load();
}
return this._loadPromise;
}
public receiveMessage(message: unknown) {
this._onMessageEvent.fire(message);
}
private createRendererContext(): RendererContext {
const { id, messaging } = this.data;
const context: RendererContext = {
setState: newState => vscode.setState({ ...vscode.getState(), [id]: newState }),
getState: <T>() => {
const state = vscode.getState();
return typeof state === 'object' && state ? state[id] as T : undefined;
},
// TODO: This is async so that we can return a promise to the API in the future.
// Currently the API is always resolved before we call `createRendererContext`.
getRenderer: async (id: string) => renderers.getRenderer(id)?.api,
workspace: {
get isTrusted() { return isWorkspaceTrusted; }
}
};
if (messaging) {
context.onDidReceiveMessage = this._onMessageEvent.event;
context.postMessage = message => postNotebookMessage('customRendererMessage', { rendererId: id, message });
}
return context;
}
/** Inner function cached in the _loadPromise(). */
private async _load(): Promise<RendererApi | undefined> {
const module: RendererModule = await __import(this.data.entrypoint);
if (!module) {
return;
}
const api = await module.activate(this.createRendererContext());
this._api = api;
// Squash any errors extends errors. They won't prevent the renderer
// itself from working, so just log them.
await Promise.all(ctx.rendererData
.filter(d => d.extends === this.data.id)
.map(d => this.loadExtension(d.id).catch(console.error)),
);
return api;
}
}
const kernelPreloads = new class {
private readonly preloads = new Map<string /* uri */, Promise<unknown>>();
/**
* Returns a promise that resolves when the given preload is activated.
*/
public waitFor(uri: string) {
return this.preloads.get(uri) || Promise.resolve(new Error(`Preload not ready: ${uri}`));
}
/**
* Loads a preload.
* @param uri URI to load from
* @param originalUri URI to show in an error message if the preload is invalid.
*/
public load(uri: string, originalUri: string) {
const promise = Promise.all([
runKernelPreload(uri, originalUri),
this.waitForAllCurrent(),
]);
this.preloads.set(uri, promise);
return promise;
}
/**
* Returns a promise that waits for all currently-registered preloads to
* activate before resolving.
*/
private waitForAllCurrent() {
return Promise.all([...this.preloads.values()].map(p => p.catch(err => err)));
}
};
const outputRunner = new class {
private readonly outputs = new Map<string, { cancelled: boolean; queue: Promise<unknown> }>();
/**
* Pushes the action onto the list of actions for the given output ID,
* ensuring that it's run in-order.
*/
public enqueue(outputId: string, action: (record: { cancelled: boolean }) => unknown) {
const record = this.outputs.get(outputId);
if (!record) {
this.outputs.set(outputId, { cancelled: false, queue: new Promise(r => r(action({ cancelled: false }))) });
} else {
record.queue = record.queue.then(r => !record.cancelled && action(record));
}
}
/**
* Cancels the rendering of all outputs.
*/
public cancelAll() {
for (const record of this.outputs.values()) {
record.cancelled = true;
}
this.outputs.clear();
}
/**
* Cancels any ongoing rendering out an output.
*/
public cancelOutput(outputId: string) {
const output = this.outputs.get(outputId);
if (output) {
output.cancelled = true;
this.outputs.delete(outputId);
}
}
};