mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-24 20:26:08 +00:00
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:
@@ -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(' ');
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 <a href="file.py">link</a> something');
|
||||
assert.equal(htmlWithLinks.textContent, 'something <a href="file.py">link</a> something');
|
||||
|
||||
@@ -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}`);
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user