Path: blob/main/src/vs/workbench/contrib/debug/browser/debugANSIHandling.ts
5240 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 { DebugLinkHoverBehaviorTypeData, 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, hoverBehavior: DebugLinkHoverBehaviorTypeData): 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, hoverBehavior);66buffer = '';6768/*69* Certain ranges that are matched here do not contain real graphics rendition sequences. For70* the sake of having a simpler expression, they have been included anyway.71*/72if (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$/)) {7374const styleCodes: number[] = ansiSequence.slice(0, -1) // Remove final 'm' character.75.split(';') // Separate style codes.76.filter(elem => elem !== '') // Filter empty elems as '34;m' -> ['34', ''].77.map(elem => parseInt(elem, 10)); // Convert to numbers.7879if (styleCodes[0] === 38 || styleCodes[0] === 48 || styleCodes[0] === 58) {80// Advanced color code - can't be combined with formatting codes like simple colors can81// Ignores invalid colors and additional info beyond what is necessary82const colorType = (styleCodes[0] === 38) ? 'foreground' : ((styleCodes[0] === 48) ? 'background' : 'underline');8384if (styleCodes[1] === 5) {85set8BitColor(styleCodes, colorType);86} else if (styleCodes[1] === 2) {87set24BitColor(styleCodes, colorType);88}89} else {90setBasicFormatters(styleCodes);91}9293} else {94// Unsupported sequence so simply hide it.95}9697} else {98currentPos = startPos;99}100}101102if (sequenceFound === false) {103buffer += text.charAt(currentPos);104currentPos++;105}106}107108// Flush remaining text buffer if not empty.109if (buffer) {110appendStylizedStringToContainer(root, buffer, styleNames, linkDetector, workspaceFolder, customFgColor, customBgColor, customUnderlineColor, highlights, currentPos - buffer.length, hoverBehavior);111}112113return root;114115/**116* Change the foreground or background color by clearing the current color117* and adding the new one.118* @param colorType If `'foreground'`, will change the foreground color, if119* `'background'`, will change the background color, and if `'underline'`120* will set the underline color.121* @param color Color to change to. If `undefined` or not provided,122* will clear current color without adding a new one.123*/124function changeColor(colorType: 'foreground' | 'background' | 'underline', color?: RGBA | string): void {125if (colorType === 'foreground') {126customFgColor = color;127} else if (colorType === 'background') {128customBgColor = color;129} else if (colorType === 'underline') {130customUnderlineColor = color;131}132styleNames = styleNames.filter(style => style !== `code-${colorType}-colored`);133if (color !== undefined) {134styleNames.push(`code-${colorType}-colored`);135}136}137138/**139* Swap foreground and background colors. Used for color inversion. Caller should check140* [] flag to make sure it is appropriate to turn ON or OFF (if it is already inverted don't call141*/142function reverseForegroundAndBackgroundColors(): void {143const oldFgColor = customFgColor;144changeColor('foreground', customBgColor);145changeColor('background', oldFgColor);146}147148/**149* Calculate and set basic ANSI formatting. Supports ON/OFF of bold, italic, underline,150* double underline, crossed-out/strikethrough, overline, dim, blink, rapid blink,151* reverse/invert video, hidden, superscript, subscript and alternate font codes,152* clearing/resetting of foreground, background and underline colors,153* setting normal foreground and background colors, and bright foreground and154* background colors. Not to be used for codes containing advanced colors.155* Will ignore invalid codes.156* @param styleCodes Array of ANSI basic styling numbers, which will be157* applied in order. New colors and backgrounds clear old ones; new formatting158* does not.159* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#SGR }160*/161function setBasicFormatters(styleCodes: number[]): void {162for (const code of styleCodes) {163switch (code) {164case 0: { // reset (everything)165styleNames = [];166customFgColor = undefined;167customBgColor = undefined;168break;169}170case 1: { // bold171styleNames = styleNames.filter(style => style !== `code-bold`);172styleNames.push('code-bold');173break;174}175case 2: { // dim176styleNames = styleNames.filter(style => style !== `code-dim`);177styleNames.push('code-dim');178break;179}180case 3: { // italic181styleNames = styleNames.filter(style => style !== `code-italic`);182styleNames.push('code-italic');183break;184}185case 4: { // underline186styleNames = styleNames.filter(style => (style !== `code-underline` && style !== `code-double-underline`));187styleNames.push('code-underline');188break;189}190case 5: { // blink191styleNames = styleNames.filter(style => style !== `code-blink`);192styleNames.push('code-blink');193break;194}195case 6: { // rapid blink196styleNames = styleNames.filter(style => style !== `code-rapid-blink`);197styleNames.push('code-rapid-blink');198break;199}200case 7: { // invert foreground and background201if (!colorsInverted) {202colorsInverted = true;203reverseForegroundAndBackgroundColors();204}205break;206}207case 8: { // hidden208styleNames = styleNames.filter(style => style !== `code-hidden`);209styleNames.push('code-hidden');210break;211}212case 9: { // strike-through/crossed-out213styleNames = styleNames.filter(style => style !== `code-strike-through`);214styleNames.push('code-strike-through');215break;216}217case 10: { // normal default font218styleNames = styleNames.filter(style => !style.startsWith('code-font'));219break;220}221case 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)222styleNames = styleNames.filter(style => !style.startsWith('code-font'));223styleNames.push(`code-font-${code - 10}`);224break;225}226case 21: { // double underline227styleNames = styleNames.filter(style => (style !== `code-underline` && style !== `code-double-underline`));228styleNames.push('code-double-underline');229break;230}231case 22: { // normal intensity (bold off and dim off)232styleNames = styleNames.filter(style => (style !== `code-bold` && style !== `code-dim`));233break;234}235case 23: { // Neither italic or blackletter (font 10)236styleNames = styleNames.filter(style => (style !== `code-italic` && style !== `code-font-10`));237break;238}239case 24: { // not underlined (Neither singly nor doubly underlined)240styleNames = styleNames.filter(style => (style !== `code-underline` && style !== `code-double-underline`));241break;242}243case 25: { // not blinking244styleNames = styleNames.filter(style => (style !== `code-blink` && style !== `code-rapid-blink`));245break;246}247case 27: { // not reversed/inverted248if (colorsInverted) {249colorsInverted = false;250reverseForegroundAndBackgroundColors();251}252break;253}254case 28: { // not hidden (reveal)255styleNames = styleNames.filter(style => style !== `code-hidden`);256break;257}258case 29: { // not crossed-out259styleNames = styleNames.filter(style => style !== `code-strike-through`);260break;261}262case 53: { // overlined263styleNames = styleNames.filter(style => style !== `code-overline`);264styleNames.push('code-overline');265break;266}267case 55: { // not overlined268styleNames = styleNames.filter(style => style !== `code-overline`);269break;270}271case 39: { // default foreground color272changeColor('foreground', undefined);273break;274}275case 49: { // default background color276changeColor('background', undefined);277break;278}279case 59: { // default underline color280changeColor('underline', undefined);281break;282}283case 73: { // superscript284styleNames = styleNames.filter(style => (style !== `code-superscript` && style !== `code-subscript`));285styleNames.push('code-superscript');286break;287}288case 74: { // subscript289styleNames = styleNames.filter(style => (style !== `code-superscript` && style !== `code-subscript`));290styleNames.push('code-subscript');291break;292}293case 75: { // neither superscript or subscript294styleNames = styleNames.filter(style => (style !== `code-superscript` && style !== `code-subscript`));295break;296}297default: {298setBasicColor(code);299break;300}301}302}303}304305/**306* Calculate and set styling for complicated 24-bit ANSI color codes.307* @param styleCodes Full list of integer codes that make up the full ANSI308* sequence, including the two defining codes and the three RGB codes.309* @param colorType If `'foreground'`, will set foreground color, if310* `'background'`, will set background color, and if it is `'underline'`311* will set the underline color.312* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#24-bit }313*/314function set24BitColor(styleCodes: number[], colorType: 'foreground' | 'background' | 'underline'): void {315if (styleCodes.length >= 5 &&316styleCodes[2] >= 0 && styleCodes[2] <= 255 &&317styleCodes[3] >= 0 && styleCodes[3] <= 255 &&318styleCodes[4] >= 0 && styleCodes[4] <= 255) {319const customColor = new RGBA(styleCodes[2], styleCodes[3], styleCodes[4]);320changeColor(colorType, customColor);321}322}323324/**325* Calculate and set styling for advanced 8-bit ANSI color codes.326* @param styleCodes Full list of integer codes that make up the ANSI327* sequence, including the two defining codes and the one color code.328* @param colorType If `'foreground'`, will set foreground color, if329* `'background'`, will set background color and if it is `'underline'`330* will set the underline color.331* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit }332*/333function set8BitColor(styleCodes: number[], colorType: 'foreground' | 'background' | 'underline'): void {334let colorNumber = styleCodes[2];335const color = calcANSI8bitColor(colorNumber);336337if (color) {338changeColor(colorType, color);339} else if (colorNumber >= 0 && colorNumber <= 15) {340if (colorType === 'underline') {341// for underline colors we just decode the 0-15 color number to theme color, set and return342const colorName = ansiColorIdentifiers[colorNumber];343changeColor(colorType, `--vscode-debug-ansi-${colorName}`);344return;345}346// Need to map to one of the four basic color ranges (30-37, 90-97, 40-47, 100-107)347colorNumber += 30;348if (colorNumber >= 38) {349// Bright colors350colorNumber += 52;351}352if (colorType === 'background') {353colorNumber += 10;354}355setBasicColor(colorNumber);356}357}358359/**360* Calculate and set styling for basic bright and dark ANSI color codes. Uses361* theme colors if available. Automatically distinguishes between foreground362* and background colors; does not support color-clearing codes 39 and 49.363* @param styleCode Integer color code on one of the following ranges:364* [30-37, 90-97, 40-47, 100-107]. If not on one of these ranges, will do365* nothing.366*/367function setBasicColor(styleCode: number): void {368let colorType: 'foreground' | 'background' | undefined;369let colorIndex: number | undefined;370371if (styleCode >= 30 && styleCode <= 37) {372colorIndex = styleCode - 30;373colorType = 'foreground';374} else if (styleCode >= 90 && styleCode <= 97) {375colorIndex = (styleCode - 90) + 8; // High-intensity (bright)376colorType = 'foreground';377} else if (styleCode >= 40 && styleCode <= 47) {378colorIndex = styleCode - 40;379colorType = 'background';380} else if (styleCode >= 100 && styleCode <= 107) {381colorIndex = (styleCode - 100) + 8; // High-intensity (bright)382colorType = 'background';383}384385if (colorIndex !== undefined && colorType) {386const colorName = ansiColorIdentifiers[colorIndex];387changeColor(colorType, `--vscode-debug-ansi-${colorName.replaceAll('.', '-')}`);388}389}390}391392/**393* @param root The {@link HTMLElement} to append the content to.394* @param stringContent The text content to be appended.395* @param cssClasses The list of CSS styles to apply to the text content.396* @param linkDetector The {@link ILinkDetector} responsible for generating links from {@param stringContent}.397* @param customTextColor If provided, will apply custom color with inline style.398* @param customBackgroundColor If provided, will apply custom backgroundColor with inline style.399* @param customUnderlineColor If provided, will apply custom textDecorationColor with inline style.400* @param highlights The ranges to highlight.401* @param offset The starting index of the stringContent in the original text.402* @param hoverBehavior hover behavior with disposable store for managing event listeners.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,415hoverBehavior: DebugLinkHoverBehaviorTypeData,416): void {417if (!root || !stringContent) {418return;419}420421const container = linkDetector.linkify(422stringContent,423hoverBehavior,424true,425workspaceFolder,426undefined,427highlights?.map(h => ({ start: h.start - offset, end: h.end - offset, extraClasses: h.extraClasses })),428);429430container.className = cssClasses.join(' ');431if (customTextColor) {432container.style.color =433typeof customTextColor === 'string' ? `var(${customTextColor})` : Color.Format.CSS.formatRGB(new Color(customTextColor));434}435if (customBackgroundColor) {436container.style.backgroundColor =437typeof customBackgroundColor === 'string' ? `var(${customBackgroundColor})` : Color.Format.CSS.formatRGB(new Color(customBackgroundColor));438}439if (customUnderlineColor) {440container.style.textDecorationColor =441typeof customUnderlineColor === 'string' ? `var(${customUnderlineColor})` : Color.Format.CSS.formatRGB(new Color(customUnderlineColor));442}443444root.appendChild(container);445}446447/**448* Calculate the color from the color set defined in the ANSI 8-bit standard.449* Standard and high intensity colors are not defined in the standard as specific450* colors, so these and invalid colors return `undefined`.451* @see {@link https://en.wikipedia.org/wiki/ANSI_escape_code#8-bit } for info.452* @param colorNumber The number (ranging from 16 to 255) referring to the color453* desired.454*/455export function calcANSI8bitColor(colorNumber: number): RGBA | undefined {456if (colorNumber % 1 !== 0) {457// Should be integer458return;459} if (colorNumber >= 16 && colorNumber <= 231) {460// Converts to one of 216 RGB colors461colorNumber -= 16;462463let blue: number = colorNumber % 6;464colorNumber = (colorNumber - blue) / 6;465let green: number = colorNumber % 6;466colorNumber = (colorNumber - green) / 6;467let red: number = colorNumber;468469// red, green, blue now range on [0, 5], need to map to [0,255]470const convFactor: number = 255 / 5;471blue = Math.round(blue * convFactor);472green = Math.round(green * convFactor);473red = Math.round(red * convFactor);474475return new RGBA(red, green, blue);476} else if (colorNumber >= 232 && colorNumber <= 255) {477// Converts to a grayscale value478colorNumber -= 232;479const colorLevel: number = Math.round(colorNumber / 23 * 255);480return new RGBA(colorLevel, colorLevel, colorLevel);481} else {482return;483}484}485486registerThemingParticipant((theme, collector) => {487const areas = [488{ selector: '.monaco-workbench .sidebar, .monaco-workbench .auxiliarybar', bg: theme.getColor(SIDE_BAR_BACKGROUND) },489{ selector: '.monaco-workbench .panel', bg: theme.getColor(PANEL_BACKGROUND) },490{ selector: '.monaco-workbench .monaco-list-row.selected', bg: theme.getColor(listInactiveSelectionBackground) },491{ selector: '.monaco-workbench .monaco-list-row.focused', bg: theme.getColor(listInactiveFocusBackground) },492{ selector: '.monaco-workbench .monaco-list:focus .monaco-list-row.focused', bg: theme.getColor(listFocusBackground) },493{ selector: '.monaco-workbench .monaco-list:focus .monaco-list-row.selected', bg: theme.getColor(listActiveSelectionBackground) },494{ selector: '.debug-hover-widget', bg: theme.getColor(editorHoverBackground) },495];496497for (const { selector, bg } of areas) {498const content = ansiColorIdentifiers499.map(color => {500const actual = theme.getColor(color);501if (!actual) { return undefined; }502// this uses the default contrast ratio of 4 (from the terminal),503// we may want to make this configurable in the future, but this is504// good to keep things sane to start with.505return `--vscode-debug-ansi-${color.replaceAll('.', '-')}:${bg ? bg.ensureConstrast(actual, 4) : actual}`;506})507.filter(isDefined);508509collector.addRule(`${selector} { ${content.join(';')} }`);510}511});512513514