未验证 提交 bcbf1cae 编写于 作者: I Isidor Nikolic 提交者: GitHub

Merge pull request #70935 from iansan5653/master

Add support for 8- and 24-bit ANSI escape color codes in the debug console
......@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
import { RGBA, Color } from 'vs/base/common/color';
/**
* @param text The content to stylize.
......@@ -15,6 +16,8 @@ export function handleANSIOutput(text: string, linkDetector: LinkDetector): HTML
const textLength: number = text.length;
let styleNames: string[] = [];
let customFgColor: RGBA | undefined;
let customBgColor: RGBA | undefined;
let currentPos: number = 0;
let buffer: string = '';
......@@ -48,45 +51,33 @@ export function handleANSIOutput(text: string, linkDetector: LinkDetector): HTML
if (sequenceFound) {
// Flush buffer with previous styles.
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector);
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, customFgColor, customBgColor);
buffer = '';
/*
* Certain ranges that are matched here do not contain real graphics rendition sequences. For
* the sake of having a simpler expression, they have been included anyway.
*/
if (ansiSequence.match(/^(?:[349][0-7]|10[0-7]|[013]|4|[34]9)(?:;(?:[349][0-7]|10[0-7]|[013]|4|[34]9))*;?m$/)) {
const styleCodes: number[] = ansiSequence.slice(0, -1) // Remove final 'm' character.
.split(';') // Separate style codes.
.filter(elem => elem !== '') // Filter empty elems as '34;m' -> ['34', ''].
.map(elem => parseInt(elem, 10)); // Convert to numbers.
for (let code of styleCodes) {
if (code === 0) {
styleNames = [];
} else if (code === 1) {
styleNames.push('code-bold');
} else if (code === 3) {
styleNames.push('code-italic');
} else if (code === 4) {
styleNames.push('code-underline');
} else if (code === 39 || (code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {
// Remove all previous foreground colour codes
styleNames = styleNames.filter(style => !style.match(/^code-foreground-\d+$/));
if (code !== 39) {
styleNames.push('code-foreground-' + code);
}
} else if (code === 49 || (code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {
// Remove all previous background colour codes
styleNames = styleNames.filter(style => !style.match(/^code-background-\d+$/));
if (code !== 49) {
styleNames.push('code-background-' + code);
}
* Certain ranges that are matched here do not contain real graphics rendition sequences. For
* the sake of having a simpler expression, they have been included anyway.
*/
if (ansiSequence.match(/^(?:[34][0-8]|9[0-7]|10[0-7]|[013]|4|[34]9)(?:;[349][0-7]|10[0-7]|[013]|[245]|[34]9)?(?:;[012]?[0-9]?[0-9])*;?m$/)) {
const styleCodes: number[] = ansiSequence.slice(0, -1) // Remove final 'm' character.
.split(';') // Separate style codes.
.filter(elem => elem !== '') // Filter empty elems as '34;m' -> ['34', ''].
.map(elem => parseInt(elem, 10)); // Convert to numbers.
if (styleCodes[0] === 38 || styleCodes[0] === 48) {
// Advanced color code - can't be combined with formatting codes like simple colors can
// Ignores invalid colors and additional info beyond what is necessary
const colorType = (styleCodes[0] === 38) ? 'foreground' : 'background';
if (styleCodes[1] === 5) {
set8BitColor(styleCodes, colorType);
} else if (styleCodes[1] === 2) {
set24BitColor(styleCodes, colorType);
}
} else {
setBasicFormatters(styleCodes);
}
} else {
......@@ -96,23 +87,121 @@ export function handleANSIOutput(text: string, linkDetector: LinkDetector): HTML
} else {
currentPos = startPos;
}
}
if (sequenceFound === false) {
buffer += text.charAt(currentPos);
currentPos++;
}
}
// Flush remaining text buffer if not empty.
if (buffer) {
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector);
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, customFgColor, customBgColor);
}
return root;
/**
* Change the foreground or background color by clearing the current color
* and adding the new one.
* @param newClass If string or number, new class will be
* `code-(foreground or background)-newClass`. If `undefined`, no new class
* will be added.
* @param colorType If `'foreground'`, will change the foreground color, if
* `'background'`, will change the background color.
* @param customColor If provided, this custom color will be used instead of
* a class-defined color.
*/
function changeColor(newClass: string | number | undefined, colorType: 'foreground' | 'background', customColor?: RGBA): void {
styleNames = styleNames.filter(style => !style.match(new RegExp(`^code-${colorType}-(\\d+|custom)$`)));
if (newClass) {
styleNames.push(`code-${colorType}-${newClass}`);
}
if (colorType === 'foreground') {
customFgColor = customColor;
} else {
customBgColor = customColor;
}
}
/**
* Calculate and set basic ANSI formatting. Supports bold, italic, underline,
* normal foreground and background colors, and bright foreground and
* background colors. Not to be used for codes containing advanced colors.
* Will ignore invalid codes.
* @param styleCodes Array of ANSI basic styling numbers, which will be
* applied in order. New colors and backgrounds clear old ones; new formatting
* does not.
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code }
*/
function setBasicFormatters(styleCodes: number[]): void {
for (let code of styleCodes) {
if (code === 0) {
styleNames = [];
} else if (code === 1) {
styleNames.push('code-bold');
} else if (code === 3) {
styleNames.push('code-italic');
} else if (code === 4) {
styleNames.push('code-underline');
} else if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {
changeColor(code, 'foreground');
} else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {
changeColor(code, 'background');
} else if (code === 39) {
changeColor(undefined, 'foreground');
} else if (code === 49) {
changeColor(undefined, 'background');
}
}
}
/**
* Calculate and set styling for complicated 24-bit ANSI color codes.
* @param styleCodes Full list of integer codes that make up the full ANSI
* sequence, including the two defining codes and the three RGB codes.
* @param colorType If `'foreground'`, will set foreground color, if
* `'background'`, will set background color.
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit }
*/
function set24BitColor(styleCodes: number[], colorType: 'foreground' | 'background'): void {
if (styleCodes.length >= 5 &&
styleCodes[2] >= 0 && styleCodes[2] <= 255 &&
styleCodes[3] >= 0 && styleCodes[3] <= 255 &&
styleCodes[4] >= 0 && styleCodes[4] <= 255) {
const customColor = new RGBA(styleCodes[2], styleCodes[3], styleCodes[4]);
changeColor('custom', colorType, customColor);
}
}
/**
* Calculate and set styling for advanced 8-bit ANSI color codes.
* @param styleCodes Full list of integer codes that make up the ANSI
* sequence, including the two defining codes and the one color code.
* @param colorType If `'foreground'`, will set foreground color, if
* `'background'`, will set background color.
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit }
*/
function set8BitColor(styleCodes: number[], colorType: 'foreground' | 'background'): void {
let colorNumber = styleCodes[2];
const color = calcANSI8bitColor(colorNumber);
if (color) {
changeColor('custom', colorType, color);
} else if (colorNumber >= 0 && colorNumber <= 15) {
// Need to map to one of the four basic color ranges (30-37, 90-97, 40-47, 100-107)
colorNumber += 30;
if (colorNumber >= 38) {
// Bright colors
colorNumber += 52;
}
if (colorType === 'background') {
colorNumber += 10;
}
changeColor(colorNumber, colorType);
}
}
}
/**
......@@ -120,8 +209,17 @@ export function handleANSIOutput(text: string, linkDetector: LinkDetector): HTML
* @param stringContent The text content to be appended.
* @param cssClasses The list of CSS styles to apply to the text content.
* @param linkDetector The {@link LinkDetector} responsible for generating links from {@param stringContent}.
* @param customTextColor If provided, will apply custom color with inline style.
* @param customBackgroundColor If provided, will apply custom color with inline style.
*/
export function appendStylizedStringToContainer(root: HTMLElement, stringContent: string, cssClasses: string[], linkDetector: LinkDetector): void {
export function appendStylizedStringToContainer(
root: HTMLElement,
stringContent: string,
cssClasses: string[],
linkDetector: LinkDetector,
customTextColor?: RGBA,
customBackgroundColor?: RGBA
): void {
if (!root || !stringContent) {
return;
}
......@@ -129,5 +227,53 @@ export function appendStylizedStringToContainer(root: HTMLElement, stringContent
const container = linkDetector.handleLinks(stringContent);
container.className = cssClasses.join(' ');
if (customTextColor) {
container.style.color =
Color.Format.CSS.formatRGB(new Color(customTextColor));
}
if (customBackgroundColor) {
container.style.backgroundColor =
Color.Format.CSS.formatRGB(new Color(customBackgroundColor));
}
root.appendChild(container);
}
/**
* Calculate the color from the color set defined in the ANSI 8-bit standard.
* Standard and high intensity colors are not defined in the standard as specific
* colors, so these and invalid colors return `undefined`.
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit } for info.
* @param colorNumber The number (ranging from 16 to 255) referring to the color
* desired.
*/
export function calcANSI8bitColor(colorNumber: number): RGBA | undefined {
if (colorNumber % 1 !== 0) {
// Should be integer
return;
} if (colorNumber >= 16 && colorNumber <= 231) {
// Converts to one of 216 RGB colors
colorNumber -= 16;
let blue: number = colorNumber % 6;
colorNumber = (colorNumber - blue) / 6;
let green: number = colorNumber % 6;
colorNumber = (colorNumber - green) / 6;
let red: number = colorNumber;
// red, green, blue now range on [0, 5], need to map to [0,255]
const convFactor: number = 255 / 5;
blue = Math.round(blue * convFactor);
green = Math.round(green * convFactor);
red = Math.round(red * convFactor);
return new RGBA(red, green, blue);
} else if (colorNumber >= 232 && colorNumber <= 255) {
// Converts to a grayscale value
colorNumber -= 232;
const colorLevel: number = Math.round(colorNumber / 23 * 255);
return new RGBA(colorLevel, colorLevel, colorLevel);
} else {
return;
}
}
\ No newline at end of file
......@@ -6,10 +6,11 @@
import * as assert from 'assert';
import * as dom from 'vs/base/browser/dom';
import { generateUuid } from 'vs/base/common/uuid';
import { appendStylizedStringToContainer, handleANSIOutput } from 'vs/workbench/contrib/debug/browser/debugANSIHandling';
import { appendStylizedStringToContainer, handleANSIOutput, calcANSI8bitColor } from 'vs/workbench/contrib/debug/browser/debugANSIHandling';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { workbenchInstantiationService } from 'vs/workbench/test/workbenchTestServices';
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
import { Color, RGBA } from 'vs/base/common/color';
suite('Debug - ANSI Handling', () => {
......@@ -182,6 +183,119 @@ suite('Debug - ANSI Handling', () => {
});
test('Expected single 8-bit color sequence operation', () => {
// Basic color codes specified with 8-bit color code format
for (let i = 0; i <= 7; i++) {
// Foreground codes should add standard classes
const fgStyle: string = 'code-foreground-' + (i + 30);
assertSingleSequenceElement('\x1b[38;5;' + i + 'm', (child) => {
assert(dom.hasClass(child, fgStyle));
});
// Background codes should add standard classes
const bgStyle: string = 'code-background-' + (i + 40);
assertSingleSequenceElement('\x1b[48;5;' + i + 'm', (child) => {
assert(dom.hasClass(child, bgStyle));
});
}
// Bright color codes specified with 8-bit color code format
for (let i = 8; i <= 15; i++) {
// Foreground codes should add standard classes
const fgStyle: string = 'code-foreground-' + (i - 8 + 90);
assertSingleSequenceElement('\x1b[38;5;' + i + 'm', (child) => {
assert(dom.hasClass(child, fgStyle));
});
// Background codes should add standard classes
const bgStyle: string = 'code-background-' + (i - 8 + 100);
assertSingleSequenceElement('\x1b[48;5;' + i + 'm', (child) => {
assert(dom.hasClass(child, bgStyle));
});
}
// 8-bit advanced colors
for (let i = 16; i <= 255; i++) {
const color = Color.Format.CSS.formatRGB(
new Color((calcANSI8bitColor(i) as RGBA))
);
// Foreground codes should add custom class and inline style
assertSingleSequenceElement('\x1b[38;5;' + i + 'm', (child) => {
assert(dom.hasClass(child, 'code-foreground-custom'));
assert(child.style.color === color);
});
// Background codes should add custom class and inline style
assertSingleSequenceElement('\x1b[48;5;' + i + 'm', (child) => {
assert(dom.hasClass(child, 'code-background-custom'));
assert(child.style.backgroundColor === color);
});
}
// Bad (nonexistent) color should not render
assertSingleSequenceElement('\x1b[48;5;300m', (child) => {
assert.equal(0, child.classList.length);
});
// Should ignore any codes after the ones needed to determine color
assertSingleSequenceElement('\x1b[48;5;100;42;77;99;4;24m', (child) => {
const color = Color.Format.CSS.formatRGB(
new Color((calcANSI8bitColor(100) as RGBA))
);
assert(dom.hasClass(child, 'code-background-custom'));
assert.equal(1, child.classList.length);
assert(child.style.backgroundColor === color);
});
});
test('Expected single 24-bit color sequence operation', () => {
// 24-bit advanced colors
for (let r = 0; r <= 255; r += 64) {
for (let g = 0; g <= 255; g += 64) {
for (let b = 0; b <= 255; b += 64) {
let cssColor: string = `rgb(${r},${g},${b})`;
// Foreground codes should add custom class and inline style
assertSingleSequenceElement(`\x1b[38;2;${r};${g};${b}m`, (child) => {
assert(dom.hasClass(child, 'code-foreground-custom'), 'DOM should have "code-foreground-custom" class for advanced ANSI colors.');
const styleBefore = child.style.color;
child.style.color = cssColor;
assert(styleBefore === child.style.color, `Incorrect inline color style found for ${cssColor} (found color: ${styleBefore}).`);
});
// Background codes should add custom class and inline style
assertSingleSequenceElement(`\x1b[48;2;${r};${g};${b}m`, (child) => {
assert(dom.hasClass(child, 'code-background-custom'), 'DOM should have "code-foreground-custom" class for advanced ANSI colors.');
const styleBefore = child.style.backgroundColor;
child.style.backgroundColor = cssColor;
assert(styleBefore === child.style.backgroundColor, `Incorrect inline color style found for ${cssColor} (found color: ${styleBefore}).`);
});
}
}
}
// Invalid color should not render
assertSingleSequenceElement('\x1b[38;2;4;4m', (child) => {
assert.equal(0, child.classList.length, `Invalid color code "38;2;4;4" should not add a class (classes found: ${child.classList}).`);
assert(child.style.color === '', `Invalid color code "38;2;4;4" should not add a custom color CSS (found color: ${child.style.color}).`);
});
// Bad (nonexistent) color should not render
assertSingleSequenceElement('\x1b[48;2;150;300;5m', (child) => {
assert.equal(0, child.classList.length, `Nonexistent color code "48;2;150;300;5" should not add a class (classes found: ${child.classList}).`);
});
// Should ignore any codes after the ones needed to determine color
assertSingleSequenceElement('\x1b[48;2;100;42;77;99;200;75m', (child) => {
assert(dom.hasClass(child, 'code-background-custom'), `Color code with extra (valid) items "48;2;100;42;77;99;200;75" should still treat initial part as valid code and add class "code-background-custom".`);
assert.equal(1, child.classList.length, `Color code with extra items "48;2;100;42;77;99;200;75" should add one and only one class. (classes found: ${child.classList}).`);
let styleBefore = child.style.backgroundColor;
child.style.backgroundColor = 'rgb(100,42,77)';
assert(child.style.backgroundColor === styleBefore, `Color code "48;2;100;42;77;99;200;75" should style background-color as rgb(100,42,77).`);
});
});
/**
* Assert that a given ANSI sequence produces the expected number of {@link HTMLSpanElement} children. For
* each child, run the provided assertion.
......@@ -189,10 +303,13 @@ suite('Debug - ANSI Handling', () => {
* @param sequence The ANSI sequence to verify.
* @param assertions A set of assertions to run on the resulting children.
*/
function assertMultipleSequenceElements(sequence: string, assertions: Array<(child: HTMLSpanElement) => void>): void {
function assertMultipleSequenceElements(sequence: string, assertions: Array<(child: HTMLSpanElement) => void>, elementsExpected?: number): void {
if (elementsExpected === undefined) {
elementsExpected = assertions.length;
}
const root: HTMLSpanElement = handleANSIOutput(sequence, linkDetector);
assert.equal(assertions.length, root.children.length);
for (let i = 0; i < assertions.length; i++) {
assert.equal(elementsExpected, root.children.length);
for (let i = 0; i < elementsExpected; i++) {
const child: Node = root.children[i];
if (child instanceof HTMLSpanElement) {
assertions[i](child);
......@@ -239,7 +356,31 @@ suite('Debug - ANSI Handling', () => {
(nothing) => {
assert.equal(0, nothing.classList.length);
},
]);
], 5);
// Different types of color codes still cancel each other
assertMultipleSequenceElements('\x1b[34msimple\x1b[38;2;100;100;100m24bit\x1b[38;5;3m8bitsimple\x1b[38;5;101m8bitadvanced', [
(simple) => {
assert.equal(1, simple.classList.length);
assert(dom.hasClass(simple, 'code-foreground-34'));
},
(adv24Bit) => {
assert.equal(1, adv24Bit.classList.length);
assert(dom.hasClass(adv24Bit, 'code-foreground-custom'));
let styleBefore = adv24Bit.style.color;
adv24Bit.style.color = 'rgb(100,100,100)';
assert(adv24Bit.style.color === styleBefore);
},
(adv8BitSimple) => {
assert.equal(1, adv8BitSimple.classList.length);
assert(dom.hasClass(adv8BitSimple, 'code-foreground-33'));
assert(adv8BitSimple.style.color === '');
},
(adv8BitAdvanced) => {
assert.equal(1, adv8BitAdvanced.classList.length);
assert(dom.hasClass(adv8BitAdvanced, 'code-foreground-custom'));
}
], 4);
});
......@@ -292,8 +433,6 @@ suite('Debug - ANSI Handling', () => {
'\x1b[;m',
'\x1b[1;;m',
'\x1b[m',
// Unsupported colour codes
'\x1b[30;50m',
'\x1b[99m'
];
......@@ -310,4 +449,40 @@ suite('Debug - ANSI Handling', () => {
});
test('calcANSI8bitColor', () => {
// Invalid values
// Negative (below range), simple range, decimals
for (let i = -10; i <= 15; i += 0.5) {
assert(calcANSI8bitColor(i) === undefined, 'Values less than 16 passed to calcANSI8bitColor should return undefined.');
}
// In-range range decimals
for (let i = 16.5; i < 254; i += 1) {
assert(calcANSI8bitColor(i) === undefined, 'Floats passed to calcANSI8bitColor should return undefined.');
}
// Above range
for (let i = 256; i < 300; i += 0.5) {
assert(calcANSI8bitColor(i) === undefined, 'Values grather than 255 passed to calcANSI8bitColor should return undefined.');
}
// All valid colors
for (let red = 0; red <= 5; red++) {
for (let green = 0; green <= 5; green++) {
for (let blue = 0; blue <= 5; blue++) {
let colorOut: any = calcANSI8bitColor(16 + red * 36 + green * 6 + blue);
assert(colorOut.r === Math.round(red * (255 / 5)), 'Incorrect red value encountered for color');
assert(colorOut.g === Math.round(green * (255 / 5)), 'Incorrect green value encountered for color');
assert(colorOut.b === Math.round(blue * (255 / 5)), 'Incorrect balue value encountered for color');
}
}
}
// All grays
for (let i = 232; i <= 255; i++) {
let grayOut: any = calcANSI8bitColor(i);
assert(grayOut.r === grayOut.g);
assert(grayOut.r === grayOut.b);
assert(grayOut.r === Math.round((i - 232) / 23 * 255));
}
});
});
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册