Skip to content

Commit bcbf1ca

Browse files
authored
Merge pull request microsoft#70935 from iansan5653/master
Add support for 8- and 24-bit ANSI escape color codes in the debug console
2 parents f2f13ba + 54d3742 commit bcbf1ca

2 files changed

Lines changed: 366 additions & 45 deletions

File tree

src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts

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

66
import { LinkDetector } from 'vs/workbench/contrib/debug/browser/linkDetector';
7+
import { RGBA, Color } from 'vs/base/common/color';
78

89
/**
910
* @param text The content to stylize.
@@ -15,6 +16,8 @@ export function handleANSIOutput(text: string, linkDetector: LinkDetector): HTML
1516
const textLength: number = text.length;
1617

1718
let styleNames: string[] = [];
19+
let customFgColor: RGBA | undefined;
20+
let customBgColor: RGBA | undefined;
1821
let currentPos: number = 0;
1922
let buffer: string = '';
2023

@@ -48,45 +51,33 @@ export function handleANSIOutput(text: string, linkDetector: LinkDetector): HTML
4851
if (sequenceFound) {
4952

5053
// Flush buffer with previous styles.
51-
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector);
54+
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, customFgColor, customBgColor);
5255

5356
buffer = '';
5457

5558
/*
56-
* Certain ranges that are matched here do not contain real graphics rendition sequences. For
57-
* the sake of having a simpler expression, they have been included anyway.
58-
*/
59-
if (ansiSequence.match(/^(?:[349][0-7]|10[0-7]|[013]|4|[34]9)(?:;(?:[349][0-7]|10[0-7]|[013]|4|[34]9))*;?m$/)) {
60-
61-
const styleCodes: number[] = ansiSequence.slice(0, -1) // Remove final 'm' character.
62-
.split(';') // Separate style codes.
63-
.filter(elem => elem !== '') // Filter empty elems as '34;m' -> ['34', ''].
64-
.map(elem => parseInt(elem, 10)); // Convert to numbers.
65-
66-
for (let code of styleCodes) {
67-
if (code === 0) {
68-
styleNames = [];
69-
} else if (code === 1) {
70-
styleNames.push('code-bold');
71-
} else if (code === 3) {
72-
styleNames.push('code-italic');
73-
} else if (code === 4) {
74-
styleNames.push('code-underline');
75-
} else if (code === 39 || (code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {
76-
// Remove all previous foreground colour codes
77-
styleNames = styleNames.filter(style => !style.match(/^code-foreground-\d+$/));
78-
79-
if (code !== 39) {
80-
styleNames.push('code-foreground-' + code);
81-
}
82-
} else if (code === 49 || (code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {
83-
// Remove all previous background colour codes
84-
styleNames = styleNames.filter(style => !style.match(/^code-background-\d+$/));
85-
86-
if (code !== 49) {
87-
styleNames.push('code-background-' + code);
88-
}
59+
* Certain ranges that are matched here do not contain real graphics rendition sequences. For
60+
* the sake of having a simpler expression, they have been included anyway.
61+
*/
62+
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$/)) {
63+
64+
const styleCodes: number[] = ansiSequence.slice(0, -1) // Remove final 'm' character.
65+
.split(';') // Separate style codes.
66+
.filter(elem => elem !== '') // Filter empty elems as '34;m' -> ['34', ''].
67+
.map(elem => parseInt(elem, 10)); // Convert to numbers.
68+
69+
if (styleCodes[0] === 38 || styleCodes[0] === 48) {
70+
// Advanced color code - can't be combined with formatting codes like simple colors can
71+
// Ignores invalid colors and additional info beyond what is necessary
72+
const colorType = (styleCodes[0] === 38) ? 'foreground' : 'background';
73+
74+
if (styleCodes[1] === 5) {
75+
set8BitColor(styleCodes, colorType);
76+
} else if (styleCodes[1] === 2) {
77+
set24BitColor(styleCodes, colorType);
8978
}
79+
} else {
80+
setBasicFormatters(styleCodes);
9081
}
9182

9283
} else {
@@ -96,38 +87,193 @@ export function handleANSIOutput(text: string, linkDetector: LinkDetector): HTML
9687
} else {
9788
currentPos = startPos;
9889
}
99-
10090
}
10191

10292
if (sequenceFound === false) {
10393
buffer += text.charAt(currentPos);
10494
currentPos++;
10595
}
106-
10796
}
10897

10998
// Flush remaining text buffer if not empty.
11099
if (buffer) {
111-
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector);
100+
appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, customFgColor, customBgColor);
112101
}
113102

114103
return root;
115104

105+
/**
106+
* Change the foreground or background color by clearing the current color
107+
* and adding the new one.
108+
* @param newClass If string or number, new class will be
109+
* `code-(foreground or background)-newClass`. If `undefined`, no new class
110+
* will be added.
111+
* @param colorType If `'foreground'`, will change the foreground color, if
112+
* `'background'`, will change the background color.
113+
* @param customColor If provided, this custom color will be used instead of
114+
* a class-defined color.
115+
*/
116+
function changeColor(newClass: string | number | undefined, colorType: 'foreground' | 'background', customColor?: RGBA): void {
117+
styleNames = styleNames.filter(style => !style.match(new RegExp(`^code-${colorType}-(\\d+|custom)$`)));
118+
if (newClass) {
119+
styleNames.push(`code-${colorType}-${newClass}`);
120+
}
121+
if (colorType === 'foreground') {
122+
customFgColor = customColor;
123+
} else {
124+
customBgColor = customColor;
125+
}
126+
}
127+
128+
/**
129+
* Calculate and set basic ANSI formatting. Supports bold, italic, underline,
130+
* normal foreground and background colors, and bright foreground and
131+
* background colors. Not to be used for codes containing advanced colors.
132+
* Will ignore invalid codes.
133+
* @param styleCodes Array of ANSI basic styling numbers, which will be
134+
* applied in order. New colors and backgrounds clear old ones; new formatting
135+
* does not.
136+
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code }
137+
*/
138+
function setBasicFormatters(styleCodes: number[]): void {
139+
for (let code of styleCodes) {
140+
if (code === 0) {
141+
styleNames = [];
142+
} else if (code === 1) {
143+
styleNames.push('code-bold');
144+
} else if (code === 3) {
145+
styleNames.push('code-italic');
146+
} else if (code === 4) {
147+
styleNames.push('code-underline');
148+
} else if ((code >= 30 && code <= 37) || (code >= 90 && code <= 97)) {
149+
changeColor(code, 'foreground');
150+
} else if ((code >= 40 && code <= 47) || (code >= 100 && code <= 107)) {
151+
changeColor(code, 'background');
152+
} else if (code === 39) {
153+
changeColor(undefined, 'foreground');
154+
} else if (code === 49) {
155+
changeColor(undefined, 'background');
156+
}
157+
}
158+
}
159+
160+
/**
161+
* Calculate and set styling for complicated 24-bit ANSI color codes.
162+
* @param styleCodes Full list of integer codes that make up the full ANSI
163+
* sequence, including the two defining codes and the three RGB codes.
164+
* @param colorType If `'foreground'`, will set foreground color, if
165+
* `'background'`, will set background color.
166+
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit }
167+
*/
168+
function set24BitColor(styleCodes: number[], colorType: 'foreground' | 'background'): void {
169+
if (styleCodes.length >= 5 &&
170+
styleCodes[2] >= 0 && styleCodes[2] <= 255 &&
171+
styleCodes[3] >= 0 && styleCodes[3] <= 255 &&
172+
styleCodes[4] >= 0 && styleCodes[4] <= 255) {
173+
const customColor = new RGBA(styleCodes[2], styleCodes[3], styleCodes[4]);
174+
changeColor('custom', colorType, customColor);
175+
}
176+
}
177+
178+
/**
179+
* Calculate and set styling for advanced 8-bit ANSI color codes.
180+
* @param styleCodes Full list of integer codes that make up the ANSI
181+
* sequence, including the two defining codes and the one color code.
182+
* @param colorType If `'foreground'`, will set foreground color, if
183+
* `'background'`, will set background color.
184+
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit }
185+
*/
186+
function set8BitColor(styleCodes: number[], colorType: 'foreground' | 'background'): void {
187+
let colorNumber = styleCodes[2];
188+
const color = calcANSI8bitColor(colorNumber);
189+
190+
if (color) {
191+
changeColor('custom', colorType, color);
192+
} else if (colorNumber >= 0 && colorNumber <= 15) {
193+
// Need to map to one of the four basic color ranges (30-37, 90-97, 40-47, 100-107)
194+
colorNumber += 30;
195+
if (colorNumber >= 38) {
196+
// Bright colors
197+
colorNumber += 52;
198+
}
199+
if (colorType === 'background') {
200+
colorNumber += 10;
201+
}
202+
changeColor(colorNumber, colorType);
203+
}
204+
}
116205
}
117206

118207
/**
119208
* @param root The {@link HTMLElement} to append the content to.
120209
* @param stringContent The text content to be appended.
121210
* @param cssClasses The list of CSS styles to apply to the text content.
122211
* @param linkDetector The {@link LinkDetector} responsible for generating links from {@param stringContent}.
212+
* @param customTextColor If provided, will apply custom color with inline style.
213+
* @param customBackgroundColor If provided, will apply custom color with inline style.
123214
*/
124-
export function appendStylizedStringToContainer(root: HTMLElement, stringContent: string, cssClasses: string[], linkDetector: LinkDetector): void {
215+
export function appendStylizedStringToContainer(
216+
root: HTMLElement,
217+
stringContent: string,
218+
cssClasses: string[],
219+
linkDetector: LinkDetector,
220+
customTextColor?: RGBA,
221+
customBackgroundColor?: RGBA
222+
): void {
125223
if (!root || !stringContent) {
126224
return;
127225
}
128226

129227
const container = linkDetector.handleLinks(stringContent);
130228

131229
container.className = cssClasses.join(' ');
230+
if (customTextColor) {
231+
container.style.color =
232+
Color.Format.CSS.formatRGB(new Color(customTextColor));
233+
}
234+
if (customBackgroundColor) {
235+
container.style.backgroundColor =
236+
Color.Format.CSS.formatRGB(new Color(customBackgroundColor));
237+
}
238+
132239
root.appendChild(container);
133240
}
241+
242+
/**
243+
* Calculate the color from the color set defined in the ANSI 8-bit standard.
244+
* Standard and high intensity colors are not defined in the standard as specific
245+
* colors, so these and invalid colors return `undefined`.
246+
* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit } for info.
247+
* @param colorNumber The number (ranging from 16 to 255) referring to the color
248+
* desired.
249+
*/
250+
export function calcANSI8bitColor(colorNumber: number): RGBA | undefined {
251+
if (colorNumber % 1 !== 0) {
252+
// Should be integer
253+
return;
254+
} if (colorNumber >= 16 && colorNumber <= 231) {
255+
// Converts to one of 216 RGB colors
256+
colorNumber -= 16;
257+
258+
let blue: number = colorNumber % 6;
259+
colorNumber = (colorNumber - blue) / 6;
260+
let green: number = colorNumber % 6;
261+
colorNumber = (colorNumber - green) / 6;
262+
let red: number = colorNumber;
263+
264+
// red, green, blue now range on [0, 5], need to map to [0,255]
265+
const convFactor: number = 255 / 5;
266+
blue = Math.round(blue * convFactor);
267+
green = Math.round(green * convFactor);
268+
red = Math.round(red * convFactor);
269+
270+
return new RGBA(red, green, blue);
271+
} else if (colorNumber >= 232 && colorNumber <= 255) {
272+
// Converts to a grayscale value
273+
colorNumber -= 232;
274+
const colorLevel: number = Math.round(colorNumber / 23 * 255);
275+
return new RGBA(colorLevel, colorLevel, colorLevel);
276+
} else {
277+
return;
278+
}
279+
}

0 commit comments

Comments
 (0)