Allow disabling filepath links (#200577)

* add setting to enable/disable linkifying filepaths

* implement linkify setting

* update setting without reload

* switch casing style
This commit is contained in:
Aaron Munger
2023-12-11 15:47:06 -08:00
committed by GitHub
parent 4c5336dae1
commit d5d1424296
13 changed files with 130 additions and 49 deletions

View File

@@ -5,11 +5,10 @@
import { RGBA, Color } from './color';
import { ansiColorIdentifiers } from './colorMap';
import { linkify } from './linkify';
import { LinkOptions, linkify } from './linkify';
export function handleANSIOutput(text: string, trustHtml: boolean): HTMLSpanElement {
const workspaceFolder = undefined;
export function handleANSIOutput(text: string, linkOptions: LinkOptions): HTMLSpanElement {
const root: HTMLSpanElement = document.createElement('span');
const textLength: number = text.length;
@@ -52,7 +51,7 @@ export function handleANSIOutput(text: string, trustHtml: boolean): HTMLSpanElem
if (sequenceFound) {
// Flush buffer with previous styles.
appendStylizedStringToContainer(root, buffer, trustHtml, styleNames, workspaceFolder, customFgColor, customBgColor, customUnderlineColor);
appendStylizedStringToContainer(root, buffer, linkOptions, styleNames, customFgColor, customBgColor, customUnderlineColor);
buffer = '';
@@ -98,7 +97,7 @@ export function handleANSIOutput(text: string, trustHtml: boolean): HTMLSpanElem
// Flush remaining text buffer if not empty.
if (buffer) {
appendStylizedStringToContainer(root, buffer, trustHtml, styleNames, workspaceFolder, customFgColor, customBgColor, customUnderlineColor);
appendStylizedStringToContainer(root, buffer, linkOptions, styleNames, customFgColor, customBgColor, customUnderlineColor);
}
return root;
@@ -382,9 +381,8 @@ export function handleANSIOutput(text: string, trustHtml: boolean): HTMLSpanElem
function appendStylizedStringToContainer(
root: HTMLElement,
stringContent: string,
trustHtml: boolean,
linkOptions: LinkOptions,
cssClasses: string[],
workspaceFolder: string | undefined,
customTextColor?: RGBA | string,
customBackgroundColor?: RGBA | string,
customUnderlineColor?: RGBA | string
@@ -397,7 +395,7 @@ function appendStylizedStringToContainer(
if (container.childElementCount === 0) {
// plain text
container = linkify(stringContent, true, workspaceFolder, trustHtml);
container = linkify(stringContent, linkOptions, true);
}
container.className = cssClasses.join(' ');

View File

@@ -176,7 +176,9 @@ function renderError(
const stackTrace = formatStackTrace(err.stack);
const outputScrolling = scrollingEnabled(outputInfo, ctx.settings);
const content = createOutputContent(outputInfo.id, stackTrace ?? '', { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml });
const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml, linkifyFilePaths: ctx.settings.linkifyFilePaths };
const content = createOutputContent(outputInfo.id, stackTrace ?? '', outputOptions);
const contentParent = document.createElement('div');
contentParent.classList.toggle('word-wrap', ctx.settings.outputWordWrap);
disposableStore.push(ctx.onDidChangeSettings(e => {
@@ -279,7 +281,7 @@ function scrollingEnabled(output: OutputItem, options: RenderOptions) {
function renderStream(outputInfo: OutputWithAppend, outputElement: HTMLElement, error: boolean, ctx: IRichRenderContext): IDisposable {
const disposableStore = createDisposableStore();
const outputScrolling = scrollingEnabled(outputInfo, ctx.settings);
const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, error };
const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, error, linkifyFilePaths: ctx.settings.linkifyFilePaths };
outputElement.classList.add('output-stream');
@@ -330,7 +332,8 @@ function renderText(outputInfo: OutputItem, outputElement: HTMLElement, ctx: IRi
const text = outputInfo.text();
const outputScrolling = scrollingEnabled(outputInfo, ctx.settings);
const content = createOutputContent(outputInfo.id, text, { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false });
const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, linkifyFilePaths: ctx.settings.linkifyFilePaths };
const content = createOutputContent(outputInfo.id, text, outputOptions);
content.classList.add('output-plaintext');
if (ctx.settings.outputWordWrap) {
content.classList.add('word-wrap');

View File

@@ -26,6 +26,11 @@ type LinkPart = {
captures: string[];
};
export type LinkOptions = {
trustHtml?: boolean;
linkifyFilePaths: boolean;
};
export class LinkDetector {
// used by unit tests
@@ -51,7 +56,8 @@ export class LinkDetector {
* When splitLines is true, each line of the text, even if it contains no links, is wrapped in a <span>
* and added as a child of the returned <span>.
*/
linkify(text: string, splitLines?: boolean, workspaceFolder?: string, trustHtml?: boolean): HTMLElement {
linkify(text: string, options: LinkOptions, splitLines?: boolean): HTMLElement {
console.log('linkifyiiiiiing', JSON.stringify(options));
if (splitLines) {
const lines = text.split('\n');
for (let i = 0; i < lines.length - 1; i++) {
@@ -61,7 +67,7 @@ export class LinkDetector {
// Remove the last element ('') that split added.
lines.pop();
}
const elements = lines.map(line => this.linkify(line, false, workspaceFolder, trustHtml));
const elements = lines.map(line => this.linkify(line, options, false));
if (elements.length === 1) {
// Do not wrap single line with extra span.
return elements[0];
@@ -72,8 +78,9 @@ export class LinkDetector {
}
const container = document.createElement('span');
for (const part of this.detectLinks(text)) {
for (const part of this.detectLinks(text, !!options.trustHtml, options.linkifyFilePaths)) {
try {
let span: HTMLSpanElement | null = null;
switch (part.kind) {
case 'text':
container.appendChild(document.createTextNode(part.value));
@@ -83,13 +90,9 @@ export class LinkDetector {
container.appendChild(this.createWebLink(part.value));
break;
case 'html':
if (this.shouldGenerateHtml(!!trustHtml)) {
const span = document.createElement('span');
span.innerHTML = this.createHtml(part.value)!;
container.appendChild(span);
} else {
container.appendChild(document.createTextNode(part.value));
}
span = document.createElement('span');
span.innerHTML = this.createHtml(part.value)!;
container.appendChild(span);
break;
}
} catch (e) {
@@ -149,15 +152,27 @@ export class LinkDetector {
return link;
}
private detectLinks(text: string): LinkPart[] {
private detectLinks(text: string, trustHtml: boolean, detectFilepaths: boolean): LinkPart[] {
if (text.length > MAX_LENGTH) {
return [{ kind: 'text', value: text, captures: [] }];
}
const regexes: RegExp[] = [HTML_LINK_REGEX, WEB_LINK_REGEX, PATH_LINK_REGEX];
const kinds: LinkKind[] = ['html', 'web', 'path'];
const regexes: RegExp[] = [];
const kinds: LinkKind[] = [];
const result: LinkPart[] = [];
if (this.shouldGenerateHtml(trustHtml)) {
regexes.push(HTML_LINK_REGEX);
kinds.push('html');
}
regexes.push(WEB_LINK_REGEX);
kinds.push('web');
if (detectFilepaths) {
regexes.push(PATH_LINK_REGEX);
kinds.push('path');
}
const splitOne = (text: string, regexIndex: number) => {
if (regexIndex >= regexes.length) {
result.push({ value: text, kind: 'text', captures: [] });
@@ -192,6 +207,6 @@ export class LinkDetector {
}
const linkDetector = new LinkDetector();
export function linkify(text: string, splitLines?: boolean, workspaceFolder?: string, trustHtml = false) {
return linkDetector.linkify(text, splitLines, workspaceFolder, trustHtml);
export function linkify(text: string, linkOptions: LinkOptions, splitLines?: boolean) {
return linkDetector.linkify(text, linkOptions, splitLines);
}

View File

@@ -32,6 +32,7 @@ export interface RenderOptions {
readonly lineLimit: number;
readonly outputScrolling: boolean;
readonly outputWordWrap: boolean;
readonly linkifyFilePaths: boolean;
}
export type IRichRenderContext = RendererContext<void> & { readonly settings: RenderOptions; readonly onDidChangeSettings: Event<RenderOptions> };
@@ -41,6 +42,7 @@ export type OutputElementOptions = {
scrollable?: boolean;
error?: boolean;
trustHtml?: boolean;
linkifyFilePaths: boolean;
};
export interface OutputWithAppend extends OutputItem {

View File

@@ -15,27 +15,32 @@ suite('Notebook builtin output link detection', () => {
LinkDetector.injectedHtmlCreator = (value: string) => value;
test('no links', () => {
const htmlWithLinks = linkify('hello', true, undefined, true);
const htmlWithLinks = linkify('hello', { linkifyFilePaths: true, trustHtml: true }, true);
assert.equal(htmlWithLinks.innerHTML, 'hello');
});
test('web link detection', () => {
const htmlWithLinks = linkify('something www.example.com something', true, undefined, true);
const htmlWithLinks = linkify('something www.example.com something', { linkifyFilePaths: true, trustHtml: true }, true);
const htmlWithLinks2 = linkify('something www.example.com something', { linkifyFilePaths: false, trustHtml: false }, true);
assert.equal(htmlWithLinks.innerHTML, 'something <a href="www.example.com">www.example.com</a> something');
assert.equal(htmlWithLinks.textContent, 'something www.example.com something');
assert.equal(htmlWithLinks2.innerHTML, 'something <a href="www.example.com">www.example.com</a> something');
assert.equal(htmlWithLinks2.textContent, 'something www.example.com something');
});
test('html link detection', () => {
const htmlWithLinks = linkify('something <a href="www.example.com">link</a> something', true, undefined, true);
const htmlWithLinks = linkify('something <a href="www.example.com">link</a> something', { linkifyFilePaths: true, trustHtml: true }, true);
const htmlWithLinks2 = linkify('something <a href="www.example.com">link</a> something', { linkifyFilePaths: false, trustHtml: true }, true);
assert.equal(htmlWithLinks.innerHTML, 'something <span><a href="www.example.com">link</a></span> something');
assert.equal(htmlWithLinks.textContent, 'something link something');
assert.equal(htmlWithLinks2.innerHTML, 'something <span><a href="www.example.com">link</a></span> something');
assert.equal(htmlWithLinks2.textContent, 'something link something');
});
test('html link without trust', () => {
const trustHtml = false;
const htmlWithLinks = linkify('something <a href="file.py">link</a> something', true, undefined, trustHtml);
const htmlWithLinks = linkify('something <a href="file.py">link</a> something', { linkifyFilePaths: true, trustHtml: false }, true);
assert.equal(htmlWithLinks.innerHTML, 'something &lt;a href="file.py"&gt;link&lt;/a&gt; something');
assert.equal(htmlWithLinks.textContent, 'something <a href="file.py">link</a> something');

View File

@@ -273,6 +273,36 @@ suite('Notebook builtin output renderer', () => {
assert.ok(inserted.innerHTML.indexOf('shouldBeTruncated') === -1, `Beginning content should be truncated`);
});
test(`Render filepath links in text output when enabled`, async () => {
LinkDetector.injectedHtmlCreator = (value: string) => value;
const context = createContext({ outputWordWrap: true, outputScrolling: true, linkifyFilePaths: true });
const renderer = await activate(context);
assert.ok(renderer, 'Renderer not created');
const outputElement = new OutputHtml().getFirstOuputElement();
const outputItem = createOutputItem('./dir/file.txt', stdoutMimeType);
await renderer!.renderOutputItem(outputItem, outputElement);
const inserted = outputElement.firstChild as HTMLElement;
assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`);
assert.ok(outputElement.innerHTML.indexOf('<a href="./dir/file.txt">') !== -1, `inner HTML:\n ${outputElement.innerHTML}`);
});
test(`No filepath links in text output when disabled`, async () => {
LinkDetector.injectedHtmlCreator = (value: string) => value;
const context = createContext({ outputWordWrap: true, outputScrolling: true, linkifyFilePaths: false });
const renderer = await activate(context);
assert.ok(renderer, 'Renderer not created');
const outputElement = new OutputHtml().getFirstOuputElement();
const outputItem = createOutputItem('./dir/file.txt', stdoutMimeType);
await renderer!.renderOutputItem(outputItem, outputElement);
const inserted = outputElement.firstChild as HTMLElement;
assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`);
assert.ok(outputElement.innerHTML.indexOf('<a href="./dir/file.txt">') === -1, `inner HTML:\n ${outputElement.innerHTML}`);
});
test(`Render with wordwrap and scrolling for error output`, async () => {
LinkDetector.injectedHtmlCreator = (value: string) => value;
const context = createContext({ outputWordWrap: true, outputScrolling: true });
@@ -474,7 +504,6 @@ suite('Notebook builtin output renderer', () => {
const inserted = outputElement.firstChild as HTMLElement;
assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`);
//assert.ok(false, `TextContent:\n ${outputElement.textContent}`);
assert.ok(outputElement.innerHTML.indexOf('class="code-background-colored"') === -1, `inner HTML:\n ${outputElement.innerHTML}`);
});

View File

@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { handleANSIOutput } from './ansi';
import { LinkOptions } from './linkify';
import { OutputElementOptions, OutputWithAppend } from './rendererTypes';
export const scrollableClass = 'scrollable';
@@ -68,44 +69,44 @@ function generateNestedViewAllElement(outputId: string) {
return container;
}
function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number, trustHtml: boolean) {
function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number, linkOptions: LinkOptions) {
const container = document.createElement('div');
const lineCount = buffer.length;
if (lineCount <= linesLimit) {
const spanElement = handleANSIOutput(buffer.join('\n'), trustHtml);
const spanElement = handleANSIOutput(buffer.join('\n'), linkOptions);
container.appendChild(spanElement);
return container;
}
container.appendChild(handleANSIOutput(buffer.slice(0, linesLimit - 5).join('\n'), trustHtml));
container.appendChild(handleANSIOutput(buffer.slice(0, linesLimit - 5).join('\n'), linkOptions));
// truncated piece
const elipses = document.createElement('div');
elipses.innerText = '...';
container.appendChild(elipses);
container.appendChild(handleANSIOutput(buffer.slice(lineCount - 5).join('\n'), trustHtml));
container.appendChild(handleANSIOutput(buffer.slice(lineCount - 5).join('\n'), linkOptions));
container.appendChild(generateViewMoreElement(id));
return container;
}
function scrollableArrayOfString(id: string, buffer: string[], trustHtml: boolean) {
function scrollableArrayOfString(id: string, buffer: string[], linkOptions: LinkOptions) {
const element = document.createElement('div');
if (buffer.length > softScrollableLineLimit) {
element.appendChild(generateNestedViewAllElement(id));
}
element.appendChild(handleANSIOutput(buffer.slice(-1 * softScrollableLineLimit).join('\n'), trustHtml));
element.appendChild(handleANSIOutput(buffer.slice(-1 * softScrollableLineLimit).join('\n'), linkOptions));
return element;
}
const outputLengths: Record<string, number> = {};
function appendScrollableOutput(element: HTMLElement, id: string, appended: string, trustHtml: boolean) {
function appendScrollableOutput(element: HTMLElement, id: string, appended: string, linkOptions: LinkOptions) {
if (!outputLengths[id]) {
outputLengths[id] = 0;
}
@@ -117,22 +118,23 @@ function appendScrollableOutput(element: HTMLElement, id: string, appended: stri
return false;
}
else {
element.appendChild(handleANSIOutput(buffer.join('\n'), trustHtml));
element.appendChild(handleANSIOutput(buffer.join('\n'), linkOptions));
outputLengths[id] = appendedLength;
}
return true;
}
export function createOutputContent(id: string, outputText: string, options: OutputElementOptions): HTMLElement {
const { linesLimit, error, scrollable, trustHtml } = options;
const { linesLimit, error, scrollable, trustHtml, linkifyFilePaths } = options;
const linkOptions: LinkOptions = { linkifyFilePaths, trustHtml };
const buffer = outputText.split(/\r\n|\r|\n/g);
outputLengths[id] = outputLengths[id] = Math.min(buffer.length, softScrollableLineLimit);
let outputElement: HTMLElement;
if (scrollable) {
outputElement = scrollableArrayOfString(id, buffer, !!trustHtml);
outputElement = scrollableArrayOfString(id, buffer, linkOptions);
} else {
outputElement = truncatedArrayOfString(id, buffer, linesLimit, !!trustHtml);
outputElement = truncatedArrayOfString(id, buffer, linesLimit, linkOptions);
}
outputElement.setAttribute('output-item-id', id);
@@ -145,9 +147,10 @@ export function createOutputContent(id: string, outputText: string, options: Out
export function appendOutput(outputInfo: OutputWithAppend, existingContent: HTMLElement, options: OutputElementOptions) {
const appendedText = outputInfo.appendedText?.();
const linkOptions = { linkifyFilePaths: options.linkifyFilePaths, trustHtml: options.trustHtml };
// appending output only supported for scrollable ouputs currently
if (appendedText && options.scrollable) {
if (appendScrollableOutput(existingContent, outputInfo.id, appendedText, false)) {
if (appendScrollableOutput(existingContent, outputInfo.id, appendedText, linkOptions)) {
return;
}
}