Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 28 additions & 11 deletions src/vs/base/common/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -858,8 +858,9 @@ export namespace Color {
* Converts an Hex color value to a Color.
* returns r, g, and b are contained in the set [0, 255]
* @param hex string (#RGB, #RGBA, #RRGGBB or #RRGGBBAA).
* @param format 'rgba' (default) or 'argb' - controls how to interpret 8-char hex colors
Comment on lines 858 to +861
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc comment should be updated to reflect that both RGBA and ARGB formats are supported. The current description mentions "#RGB, #RGBA, #RRGGBB or #RRGGBBAA" but doesn't indicate that ARGB formats are also accepted when the format parameter is set to 'argb'.

Suggested change
* Converts an Hex color value to a Color.
* returns r, g, and b are contained in the set [0, 255]
* @param hex string (#RGB, #RGBA, #RRGGBB or #RRGGBBAA).
* @param format 'rgba' (default) or 'argb' - controls how to interpret 8-char hex colors
* Converts a Hex color value to a Color.
* Returns r, g, and b in the set [0, 255].
* @param hex string - Supported formats:
* - When format is 'rgba' (default): #RGB, #RGBA, #RRGGBB, #RRGGBBAA
* - When format is 'argb': #ARGB, #AARRGGBB
* @param format 'rgba' (default) or 'argb' - controls how to interpret 4/8-char hex colors

Copilot uses AI. Check for mistakes.
*/
export function parseHex(hex: string): Color | null {
export function parseHex(hex: string, format: 'rgba' | 'argb' = 'rgba'): Color | null {
const length = hex.length;

if (length === 0) {
Expand All @@ -881,11 +882,19 @@ export namespace Color {
}

if (length === 9) {
// #RRGGBBAA format
const r = 16 * _parseHexDigit(hex.charCodeAt(1)) + _parseHexDigit(hex.charCodeAt(2));
const g = 16 * _parseHexDigit(hex.charCodeAt(3)) + _parseHexDigit(hex.charCodeAt(4));
const b = 16 * _parseHexDigit(hex.charCodeAt(5)) + _parseHexDigit(hex.charCodeAt(6));
const a = 16 * _parseHexDigit(hex.charCodeAt(7)) + _parseHexDigit(hex.charCodeAt(8));
// #RRGGBBAA or #AARRGGBB format
let r: number, g: number, b: number, a: number;
if (format === 'argb') {
a = 16 * _parseHexDigit(hex.charCodeAt(1)) + _parseHexDigit(hex.charCodeAt(2));
r = 16 * _parseHexDigit(hex.charCodeAt(3)) + _parseHexDigit(hex.charCodeAt(4));
g = 16 * _parseHexDigit(hex.charCodeAt(5)) + _parseHexDigit(hex.charCodeAt(6));
b = 16 * _parseHexDigit(hex.charCodeAt(7)) + _parseHexDigit(hex.charCodeAt(8));
} else {
r = 16 * _parseHexDigit(hex.charCodeAt(1)) + _parseHexDigit(hex.charCodeAt(2));
g = 16 * _parseHexDigit(hex.charCodeAt(3)) + _parseHexDigit(hex.charCodeAt(4));
b = 16 * _parseHexDigit(hex.charCodeAt(5)) + _parseHexDigit(hex.charCodeAt(6));
a = 16 * _parseHexDigit(hex.charCodeAt(7)) + _parseHexDigit(hex.charCodeAt(8));
}
return new Color(new RGBA(r, g, b, a / 255));
}

Expand All @@ -898,11 +907,19 @@ export namespace Color {
}

if (length === 5) {
// #RGBA format
const r = _parseHexDigit(hex.charCodeAt(1));
const g = _parseHexDigit(hex.charCodeAt(2));
const b = _parseHexDigit(hex.charCodeAt(3));
const a = _parseHexDigit(hex.charCodeAt(4));
// #RGBA or #ARGB format
let r: number, g: number, b: number, a: number;
if (format === 'argb') {
a = _parseHexDigit(hex.charCodeAt(1));
r = _parseHexDigit(hex.charCodeAt(2));
g = _parseHexDigit(hex.charCodeAt(3));
b = _parseHexDigit(hex.charCodeAt(4));
} else {
r = _parseHexDigit(hex.charCodeAt(1));
g = _parseHexDigit(hex.charCodeAt(2));
b = _parseHexDigit(hex.charCodeAt(3));
a = _parseHexDigit(hex.charCodeAt(4));
}
return new Color(new RGBA(16 * r + r, 16 * g + g, 16 * b + b, (16 * a + a) / 255));
}

Expand Down
16 changes: 16 additions & 0 deletions src/vs/base/test/common/color.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,22 @@ suite('Color', () => {
assert.deepStrictEqual(Color.Format.CSS.parseHex('#CFA8')!.rgba, new RGBA(204, 255, 170, 0.533));
});

test('parseHex with ARGB format', () => {
// 8-digit ARGB format: #AARRGGBB
assert.deepStrictEqual(Color.Format.CSS.parseHex('#80FF0000', 'argb')!.rgba, new RGBA(255, 0, 0, 128 / 255));
assert.deepStrictEqual(Color.Format.CSS.parseHex('#FF00FF00', 'argb')!.rgba, new RGBA(0, 255, 0, 1));
assert.deepStrictEqual(Color.Format.CSS.parseHex('#000000FF', 'argb')!.rgba, new RGBA(0, 0, 255, 0));

// 4-digit ARGB format: #ARGB
assert.deepStrictEqual(Color.Format.CSS.parseHex('#8F00', 'argb')!.rgba, new RGBA(255, 0, 0, 136 / 255));
assert.deepStrictEqual(Color.Format.CSS.parseHex('#F0F0', 'argb')!.rgba, new RGBA(0, 255, 0, 1));
assert.deepStrictEqual(Color.Format.CSS.parseHex('#000F', 'argb')!.rgba, new RGBA(0, 0, 255, 0));

// Verify RGBA is still default
assert.deepStrictEqual(Color.Format.CSS.parseHex('#FF000080')!.rgba, new RGBA(255, 0, 0, 128 / 255));
assert.deepStrictEqual(Color.Format.CSS.parseHex('#F008')!.rgba, new RGBA(255, 0, 0, 136 / 255));
});

suite('format', () => {
test('formatHSL should use whole numbers for percentages', () => {
// Test case matching the issue: color with fractional percentages that should be rounded
Expand Down
4 changes: 2 additions & 2 deletions src/vs/editor/browser/services/editorWorkerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -239,9 +239,9 @@ export class EditorWorkerService extends Disposable implements IEditorWorkerServ
return worker.$findSectionHeaders(uri.toString(), options);
}

public async computeDefaultDocumentColors(uri: URI): Promise<languages.IColorInformation[] | null> {
public async computeDefaultDocumentColors(uri: URI, colorFormat?: 'rgba' | 'argb'): Promise<languages.IColorInformation[] | null> {
const worker = await this._workerWithResources([uri]);
return worker.$computeDefaultDocumentColors(uri.toString());
return worker.$computeDefaultDocumentColors(uri.toString(), colorFormat);
}

private async _workerWithResources(resources: URI[], forceLargeModels: boolean = false): Promise<Proxied<EditorWorker>> {
Expand Down
16 changes: 16 additions & 0 deletions src/vs/editor/common/config/editorOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,10 @@ export interface IEditorOptions {
* Controls the max number of color decorators that can be rendered in an editor at once.
*/
colorDecoratorsLimit?: number;
/**
* Controls the hexadecimal color format used by color decorators.
*/
colorDecoratorsHexFormat?: 'rgba' | 'argb';
/**
* Control the behaviour of comments in the editor.
*/
Expand Down Expand Up @@ -5751,6 +5755,7 @@ export const enum EditorOption {
codeLensFontSize,
colorDecorators,
colorDecoratorsLimit,
colorDecoratorsHexFormat,
columnSelection,
comments,
contextmenu,
Expand Down Expand Up @@ -6107,6 +6112,17 @@ export const EditorOptions = {
markdownDescription: nls.localize('colorDecoratorsLimit', "Controls the max number of color decorators that can be rendered in an editor at once.")
}
)),
colorDecoratorsHexFormat: register(new EditorStringEnumOption(
EditorOption.colorDecoratorsHexFormat, 'colorDecoratorsHexFormat', 'rgba' as 'rgba' | 'argb',
['rgba', 'argb'] as const,
{
enumDescriptions: [
nls.localize('editor.colorDecoratorsHexFormat.rgba', "Use RGBA format (#RRGGBBAA)"),
nls.localize('editor.colorDecoratorsHexFormat.argb', "Use ARGB format (#AARRGGBB)")
],
markdownDescription: nls.localize('colorDecoratorsHexFormat', "Controls the hexadecimal color format for parsing colors. RGBA is standard for web (CSS), while ARGB is used by Qt and some other frameworks.")
}
)),
columnSelection: register(new EditorBooleanOption(
EditorOption.columnSelection, 'columnSelection', false,
{ description: nls.localize('columnSelection', "Enable that the selection with the mouse and keys is doing column selection.") }
Expand Down
12 changes: 6 additions & 6 deletions src/vs/editor/common/languages/defaultDocumentColorsComputer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,11 @@ function _findRange(model: IDocumentColorComputerTarget, match: RegExpMatchArray
return range;
}

function _findHexColorInformation(range: IRange | undefined, hexValue: string) {
function _findHexColorInformation(range: IRange | undefined, hexValue: string, format: 'rgba' | 'argb' = 'rgba') {
if (!range) {
return;
}
const parsedHexColor = Color.Format.CSS.parseHex(hexValue);
const parsedHexColor = Color.Format.CSS.parseHex(hexValue, format);
if (!parsedHexColor) {
return;
}
Expand Down Expand Up @@ -98,7 +98,7 @@ function _findMatches(model: IDocumentColorComputerTarget | string, regex: RegEx
}
}

function computeColors(model: IDocumentColorComputerTarget): IColorInformation[] {
function computeColors(model: IDocumentColorComputerTarget, format: 'rgba' | 'argb' = 'rgba'): IColorInformation[] {
const result: IColorInformation[] = [];
// Early validation for RGB and HSL
const initialValidationRegex = /\b(rgb|rgba|hsl|hsla)(\([0-9\s,.\%]*\))|^(#)([A-Fa-f0-9]{3})\b|^(#)([A-Fa-f0-9]{4})\b|^(#)([A-Fa-f0-9]{6})\b|^(#)([A-Fa-f0-9]{8})\b|(?<=['"\s])(#)([A-Fa-f0-9]{3})\b|(?<=['"\s])(#)([A-Fa-f0-9]{4})\b|(?<=['"\s])(#)([A-Fa-f0-9]{6})\b|(?<=['"\s])(#)([A-Fa-f0-9]{8})\b/gm;
Expand Down Expand Up @@ -127,7 +127,7 @@ function computeColors(model: IDocumentColorComputerTarget): IColorInformation[]
const regexParameters = /^\(\s*((?:360(?:\.0+)?|(?:36[0]|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])(?:\.\d+)?))\s*[\s,]\s*(100|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(100|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(0[.][0-9]+|[.][0-9]+|[01][.]0*|[01])\s*\)$/gm;
colorInformation = _findHSLColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), true);
} else if (colorScheme === '#') {
colorInformation = _findHexColorInformation(_findRange(model, initialMatch), colorScheme + colorParameters);
colorInformation = _findHexColorInformation(_findRange(model, initialMatch), colorScheme + colorParameters, format);
}
if (colorInformation) {
result.push(colorInformation);
Expand All @@ -140,10 +140,10 @@ function computeColors(model: IDocumentColorComputerTarget): IColorInformation[]
/**
* Returns an array of all default document colors in the provided document
*/
export function computeDefaultDocumentColors(model: IDocumentColorComputerTarget): IColorInformation[] {
export function computeDefaultDocumentColors(model: IDocumentColorComputerTarget, format: 'rgba' | 'argb' = 'rgba'): IColorInformation[] {
if (!model || typeof model.getValue !== 'function' || typeof model.positionAt !== 'function') {
// Unknown caller!
return [];
}
return computeColors(model);
return computeColors(model, format);
}
4 changes: 2 additions & 2 deletions src/vs/editor/common/services/editorWebWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -408,12 +408,12 @@ export class EditorWorker implements IDisposable, IWorkerTextModelSyncChannelSer

// --- BEGIN default document colors -----------------------------------------------------------

public async $computeDefaultDocumentColors(modelUrl: string): Promise<IColorInformation[] | null> {
public async $computeDefaultDocumentColors(modelUrl: string, colorFormat: 'rgba' | 'argb' = 'rgba'): Promise<IColorInformation[] | null> {
const model = this._getModel(modelUrl);
if (!model) {
return null;
}
return computeDefaultDocumentColors(model);
return computeDefaultDocumentColors(model, colorFormat);
}

// ---- BEGIN suggest --------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/vs/editor/common/services/editorWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export interface IEditorWorkerService {

findSectionHeaders(uri: URI, options: FindSectionHeaderOptions): Promise<SectionHeader[]>;

computeDefaultDocumentColors(uri: URI): Promise<IColorInformation[] | null>;
computeDefaultDocumentColors(uri: URI, colorFormat?: 'rgba' | 'argb'): Promise<IColorInformation[] | null>;

}

Expand Down
Loading
Loading