mirror of
https://github.com/ckaczor/azuredatastudio.git
synced 2026-02-16 10:58:30 -05:00
Enable VS Code notebooks with a built-in SQL kernel. (#21995)
This commit is contained in:
281
extensions/notebook-renderers/src/index.ts
Normal file
281
extensions/notebook-renderers/src/index.ts
Normal file
@@ -0,0 +1,281 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import type { ActivationFunction, OutputItem, RendererContext } from 'vscode-notebook-renderer';
|
||||
import { truncatedArrayOfString } from './textHelper';
|
||||
|
||||
interface IDisposable {
|
||||
dispose(): void;
|
||||
}
|
||||
|
||||
function clearContainer(container: HTMLElement) {
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function renderImage(outputInfo: OutputItem, element: HTMLElement): IDisposable {
|
||||
const blob = new Blob([outputInfo.data()], { type: outputInfo.mime });
|
||||
const src = URL.createObjectURL(blob);
|
||||
const disposable = {
|
||||
dispose: () => {
|
||||
URL.revokeObjectURL(src);
|
||||
}
|
||||
};
|
||||
|
||||
const image = document.createElement('img');
|
||||
image.src = src;
|
||||
const display = document.createElement('div');
|
||||
display.classList.add('display');
|
||||
display.appendChild(image);
|
||||
element.appendChild(display);
|
||||
|
||||
return disposable;
|
||||
}
|
||||
|
||||
const ttPolicy = window.trustedTypes?.createPolicy('notebookRenderer', {
|
||||
createHTML: value => value,
|
||||
createScript: value => value,
|
||||
});
|
||||
|
||||
const preservedScriptAttributes: (keyof HTMLScriptElement)[] = [
|
||||
'type', 'src', 'nonce', 'noModule', 'async',
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
function renderHTML(outputInfo: OutputItem, container: HTMLElement): void {
|
||||
clearContainer(container);
|
||||
const htmlContent = outputInfo.text();
|
||||
const element = document.createElement('div');
|
||||
const trustedHtml = ttPolicy?.createHTML(htmlContent) ?? htmlContent;
|
||||
element.innerHTML = trustedHtml as string;
|
||||
container.appendChild(element);
|
||||
domEval(element);
|
||||
}
|
||||
|
||||
function renderJavascript(outputInfo: OutputItem, container: HTMLElement): void {
|
||||
const str = outputInfo.text();
|
||||
const scriptVal = `<script type="application/javascript">${str}</script>`;
|
||||
const element = document.createElement('div');
|
||||
const trustedHtml = ttPolicy?.createHTML(scriptVal) ?? scriptVal;
|
||||
element.innerHTML = trustedHtml as string;
|
||||
container.appendChild(element);
|
||||
domEval(element);
|
||||
}
|
||||
|
||||
function renderError(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
|
||||
const element = document.createElement('div');
|
||||
container.appendChild(element);
|
||||
type ErrorLike = Partial<Error>;
|
||||
|
||||
let err: ErrorLike;
|
||||
try {
|
||||
err = <ErrorLike>JSON.parse(outputInfo.text());
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
return;
|
||||
}
|
||||
|
||||
if (err.stack) {
|
||||
const stack = document.createElement('pre');
|
||||
stack.classList.add('traceback');
|
||||
stack.style.margin = '8px 0';
|
||||
const element = document.createElement('span');
|
||||
truncatedArrayOfString(outputInfo.id, [err.stack ?? ''], ctx.settings.lineLimit, element);
|
||||
stack.appendChild(element);
|
||||
container.appendChild(stack);
|
||||
} else {
|
||||
const header = document.createElement('div');
|
||||
const headerMessage = err.name && err.message ? `${err.name}: ${err.message}` : err.name || err.message;
|
||||
if (headerMessage) {
|
||||
header.innerText = headerMessage;
|
||||
container.appendChild(header);
|
||||
}
|
||||
}
|
||||
|
||||
container.classList.add('error');
|
||||
}
|
||||
|
||||
function renderStream(outputInfo: OutputItem, container: HTMLElement, error: boolean, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
|
||||
const outputContainer = container.parentElement;
|
||||
if (!outputContainer) {
|
||||
// should never happen
|
||||
return;
|
||||
}
|
||||
|
||||
const prev = outputContainer.previousSibling;
|
||||
if (prev) {
|
||||
// OutputItem in the same cell
|
||||
// check if the previous item is a stream
|
||||
const outputElement = (prev.firstChild as HTMLElement | null);
|
||||
if (outputElement && outputElement.getAttribute('output-mime-type') === outputInfo.mime) {
|
||||
// same stream
|
||||
const text = outputInfo.text();
|
||||
|
||||
const element = document.createElement('span');
|
||||
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, element);
|
||||
outputElement.appendChild(element);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const element = document.createElement('span');
|
||||
element.classList.add('output-stream');
|
||||
|
||||
const text = outputInfo.text();
|
||||
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, element);
|
||||
while (container.firstChild) {
|
||||
container.removeChild(container.firstChild);
|
||||
}
|
||||
container.appendChild(element);
|
||||
container.setAttribute('output-mime-type', outputInfo.mime);
|
||||
if (error) {
|
||||
container.classList.add('error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderText(outputInfo: OutputItem, container: HTMLElement, ctx: RendererContext<void> & { readonly settings: { readonly lineLimit: number } }): void {
|
||||
clearContainer(container);
|
||||
const contentNode = document.createElement('div');
|
||||
contentNode.classList.add('output-plaintext');
|
||||
const text = outputInfo.text();
|
||||
truncatedArrayOfString(outputInfo.id, [text], ctx.settings.lineLimit, contentNode);
|
||||
container.appendChild(contentNode);
|
||||
|
||||
}
|
||||
|
||||
export const activate: ActivationFunction<void> = (ctx) => {
|
||||
const disposables = new Map<string, IDisposable>();
|
||||
const latestContext = ctx as (RendererContext<void> & { readonly settings: { readonly lineLimit: number } });
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.textContent = `
|
||||
.output-plaintext,
|
||||
.output-stream,
|
||||
.traceback {
|
||||
line-height: var(--notebook-cell-output-line-height);
|
||||
font-family: var(--notebook-cell-output-font-family);
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
|
||||
font-size: var(--notebook-cell-output-font-size);
|
||||
user-select: text;
|
||||
-webkit-user-select: text;
|
||||
-ms-user-select: text;
|
||||
cursor: auto;
|
||||
}
|
||||
span.output-stream {
|
||||
display: inline-block;
|
||||
}
|
||||
.output-plaintext .code-bold,
|
||||
.output-stream .code-bold,
|
||||
.traceback .code-bold {
|
||||
font-weight: bold;
|
||||
}
|
||||
.output-plaintext .code-italic,
|
||||
.output-stream .code-italic,
|
||||
.traceback .code-italic {
|
||||
font-style: italic;
|
||||
}
|
||||
.output-plaintext .code-strike-through,
|
||||
.output-stream .code-strike-through,
|
||||
.traceback .code-strike-through {
|
||||
text-decoration: line-through;
|
||||
}
|
||||
.output-plaintext .code-underline,
|
||||
.output-stream .code-underline,
|
||||
.traceback .code-underline {
|
||||
text-decoration: underline;
|
||||
}
|
||||
`;
|
||||
document.body.appendChild(style);
|
||||
return {
|
||||
renderOutputItem: (outputInfo, element) => {
|
||||
switch (outputInfo.mime) {
|
||||
case 'text/html':
|
||||
case 'image/svg+xml':
|
||||
{
|
||||
if (!ctx.workspace.isTrusted) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderHTML(outputInfo, element);
|
||||
}
|
||||
break;
|
||||
case 'application/javascript':
|
||||
{
|
||||
if (!ctx.workspace.isTrusted) {
|
||||
return;
|
||||
}
|
||||
|
||||
renderJavascript(outputInfo, element);
|
||||
}
|
||||
break;
|
||||
case 'image/gif':
|
||||
case 'image/png':
|
||||
case 'image/jpeg':
|
||||
case 'image/git':
|
||||
{
|
||||
const disposable = renderImage(outputInfo, element);
|
||||
disposables.set(outputInfo.id, disposable);
|
||||
}
|
||||
break;
|
||||
case 'application/vnd.code.notebook.error':
|
||||
{
|
||||
renderError(outputInfo, element, latestContext);
|
||||
}
|
||||
break;
|
||||
case 'application/vnd.code.notebook.stdout':
|
||||
case 'application/x.notebook.stdout':
|
||||
case 'application/x.notebook.stream':
|
||||
{
|
||||
renderStream(outputInfo, element, false, latestContext);
|
||||
}
|
||||
break;
|
||||
case 'application/vnd.code.notebook.stderr':
|
||||
case 'application/x.notebook.stderr':
|
||||
{
|
||||
renderStream(outputInfo, element, true, latestContext);
|
||||
}
|
||||
break;
|
||||
case 'text/plain':
|
||||
{
|
||||
renderText(outputInfo, element, latestContext);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
},
|
||||
disposeOutputItem: (id: string | undefined) => {
|
||||
if (id) {
|
||||
disposables.get(id)?.dispose();
|
||||
} else {
|
||||
disposables.forEach(d => d.dispose());
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user