Skip to content

Commit

Permalink
Allow disabling filepath links (#200577)
Browse files Browse the repository at this point in the history
* add setting to enable/disable linkifying filepaths

* implement linkify setting

* update setting without reload

* switch casing style
  • Loading branch information
amunger authored Dec 11, 2023
1 parent 4c5336d commit d5d1424
Show file tree
Hide file tree
Showing 13 changed files with 130 additions and 49 deletions.
14 changes: 6 additions & 8 deletions extensions/notebook-renderers/src/ansi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 = '';

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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(' ');
Expand Down
9 changes: 6 additions & 3 deletions extensions/notebook-renderers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 => {
Expand Down Expand Up @@ -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');

Expand Down Expand Up @@ -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');
Expand Down
45 changes: 30 additions & 15 deletions extensions/notebook-renderers/src/linkify.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ type LinkPart = {
captures: string[];
};

export type LinkOptions = {
trustHtml?: boolean;
linkifyFilePaths: boolean;
};

export class LinkDetector {

// used by unit tests
Expand All @@ -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++) {
Expand All @@ -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];
Expand All @@ -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));
Expand All @@ -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) {
Expand Down Expand Up @@ -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: [] });
Expand Down Expand Up @@ -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);
}
2 changes: 2 additions & 0 deletions extensions/notebook-renderers/src/rendererTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> };
Expand All @@ -41,6 +42,7 @@ export type OutputElementOptions = {
scrollable?: boolean;
error?: boolean;
trustHtml?: boolean;
linkifyFilePaths: boolean;
};

export interface OutputWithAppend extends OutputItem {
Expand Down
15 changes: 10 additions & 5 deletions extensions/notebook-renderers/src/test/linkify.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
31 changes: 30 additions & 1 deletion extensions/notebook-renderers/src/test/notebookRenderer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down Expand Up @@ -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}`);
});

Expand Down
27 changes: 15 additions & 12 deletions extensions/notebook-renderers/src/textHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import { handleANSIOutput } from './ansi';
import { LinkOptions } from './linkify';
import { OutputElementOptions, OutputWithAppend } from './rendererTypes';
export const scrollableClass = 'scrollable';

Expand Down Expand Up @@ -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;
}
Expand All @@ -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);
Expand All @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -922,6 +922,12 @@ configurationRegistry.registerConfiguration({
tags: ['notebookLayout', 'notebookOutputLayout'],
minimum: 1,
},
[NotebookSetting.LinkifyOutputFilePaths]: {
description: nls.localize('notebook.disableOutputFilePathLinks', "Control whether to disable filepath links in the output of notebook cells."),
type: 'boolean',
default: true,
tags: ['notebookOutputLayout']
},
[NotebookSetting.markupFontSize]: {
markdownDescription: nls.localize('notebook.markup.fontSize', "Controls the font size in pixels of rendered markup in notebooks. When set to {0}, 120% of {1} is used.", '`0`', '`#editor.fontSize#`'),
type: 'number',
Expand Down
Loading

0 comments on commit d5d1424

Please sign in to comment.