Path: blob/main/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { IHighlight } from '../../../../base/browser/ui/highlightedlabel/highlightedLabel.js';6import { Color, RGBA } from '../../../../base/common/color.js';7import { isDefined } from '../../../../base/common/types.js';8import { editorHoverBackground, listActiveSelectionBackground, listFocusBackground, listInactiveFocusBackground, listInactiveSelectionBackground } from '../../../../platform/theme/common/colorRegistry.js';9import { registerThemingParticipant } from '../../../../platform/theme/common/themeService.js';10import { IWorkspaceFolder } from '../../../../platform/workspace/common/workspace.js';11import { PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from '../../../common/theme.js';12import { ansiColorIdentifiers } from '../../terminal/common/terminalColorRegistry.js';13import { ILinkDetector } from './linkDetector.js';1415/**16* @param text The content to stylize.17* @returns An {@link HTMLSpanElement} that contains the potentially stylized text.18*/19export function handleANSIOutput(text: string, linkDetector: ILinkDetector, workspaceFolder: IWorkspaceFolder | undefined, highlights: IHighlight[] | undefined): HTMLSpanElement {2021const root: HTMLSpanElement = document.createElement('span');22const textLength: number = text.length;2324let styleNames: string[] = [];25let customFgColor: RGBA | string | undefined;26let customBgColor: RGBA | string | undefined;27let customUnderlineColor: RGBA | string | undefined;28let colorsInverted: boolean = false;29let currentPos: number = 0;30let unprintedChars = 0;31let buffer: string = '';3233while (currentPos < textLength) {3435let sequenceFound: boolean = false;3637// Potentially an ANSI escape sequence.38// See http://ascii-table.com/ansi-escape-sequences.php & https://en.wikipedia.org/wiki/ANSI_escape_code39if (text.charCodeAt(currentPos) === 27 && text.charAt(currentPos + 1) === '[') {4041const startPos: number = currentPos;42currentPos += 2; // Ignore 'Esc[' as it's in every sequence.4344let ansiSequence: string = '';4546while (currentPos < textLength) {47const char: string = text.charAt(currentPos);48ansiSequence += char;4950currentPos++;5152// Look for a known sequence terminating character.53if (char.match(/^[ABCDHIJKfhmpsu]$/)) {54sequenceFound = true;55break;56}5758}5960if (sequenceFound) {6162unprintedChars += 2 + ansiSequence.length;6364// Flush buffer with previous styles.65appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor, highlights, currentPos - buffer.length - unprintedChars);6667buffer = '';6869/*70* Certain ranges that are matched here do not contain real graphics rendition sequences. For71* the sake of having a simpler expression, they have been included anyway.72*/73if (ansiSequence.match(/^(?:[34][0-8]|9[0-7]|10[0-7]|[0-9]|2[1-5,7-9]|[34]9|5[8,9]|1[0-9])(?:;[349][0-7]|10[0-7]|[013]|[245]|[34]9)?(?:;[012]?[0-9]?[0-9])*;?m$/)) {7475const styleCodes: number[] = ansiSequence.slice(0, -1) // Remove final 'm' character.76.split(';') // Separate style codes.77.filter(elem => elem !== '') // Filter empty elems as '34;m' -> ['34', ''].78.map(elem => parseInt(elem, 10)); // Convert to numbers.7980if (styleCodes[0] === 38 || styleCodes[0] === 48 || styleCodes[0] === 58) {81// Advanced color code - can't be combined with formatting codes like simple colors can82// Ignores invalid colors and additional info beyond what is necessary83const colorType = (styleCodes[0] === 38) ? 'foreground' : ((styleCodes[0] === 48) ? 'background' : 'underline');8485if (styleCodes[1] === 5) {86set8BitColor(styleCodes, colorType);87} else if (styleCodes[1] === 2) {88set24BitColor(styleCodes, colorType);89}90} else {91setBasicFormatters(styleCodes);92}9394} else {95// Unsupported sequence so simply hide it.96}9798} else {99currentPos = startPos;100}101}102103if (sequenceFound === false) {104buffer += text.charAt(currentPos);105currentPos++;106}107}108109// Flush remaining text buffer if not empty.110if (buffer) {111appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor, highlights, currentPos - buffer.length);112}113114return root;115116/**117* Change the foreground or background color by clearing the current color118* and adding the new one.119* @param colorType If `'foreground'`, will change the foreground color, if120* `'background'`, will change the background color, and if `'underline'`121* will set the underline color.122* @param color Color to change to. If `undefined` or not provided,123* will clear current color without adding a new one.124*/125function changeColor(colorType: 'foreground' | 'background' | 'underline', color?: RGBA | string): void {126if (colorType === 'foreground') {127customFgColor = color;128} else if (colorType === 'background') {129customBgColor = color;130} else if (colorType === 'underline') {131customUnderlineColor = color;132}133styleNames = styleNames.filter(style => style !== `code-${colorType}-colored`);134if (color !== undefined) {135styleNames.push(`code-${colorType}-colored`);136}137}138139/**140* Swap foreground and background colors. Used for color inversion. Caller should check141* [] flag to make sure it is appropriate to turn ON or OFF (if it is already inverted don't call142*/143function reverseForegroundAndBackgroundColors(): void {144const oldFgColor = customFgColor;145changeColor('foreground', customBgColor);146changeColor('background', oldFgColor);147}148149/**150* Calculate and set basic ANSI formatting. Supports ON/OFF of bold, italic, underline,151* double underline, crossed-out/strikethrough, overline, dim, blink, rapid blink,152* reverse/invert video, hidden, superscript, subscript and alternate font codes,153* clearing/resetting of foreground, background and underline colors,154* setting normal foreground and background colors, and bright foreground and155* background colors. Not to be used for codes containing advanced colors.156* Will ignore invalid codes.157* @param styleCodes Array of ANSI basic styling numbers, which will be158* applied in order. New colors and backgrounds clear old ones; new formatting159* does not.160* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#SGR }161*/162function setBasicFormatters(styleCodes: number[]): void {163for (const code of styleCodes) {164switch (code) {165case 0: { // reset (everything)166styleNames = [];167customFgColor = undefined;168customBgColor = undefined;169break;170}171case 1: { // bold172styleNames = styleNames.filter(style => style !== `code-bold`);173styleNames.push('code-bold');174break;175}176case 2: { // dim177styleNames = styleNames.filter(style => style !== `code-dim`);178styleNames.push('code-dim');179break;180}181case 3: { // italic182styleNames = styleNames.filter(style => style !== `code-italic`);183styleNames.push('code-italic');184break;185}186case 4: { // underline187styleNames = styleNames.filter(style => (style !== `code-underline` && style !== `code-double-underline`));188styleNames.push('code-underline');189break;190}191case 5: { // blink192styleNames = styleNames.filter(style => style !== `code-blink`);193styleNames.push('code-blink');194break;195}196case 6: { // rapid blink197styleNames = styleNames.filter(style => style !== `code-rapid-blink`);198styleNames.push('code-rapid-blink');199break;200}201case 7: { // invert foreground and background202if (!colorsInverted) {203colorsInverted = true;204reverseForegroundAndBackgroundColors();205}206break;207}208case 8: { // hidden209styleNames = styleNames.filter(style => style !== `code-hidden`);210styleNames.push('code-hidden');211break;212}213case 9: { // strike-through/crossed-out214styleNames = styleNames.filter(style => style !== `code-strike-through`);215styleNames.push('code-strike-through');216break;217}218case 10: { // normal default font219styleNames = styleNames.filter(style => !style.startsWith('code-font'));220break;221}222case 11: case 12: case 13: case 14: case 15: case 16: case 17: case 18: case 19: case 20: { // font codes (and 20 is 'blackletter' font code)223styleNames = styleNames.filter(style => !style.startsWith('code-font'));224styleNames.push(`code-font-${code - 10}`);225break;226}227case 21: { // double underline228styleNames = styleNames.filter(style => (style !== `code-underline` && style !== `code-double-underline`));229styleNames.push('code-double-underline');230break;231}232case 22: { // normal intensity (bold off and dim off)233styleNames = styleNames.filter(style => (style !== `code-bold` && style !== `code-dim`));234break;235}236case 23: { // Neither italic or blackletter (font 10)237styleNames = styleNames.filter(style => (style !== `code-italic` && style !== `code-font-10`));238break;239}240case 24: { // not underlined (Neither singly nor doubly underlined)241styleNames = styleNames.filter(style => (style !== `code-underline` && style !== `code-double-underline`));242break;243}244case 25: { // not blinking245styleNames = styleNames.filter(style => (style !== `code-blink` && style !== `code-rapid-blink`));246break;247}248case 27: { // not reversed/inverted249if (colorsInverted) {250colorsInverted = false;251reverseForegroundAndBackgroundColors();252}253break;254}255case 28: { // not hidden (reveal)256styleNames = styleNames.filter(style => style !== `code-hidden`);257break;258}259case 29: { // not crossed-out260styleNames = styleNames.filter(style => style !== `code-strike-through`);261break;262}263case 53: { // overlined264styleNames = styleNames.filter(style => style !== `code-overline`);265styleNames.push('code-overline');266break;267}268case 55: { // not overlined269styleNames = styleNames.filter(style => style !== `code-overline`);270break;271}272case 39: { // default foreground color273changeColor('foreground', undefined);274break;275}276case 49: { // default background color277changeColor('background', undefined);278break;279}280case 59: { // default underline color281changeColor('underline', undefined);282break;283}284case 73: { // superscript285styleNames = styleNames.filter(style => (style !== `code-superscript` && style !== `code-subscript`));286styleNames.push('code-superscript');287break;288}289case 74: { // subscript290styleNames = styleNames.filter(style => (style !== `code-superscript` && style !== `code-subscript`));291styleNames.push('code-subscript');292break;293}294case 75: { // neither superscript or subscript295styleNames = styleNames.filter(style => (style !== `code-superscript` && style !== `code-subscript`));296break;297}298default: {299setBasicColor(code);300break;301}302}303}304}305306/**307* Calculate and set styling for complicated 24-bit ANSI color codes.308* @param styleCodes Full list of integer codes that make up the full ANSI309* sequence, including the two defining codes and the three RGB codes.310* @param colorType If `'foreground'`, will set foreground color, if311* `'background'`, will set background color, and if it is `'underline'`312* will set the underline color.313* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit }314*/315function set24BitColor(styleCodes: number[], colorType: 'foreground' | 'background' | 'underline'): void {316if (styleCodes.length >= 5 &&317styleCodes[2] >= 0 && styleCodes[2] <= 255 &&318styleCodes[3] >= 0 && styleCodes[3] <= 255 &&319styleCodes[4] >= 0 && styleCodes[4] <= 255) {320const customColor = new RGBA(styleCodes[2], styleCodes[3], styleCodes[4]);321changeColor(colorType, customColor);322}323}324325/**326* Calculate and set styling for advanced 8-bit ANSI color codes.327* @param styleCodes Full list of integer codes that make up the ANSI328* sequence, including the two defining codes and the one color code.329* @param colorType If `'foreground'`, will set foreground color, if330* `'background'`, will set background color and if it is `'underline'`331* will set the underline color.332* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit }333*/334function set8BitColor(styleCodes: number[], colorType: 'foreground' | 'background' | 'underline'): void {335let colorNumber = styleCodes[2];336const color = calcANSI8bitColor(colorNumber);337338if (color) {339changeColor(colorType, color);340} else if (colorNumber >= 0 && colorNumber <= 15) {341if (colorType === 'underline') {342// for underline colors we just decode the 0-15 color number to theme color, set and return343const colorName = ansiColorIdentifiers[colorNumber];344changeColor(colorType, `--vscode-debug-ansi-${colorName}`);345return;346}347// Need to map to one of the four basic color ranges (30-37, 90-97, 40-47, 100-107)348colorNumber += 30;349if (colorNumber >= 38) {350// Bright colors351colorNumber += 52;352}353if (colorType === 'background') {354colorNumber += 10;355}356setBasicColor(colorNumber);357}358}359360/**361* Calculate and set styling for basic bright and dark ANSI color codes. Uses362* theme colors if available. Automatically distinguishes between foreground363* and background colors; does not support color-clearing codes 39 and 49.364* @param styleCode Integer color code on one of the following ranges:365* [30-37, 90-97, 40-47, 100-107]. If not on one of these ranges, will do366* nothing.367*/368function setBasicColor(styleCode: number): void {369let colorType: 'foreground' | 'background' | undefined;370let colorIndex: number | undefined;371372if (styleCode >= 30 && styleCode <= 37) {373colorIndex = styleCode - 30;374colorType = 'foreground';375} else if (styleCode >= 90 && styleCode <= 97) {376colorIndex = (styleCode - 90) + 8; // High-intensity (bright)377colorType = 'foreground';378} else if (styleCode >= 40 && styleCode <= 47) {379colorIndex = styleCode - 40;380colorType = 'background';381} else if (styleCode >= 100 && styleCode <= 107) {382colorIndex = (styleCode - 100) + 8; // High-intensity (bright)383colorType = 'background';384}385386if (colorIndex !== undefined && colorType) {387const colorName = ansiColorIdentifiers[colorIndex];388changeColor(colorType, `--vscode-debug-ansi-${colorName.replaceAll('.', '-')}`);389}390}391}392393/**394* @param root The {@link HTMLElement} to append the content to.395* @param stringContent The text content to be appended.396* @param cssClasses The list of CSS styles to apply to the text content.397* @param linkDetector The {@link ILinkDetector} responsible for generating links from {@param stringContent}.398* @param customTextColor If provided, will apply custom color with inline style.399* @param customBackgroundColor If provided, will apply custom backgroundColor with inline style.400* @param customUnderlineColor If provided, will apply custom textDecorationColor with inline style.401* @param highlights The ranges to highlight.402* @param offset The starting index of the stringContent in the original text.403*/404export function appendStylizedStringToContainer(405root: HTMLElement,406stringContent: string,407cssClasses: string[],408linkDetector: ILinkDetector,409workspaceFolder: IWorkspaceFolder | undefined,410customTextColor: RGBA | string | undefined,411customBackgroundColor: RGBA | string | undefined,412customUnderlineColor: RGBA | string | undefined,413highlights: IHighlight[] | undefined,414offset: number,415): void {416if (!root || !stringContent) {417return;418}419420const container = linkDetector.linkify(421stringContent,422true,423workspaceFolder,424undefined,425undefined,426highlights?.map(h => ({ start: h.start - offset, end: h.end - offset, extraClasses: h.extraClasses })),427);428429container.className = cssClasses.join(' ');430if (customTextColor) {431container.style.color =432typeof customTextColor === 'string' ? `var(${customTextColor})` : Color.Format.CSS.formatRGB(new Color(customTextColor));433}434if (customBackgroundColor) {435container.style.backgroundColor =436typeof customBackgroundColor === 'string' ? `var(${customBackgroundColor})` : Color.Format.CSS.formatRGB(new Color(customBackgroundColor));437}438if (customUnderlineColor) {439container.style.textDecorationColor =440typeof customUnderlineColor === 'string' ? `var(${customUnderlineColor})` : Color.Format.CSS.formatRGB(new Color(customUnderlineColor));441}442443root.appendChild(container);444}445446/**447* Calculate the color from the color set defined in the ANSI 8-bit standard.448* Standard and high intensity colors are not defined in the standard as specific449* colors, so these and invalid colors return `undefined`.450* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit } for info.451* @param colorNumber The number (ranging from 16 to 255) referring to the color452* desired.453*/454export function calcANSI8bitColor(colorNumber: number): RGBA | undefined {455if (colorNumber % 1 !== 0) {456// Should be integer457return;458} if (colorNumber >= 16 && colorNumber <= 231) {459// Converts to one of 216 RGB colors460colorNumber -= 16;461462let blue: number = colorNumber % 6;463colorNumber = (colorNumber - blue) / 6;464let green: number = colorNumber % 6;465colorNumber = (colorNumber - green) / 6;466let red: number = colorNumber;467468// red, green, blue now range on [0, 5], need to map to [0,255]469const convFactor: number = 255 / 5;470blue = Math.round(blue * convFactor);471green = Math.round(green * convFactor);472red = Math.round(red * convFactor);473474return new RGBA(red, green, blue);475} else if (colorNumber >= 232 && colorNumber <= 255) {476// Converts to a grayscale value477colorNumber -= 232;478const colorLevel: number = Math.round(colorNumber / 23 * 255);479return new RGBA(colorLevel, colorLevel, colorLevel);480} else {481return;482}483}484485registerThemingParticipant((theme, collector) => {486const areas = [487{ selector: '.monaco-workbench .sidebar, .monaco-workbench .auxiliarybar', bg: theme.getColor(SIDE_BAR_BACKGROUND) },488{ selector: '.monaco-workbench .panel', bg: theme.getColor(PANEL_BACKGROUND) },489{ selector: '.monaco-workbench .monaco-list-row.selected', bg: theme.getColor(listInactiveSelectionBackground) },490{ selector: '.monaco-workbench .monaco-list-row.focused', bg: theme.getColor(listInactiveFocusBackground) },491{ selector: '.monaco-workbench .monaco-list:focus .monaco-list-row.focused', bg: theme.getColor(listFocusBackground) },492{ selector: '.monaco-workbench .monaco-list:focus .monaco-list-row.selected', bg: theme.getColor(listActiveSelectionBackground) },493{ selector: '.debug-hover-widget', bg: theme.getColor(editorHoverBackground) },494];495496for (const { selector, bg } of areas) {497const content = ansiColorIdentifiers498.map(color => {499const actual = theme.getColor(color);500if (!actual) { return undefined; }501// this uses the default contrast ratio of 4 (from the terminal),502// we may want to make this configurable in the future, but this is503// good to keep things sane to start with.504return `--vscode-debug-ansi-${color.replaceAll('.', '-')}:${bg ? bg.ensureConstrast(actual, 4) : actual}`;505})506.filter(isDefined);507508collector.addRule(`${selector} { ${content.join(';')} }`);509}510});511512513