Skip to content

Commit d5d1424

Browse files
authored
Allow disabling filepath links (#200577)
* add setting to enable/disable linkifying filepaths * implement linkify setting * update setting without reload * switch casing style
1 parent 4c5336d commit d5d1424

File tree

13 files changed

+130
-49
lines changed

13 files changed

+130
-49
lines changed

extensions/notebook-renderers/src/ansi.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,10 @@
55

66
import { RGBA, Color } from './color';
77
import { ansiColorIdentifiers } from './colorMap';
8-
import { linkify } from './linkify';
8+
import { LinkOptions, linkify } from './linkify';
99

1010

11-
export function handleANSIOutput(text: string, trustHtml: boolean): HTMLSpanElement {
12-
const workspaceFolder = undefined;
11+
export function handleANSIOutput(text: string, linkOptions: LinkOptions): HTMLSpanElement {
1312

1413
const root: HTMLSpanElement = document.createElement('span');
1514
const textLength: number = text.length;
@@ -52,7 +51,7 @@ export function handleANSIOutput(text: string, trustHtml: boolean): HTMLSpanElem
5251
if (sequenceFound) {
5352

5453
// Flush buffer with previous styles.
55-
appendStylizedStringToContainer(root, buffer, trustHtml, styleNames, workspaceFolder, customFgColor, customBgColor, customUnderlineColor);
54+
appendStylizedStringToContainer(root, buffer, linkOptions, styleNames, customFgColor, customBgColor, customUnderlineColor);
5655

5756
buffer = '';
5857

@@ -98,7 +97,7 @@ export function handleANSIOutput(text: string, trustHtml: boolean): HTMLSpanElem
9897

9998
// Flush remaining text buffer if not empty.
10099
if (buffer) {
101-
appendStylizedStringToContainer(root, buffer, trustHtml, styleNames, workspaceFolder, customFgColor, customBgColor, customUnderlineColor);
100+
appendStylizedStringToContainer(root, buffer, linkOptions, styleNames, customFgColor, customBgColor, customUnderlineColor);
102101
}
103102

104103
return root;
@@ -382,9 +381,8 @@ export function handleANSIOutput(text: string, trustHtml: boolean): HTMLSpanElem
382381
function appendStylizedStringToContainer(
383382
root: HTMLElement,
384383
stringContent: string,
385-
trustHtml: boolean,
384+
linkOptions: LinkOptions,
386385
cssClasses: string[],
387-
workspaceFolder: string | undefined,
388386
customTextColor?: RGBA | string,
389387
customBackgroundColor?: RGBA | string,
390388
customUnderlineColor?: RGBA | string
@@ -397,7 +395,7 @@ function appendStylizedStringToContainer(
397395

398396
if (container.childElementCount === 0) {
399397
// plain text
400-
container = linkify(stringContent, true, workspaceFolder, trustHtml);
398+
container = linkify(stringContent, linkOptions, true);
401399
}
402400

403401
container.className = cssClasses.join(' ');

extensions/notebook-renderers/src/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,9 @@ function renderError(
176176
const stackTrace = formatStackTrace(err.stack);
177177

178178
const outputScrolling = scrollingEnabled(outputInfo, ctx.settings);
179-
const content = createOutputContent(outputInfo.id, stackTrace ?? '', { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml });
179+
const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml, linkifyFilePaths: ctx.settings.linkifyFilePaths };
180+
181+
const content = createOutputContent(outputInfo.id, stackTrace ?? '', outputOptions);
180182
const contentParent = document.createElement('div');
181183
contentParent.classList.toggle('word-wrap', ctx.settings.outputWordWrap);
182184
disposableStore.push(ctx.onDidChangeSettings(e => {
@@ -279,7 +281,7 @@ function scrollingEnabled(output: OutputItem, options: RenderOptions) {
279281
function renderStream(outputInfo: OutputWithAppend, outputElement: HTMLElement, error: boolean, ctx: IRichRenderContext): IDisposable {
280282
const disposableStore = createDisposableStore();
281283
const outputScrolling = scrollingEnabled(outputInfo, ctx.settings);
282-
const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, error };
284+
const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, error, linkifyFilePaths: ctx.settings.linkifyFilePaths };
283285

284286
outputElement.classList.add('output-stream');
285287

@@ -330,7 +332,8 @@ function renderText(outputInfo: OutputItem, outputElement: HTMLElement, ctx: IRi
330332

331333
const text = outputInfo.text();
332334
const outputScrolling = scrollingEnabled(outputInfo, ctx.settings);
333-
const content = createOutputContent(outputInfo.id, text, { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false });
335+
const outputOptions = { linesLimit: ctx.settings.lineLimit, scrollable: outputScrolling, trustHtml: false, linkifyFilePaths: ctx.settings.linkifyFilePaths };
336+
const content = createOutputContent(outputInfo.id, text, outputOptions);
334337
content.classList.add('output-plaintext');
335338
if (ctx.settings.outputWordWrap) {
336339
content.classList.add('word-wrap');

extensions/notebook-renderers/src/linkify.ts

Lines changed: 30 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ type LinkPart = {
2626
captures: string[];
2727
};
2828

29+
export type LinkOptions = {
30+
trustHtml?: boolean;
31+
linkifyFilePaths: boolean;
32+
};
33+
2934
export class LinkDetector {
3035

3136
// used by unit tests
@@ -51,7 +56,8 @@ export class LinkDetector {
5156
* When splitLines is true, each line of the text, even if it contains no links, is wrapped in a <span>
5257
* and added as a child of the returned <span>.
5358
*/
54-
linkify(text: string, splitLines?: boolean, workspaceFolder?: string, trustHtml?: boolean): HTMLElement {
59+
linkify(text: string, options: LinkOptions, splitLines?: boolean): HTMLElement {
60+
console.log('linkifyiiiiiing', JSON.stringify(options));
5561
if (splitLines) {
5662
const lines = text.split('\n');
5763
for (let i = 0; i < lines.length - 1; i++) {
@@ -61,7 +67,7 @@ export class LinkDetector {
6167
// Remove the last element ('') that split added.
6268
lines.pop();
6369
}
64-
const elements = lines.map(line => this.linkify(line, false, workspaceFolder, trustHtml));
70+
const elements = lines.map(line => this.linkify(line, options, false));
6571
if (elements.length === 1) {
6672
// Do not wrap single line with extra span.
6773
return elements[0];
@@ -72,8 +78,9 @@ export class LinkDetector {
7278
}
7379

7480
const container = document.createElement('span');
75-
for (const part of this.detectLinks(text)) {
81+
for (const part of this.detectLinks(text, !!options.trustHtml, options.linkifyFilePaths)) {
7682
try {
83+
let span: HTMLSpanElement | null = null;
7784
switch (part.kind) {
7885
case 'text':
7986
container.appendChild(document.createTextNode(part.value));
@@ -83,13 +90,9 @@ export class LinkDetector {
8390
container.appendChild(this.createWebLink(part.value));
8491
break;
8592
case 'html':
86-
if (this.shouldGenerateHtml(!!trustHtml)) {
87-
const span = document.createElement('span');
88-
span.innerHTML = this.createHtml(part.value)!;
89-
container.appendChild(span);
90-
} else {
91-
container.appendChild(document.createTextNode(part.value));
92-
}
93+
span = document.createElement('span');
94+
span.innerHTML = this.createHtml(part.value)!;
95+
container.appendChild(span);
9396
break;
9497
}
9598
} catch (e) {
@@ -149,15 +152,27 @@ export class LinkDetector {
149152
return link;
150153
}
151154

152-
private detectLinks(text: string): LinkPart[] {
155+
private detectLinks(text: string, trustHtml: boolean, detectFilepaths: boolean): LinkPart[] {
153156
if (text.length > MAX_LENGTH) {
154157
return [{ kind: 'text', value: text, captures: [] }];
155158
}
156159

157-
const regexes: RegExp[] = [HTML_LINK_REGEX, WEB_LINK_REGEX, PATH_LINK_REGEX];
158-
const kinds: LinkKind[] = ['html', 'web', 'path'];
160+
const regexes: RegExp[] = [];
161+
const kinds: LinkKind[] = [];
159162
const result: LinkPart[] = [];
160163

164+
if (this.shouldGenerateHtml(trustHtml)) {
165+
regexes.push(HTML_LINK_REGEX);
166+
kinds.push('html');
167+
}
168+
regexes.push(WEB_LINK_REGEX);
169+
kinds.push('web');
170+
if (detectFilepaths) {
171+
regexes.push(PATH_LINK_REGEX);
172+
kinds.push('path');
173+
}
174+
175+
161176
const splitOne = (text: string, regexIndex: number) => {
162177
if (regexIndex >= regexes.length) {
163178
result.push({ value: text, kind: 'text', captures: [] });
@@ -192,6 +207,6 @@ export class LinkDetector {
192207
}
193208

194209
const linkDetector = new LinkDetector();
195-
export function linkify(text: string, splitLines?: boolean, workspaceFolder?: string, trustHtml = false) {
196-
return linkDetector.linkify(text, splitLines, workspaceFolder, trustHtml);
210+
export function linkify(text: string, linkOptions: LinkOptions, splitLines?: boolean) {
211+
return linkDetector.linkify(text, linkOptions, splitLines);
197212
}

extensions/notebook-renderers/src/rendererTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface RenderOptions {
3232
readonly lineLimit: number;
3333
readonly outputScrolling: boolean;
3434
readonly outputWordWrap: boolean;
35+
readonly linkifyFilePaths: boolean;
3536
}
3637

3738
export type IRichRenderContext = RendererContext<void> & { readonly settings: RenderOptions; readonly onDidChangeSettings: Event<RenderOptions> };
@@ -41,6 +42,7 @@ export type OutputElementOptions = {
4142
scrollable?: boolean;
4243
error?: boolean;
4344
trustHtml?: boolean;
45+
linkifyFilePaths: boolean;
4446
};
4547

4648
export interface OutputWithAppend extends OutputItem {

extensions/notebook-renderers/src/test/linkify.test.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,27 +15,32 @@ suite('Notebook builtin output link detection', () => {
1515
LinkDetector.injectedHtmlCreator = (value: string) => value;
1616

1717
test('no links', () => {
18-
const htmlWithLinks = linkify('hello', true, undefined, true);
18+
const htmlWithLinks = linkify('hello', { linkifyFilePaths: true, trustHtml: true }, true);
1919
assert.equal(htmlWithLinks.innerHTML, 'hello');
2020
});
2121

2222
test('web link detection', () => {
23-
const htmlWithLinks = linkify('something www.example.com something', true, undefined, true);
23+
const htmlWithLinks = linkify('something www.example.com something', { linkifyFilePaths: true, trustHtml: true }, true);
24+
const htmlWithLinks2 = linkify('something www.example.com something', { linkifyFilePaths: false, trustHtml: false }, true);
2425

2526
assert.equal(htmlWithLinks.innerHTML, 'something <a href="www.example.com">www.example.com</a> something');
2627
assert.equal(htmlWithLinks.textContent, 'something www.example.com something');
28+
assert.equal(htmlWithLinks2.innerHTML, 'something <a href="www.example.com">www.example.com</a> something');
29+
assert.equal(htmlWithLinks2.textContent, 'something www.example.com something');
2730
});
2831

2932
test('html link detection', () => {
30-
const htmlWithLinks = linkify('something <a href="www.example.com">link</a> something', true, undefined, true);
33+
const htmlWithLinks = linkify('something <a href="www.example.com">link</a> something', { linkifyFilePaths: true, trustHtml: true }, true);
34+
const htmlWithLinks2 = linkify('something <a href="www.example.com">link</a> something', { linkifyFilePaths: false, trustHtml: true }, true);
3135

3236
assert.equal(htmlWithLinks.innerHTML, 'something <span><a href="www.example.com">link</a></span> something');
3337
assert.equal(htmlWithLinks.textContent, 'something link something');
38+
assert.equal(htmlWithLinks2.innerHTML, 'something <span><a href="www.example.com">link</a></span> something');
39+
assert.equal(htmlWithLinks2.textContent, 'something link something');
3440
});
3541

3642
test('html link without trust', () => {
37-
const trustHtml = false;
38-
const htmlWithLinks = linkify('something <a href="file.py">link</a> something', true, undefined, trustHtml);
43+
const htmlWithLinks = linkify('something <a href="file.py">link</a> something', { linkifyFilePaths: true, trustHtml: false }, true);
3944

4045
assert.equal(htmlWithLinks.innerHTML, 'something &lt;a href="file.py"&gt;link&lt;/a&gt; something');
4146
assert.equal(htmlWithLinks.textContent, 'something <a href="file.py">link</a> something');

extensions/notebook-renderers/src/test/notebookRenderer.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,36 @@ suite('Notebook builtin output renderer', () => {
273273
assert.ok(inserted.innerHTML.indexOf('shouldBeTruncated') === -1, `Beginning content should be truncated`);
274274
});
275275

276+
test(`Render filepath links in text output when enabled`, async () => {
277+
LinkDetector.injectedHtmlCreator = (value: string) => value;
278+
const context = createContext({ outputWordWrap: true, outputScrolling: true, linkifyFilePaths: true });
279+
const renderer = await activate(context);
280+
assert.ok(renderer, 'Renderer not created');
281+
282+
const outputElement = new OutputHtml().getFirstOuputElement();
283+
const outputItem = createOutputItem('./dir/file.txt', stdoutMimeType);
284+
await renderer!.renderOutputItem(outputItem, outputElement);
285+
286+
const inserted = outputElement.firstChild as HTMLElement;
287+
assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`);
288+
assert.ok(outputElement.innerHTML.indexOf('<a href="./dir/file.txt">') !== -1, `inner HTML:\n ${outputElement.innerHTML}`);
289+
});
290+
291+
test(`No filepath links in text output when disabled`, async () => {
292+
LinkDetector.injectedHtmlCreator = (value: string) => value;
293+
const context = createContext({ outputWordWrap: true, outputScrolling: true, linkifyFilePaths: false });
294+
const renderer = await activate(context);
295+
assert.ok(renderer, 'Renderer not created');
296+
297+
const outputElement = new OutputHtml().getFirstOuputElement();
298+
const outputItem = createOutputItem('./dir/file.txt', stdoutMimeType);
299+
await renderer!.renderOutputItem(outputItem, outputElement);
300+
301+
const inserted = outputElement.firstChild as HTMLElement;
302+
assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`);
303+
assert.ok(outputElement.innerHTML.indexOf('<a href="./dir/file.txt">') === -1, `inner HTML:\n ${outputElement.innerHTML}`);
304+
});
305+
276306
test(`Render with wordwrap and scrolling for error output`, async () => {
277307
LinkDetector.injectedHtmlCreator = (value: string) => value;
278308
const context = createContext({ outputWordWrap: true, outputScrolling: true });
@@ -474,7 +504,6 @@ suite('Notebook builtin output renderer', () => {
474504

475505
const inserted = outputElement.firstChild as HTMLElement;
476506
assert.ok(inserted, `nothing appended to output element: ${outputElement.innerHTML}`);
477-
//assert.ok(false, `TextContent:\n ${outputElement.textContent}`);
478507
assert.ok(outputElement.innerHTML.indexOf('class="code-background-colored"') === -1, `inner HTML:\n ${outputElement.innerHTML}`);
479508
});
480509

extensions/notebook-renderers/src/textHelper.ts

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { handleANSIOutput } from './ansi';
7+
import { LinkOptions } from './linkify';
78
import { OutputElementOptions, OutputWithAppend } from './rendererTypes';
89
export const scrollableClass = 'scrollable';
910

@@ -68,44 +69,44 @@ function generateNestedViewAllElement(outputId: string) {
6869
return container;
6970
}
7071

71-
function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number, trustHtml: boolean) {
72+
function truncatedArrayOfString(id: string, buffer: string[], linesLimit: number, linkOptions: LinkOptions) {
7273
const container = document.createElement('div');
7374
const lineCount = buffer.length;
7475

7576
if (lineCount <= linesLimit) {
76-
const spanElement = handleANSIOutput(buffer.join('\n'), trustHtml);
77+
const spanElement = handleANSIOutput(buffer.join('\n'), linkOptions);
7778
container.appendChild(spanElement);
7879
return container;
7980
}
8081

81-
container.appendChild(handleANSIOutput(buffer.slice(0, linesLimit - 5).join('\n'), trustHtml));
82+
container.appendChild(handleANSIOutput(buffer.slice(0, linesLimit - 5).join('\n'), linkOptions));
8283

8384
// truncated piece
8485
const elipses = document.createElement('div');
8586
elipses.innerText = '...';
8687
container.appendChild(elipses);
8788

88-
container.appendChild(handleANSIOutput(buffer.slice(lineCount - 5).join('\n'), trustHtml));
89+
container.appendChild(handleANSIOutput(buffer.slice(lineCount - 5).join('\n'), linkOptions));
8990

9091
container.appendChild(generateViewMoreElement(id));
9192

9293
return container;
9394
}
9495

95-
function scrollableArrayOfString(id: string, buffer: string[], trustHtml: boolean) {
96+
function scrollableArrayOfString(id: string, buffer: string[], linkOptions: LinkOptions) {
9697
const element = document.createElement('div');
9798
if (buffer.length > softScrollableLineLimit) {
9899
element.appendChild(generateNestedViewAllElement(id));
99100
}
100101

101-
element.appendChild(handleANSIOutput(buffer.slice(-1 * softScrollableLineLimit).join('\n'), trustHtml));
102+
element.appendChild(handleANSIOutput(buffer.slice(-1 * softScrollableLineLimit).join('\n'), linkOptions));
102103

103104
return element;
104105
}
105106

106107
const outputLengths: Record<string, number> = {};
107108

108-
function appendScrollableOutput(element: HTMLElement, id: string, appended: string, trustHtml: boolean) {
109+
function appendScrollableOutput(element: HTMLElement, id: string, appended: string, linkOptions: LinkOptions) {
109110
if (!outputLengths[id]) {
110111
outputLengths[id] = 0;
111112
}
@@ -117,22 +118,23 @@ function appendScrollableOutput(element: HTMLElement, id: string, appended: stri
117118
return false;
118119
}
119120
else {
120-
element.appendChild(handleANSIOutput(buffer.join('\n'), trustHtml));
121+
element.appendChild(handleANSIOutput(buffer.join('\n'), linkOptions));
121122
outputLengths[id] = appendedLength;
122123
}
123124
return true;
124125
}
125126

126127
export function createOutputContent(id: string, outputText: string, options: OutputElementOptions): HTMLElement {
127-
const { linesLimit, error, scrollable, trustHtml } = options;
128+
const { linesLimit, error, scrollable, trustHtml, linkifyFilePaths } = options;
129+
const linkOptions: LinkOptions = { linkifyFilePaths, trustHtml };
128130
const buffer = outputText.split(/\r\n|\r|\n/g);
129131
outputLengths[id] = outputLengths[id] = Math.min(buffer.length, softScrollableLineLimit);
130132

131133
let outputElement: HTMLElement;
132134
if (scrollable) {
133-
outputElement = scrollableArrayOfString(id, buffer, !!trustHtml);
135+
outputElement = scrollableArrayOfString(id, buffer, linkOptions);
134136
} else {
135-
outputElement = truncatedArrayOfString(id, buffer, linesLimit, !!trustHtml);
137+
outputElement = truncatedArrayOfString(id, buffer, linesLimit, linkOptions);
136138
}
137139

138140
outputElement.setAttribute('output-item-id', id);
@@ -145,9 +147,10 @@ export function createOutputContent(id: string, outputText: string, options: Out
145147

146148
export function appendOutput(outputInfo: OutputWithAppend, existingContent: HTMLElement, options: OutputElementOptions) {
147149
const appendedText = outputInfo.appendedText?.();
150+
const linkOptions = { linkifyFilePaths: options.linkifyFilePaths, trustHtml: options.trustHtml };
148151
// appending output only supported for scrollable ouputs currently
149152
if (appendedText && options.scrollable) {
150-
if (appendScrollableOutput(existingContent, outputInfo.id, appendedText, false)) {
153+
if (appendScrollableOutput(existingContent, outputInfo.id, appendedText, linkOptions)) {
151154
return;
152155
}
153156
}

src/vs/workbench/contrib/notebook/browser/notebook.contribution.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -922,6 +922,12 @@ configurationRegistry.registerConfiguration({
922922
tags: ['notebookLayout', 'notebookOutputLayout'],
923923
minimum: 1,
924924
},
925+
[NotebookSetting.LinkifyOutputFilePaths]: {
926+
description: nls.localize('notebook.disableOutputFilePathLinks', "Control whether to disable filepath links in the output of notebook cells."),
927+
type: 'boolean',
928+
default: true,
929+
tags: ['notebookOutputLayout']
930+
},
925931
[NotebookSetting.markupFontSize]: {
926932
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#`'),
927933
type: 'number',

0 commit comments

Comments
 (0)