From 1e32efee10d3fde564e8475a6d6e1473ce066206 Mon Sep 17 00:00:00 2001 From: Paul Marechal Date: Sat, 7 Mar 2026 17:19:40 -0500 Subject: [PATCH 1/3] gdscript: allow color picking Implement `DocumentColorProvider` by extracting Color expressions, interpreting them, and replacing arguments in place when possible. --- package.json | 15 + src/extension.ts | 7 +- src/providers/document_colors.ts | 545 +++++++++++++++++++++++++++++++ src/providers/index.ts | 1 + src/utils/colors.ts | 213 ++++++++++++ src/utils/vscode_utils.ts | 2 +- 6 files changed, 780 insertions(+), 3 deletions(-) create mode 100644 src/providers/document_colors.ts create mode 100644 src/utils/colors.ts diff --git a/package.json b/package.json index 391ff46b5..2318fe43b 100644 --- a/package.json +++ b/package.json @@ -375,6 +375,21 @@ "type": "boolean", "default": true, "description": "Whether to enable inlay hints in GDResource (.tscn, .tres, etc) files" + }, + "godotTools.colorPicker.precision": { + "type": "integer", + "default": 3, + "description": "Amount of decimals to include when using the color picker. 0 for arbitrary precision." + }, + "godotTools.colorPicker.padValues": { + "type": "boolean", + "default": false, + "description": "Write either 1.2 (unpadded) or either 1.200 (padded)." + }, + "godotTools.colorPicker.uppercaseHex": { + "type": "boolean", + "default": false, + "description": "Write either 0x008b8bff or either 0x008B8BFF." } } }, diff --git a/src/extension.ts b/src/extension.ts index 901c0c40e..829b2205b 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -5,6 +5,7 @@ import { attemptSettingsUpdate, get_extension_uri, clean_godot_path } from "./ut import { GDInlayHintsProvider, GDHoverProvider, + GDDocumentColorProvider, GDDocumentDropEditProvider, GDDocumentLinkProvider, GDSemanticTokensProvider, @@ -37,6 +38,7 @@ interface Extension { lsp?: ClientConnectionManager; debug?: GodotDebugger; scenePreviewProvider?: ScenePreviewProvider; + colorProvider?: GDDocumentColorProvider; linkProvider?: GDDocumentLinkProvider; dropsProvider?: GDDocumentDropEditProvider; hoverProvider?: GDHoverProvider; @@ -58,6 +60,7 @@ export function activate(context: vscode.ExtensionContext) { globals.lsp = new ClientConnectionManager(context); globals.debug = new GodotDebugger(context); globals.scenePreviewProvider = new ScenePreviewProvider(context); + globals.colorProvider = new GDDocumentColorProvider(context); globals.linkProvider = new GDDocumentLinkProvider(context); globals.dropsProvider = new GDDocumentDropEditProvider(context); globals.hoverProvider = new GDHoverProvider(context); @@ -101,14 +104,14 @@ async function initial_setup() { break; } case "WRONG_VERSION": { - const message = `The specified Godot executable, '${godotPath}' is the wrong version. + const message = `The specified Godot executable, '${godotPath}' is the wrong version. The current project uses Godot v${projectVersion}, but the specified executable is Godot v${result.version}. Extension features will not work correctly unless this is fixed.`; prompt_for_godot_executable(message, settingName); break; } case "INVALID_EXE": { - const message = `The specified Godot executable, '${godotPath}' is invalid. + const message = `The specified Godot executable, '${godotPath}' is invalid. Extension features will not work correctly unless this is fixed.`; prompt_for_godot_executable(message, settingName); break; diff --git a/src/providers/document_colors.ts b/src/providers/document_colors.ts new file mode 100644 index 000000000..aad44ad6d --- /dev/null +++ b/src/providers/document_colors.ts @@ -0,0 +1,545 @@ +import * as vscode from "vscode"; +import { Color8, hex, NAMED_COLORS, to_html, to_rgba32 } from "../utils/colors"; +import { EXTENSION_PREFIX } from "../utils"; + +type Arg = + | ArgString + | ArgHexadecimal + | ArgDecimal + +type ArgRange = [start: number, end: number]; +type ArgString = { type: "string", value: string, range: ArgRange }; +type ArgHexadecimal = { type: "hexadecimal", value: number, range: ArgRange }; +type ArgDecimal = { type: "decimal", value: number, range: ArgRange }; + +function argIsNumber(arg: Arg): arg is ArgHexadecimal | ArgDecimal { + return typeof arg.value === "number"; +} +function argIsString(arg: Arg): arg is ArgString { + return typeof arg.value === "string"; +} +function argIsOptionalNumber(arg: Arg | null): arg is ArgHexadecimal | ArgDecimal | null { + return arg === null || argIsNumber(arg); +} + +interface ColorExpression { + text: string; + fn: string; + args: Arg[]; + range: vscode.Range; +} + +interface ColorPresentationsContext { + readonly document: vscode.TextDocument; + readonly range: vscode.Range; +} + +const SECTION_COLOR_PICKER = `${EXTENSION_PREFIX}.colorPicker`; + +/** + * This is GDScript's default color when no components are provided + */ +const COLOR_BLACK = new vscode.Color(0, 0, 0, 1); + +/** + * Matches `Color()` or `Color8()` or `Color.some_method()` + * + * Match groups: `fn` and `args` + */ +const RE_COLOR_EXPR = /(?\bColor(?:8|\.hex|\.from_rgba8)?)\((?.*?)\)[^;\n]*?/; +/** + * Matches arguments from an argument list + * + * Match group: `arg` + */ +const RE_ARGUMENTS = /(?:\s*(?:#[^\n]*\n)?\s*)(?.*?)(?:\s*(?:#[^\n]*\n)?\s*)(?:,|$)/; +/** + * Match group: `constant` + */ +const RE_COLOR_CODE_CONSTANT = /(?\w+)/; +/** + * Match group: `html` + */ +const RE_COLOR_CODE_HTML = /#?(?[0-9a-fA-F]{3,8})/; +/** + * Matches something that looks like a constant/named color + * + * Match group: `color` + */ +const RE_NAMED_COLOR = /\bColor.(?[A-Z]+(?:_[A-Z]+)*)/; +/** + * Matches decimal numbers + * + * Match group `decimal` + */ +const RE_DECIMAL = /(?\d*\.\d+|\d+\.?)/; +/** + * Matches hexadecimal numbers + * + * Match group: `hex` + */ +const RE_HEXADECIMAL = /(?0x[0-9a-fA-F]{1,8})/; +/** + * Matches strings, not fool-proof but enough for what we're looking after + * + * Match group: `string` + */ +const RE_STRING = /["'](?.*?)["']/; + +export class GDDocumentColorProvider implements vscode.DocumentColorProvider { + + private colorPickerConfiguration: vscode.WorkspaceConfiguration; + + private precision: number; + private padValues: boolean; + private uppercaseHex: boolean; + + constructor(private context: vscode.ExtensionContext) { + const selector: vscode.DocumentSelector = [ + { language: "gdscript", scheme: "file" }, + ]; + this.colorPickerConfiguration = vscode.workspace.getConfiguration(SECTION_COLOR_PICKER); + context.subscriptions.push( + vscode.languages.registerColorProvider(selector, this), + vscode.workspace.onDidChangeConfiguration(e => { + if(e.affectsConfiguration(SECTION_COLOR_PICKER)) { + this.updatePrecision(); + this.updatePadValues(); + this.updateUppercaseHex(); + } + }), + ); + this.updatePrecision(); + this.updatePadValues(); + this.updateUppercaseHex(); + } + + private updatePrecision(): void { + this.precision = this.colorPickerConfiguration.get("precision"); + } + + private updatePadValues(): void { + this.padValues = this.colorPickerConfiguration.get("padValues"); + } + + private updateUppercaseHex(): void { + this.uppercaseHex = this.colorPickerConfiguration.get("uppercaseHex"); + } + + async provideDocumentColors(document: vscode.TextDocument, token: vscode.CancellationToken): Promise { + const colors: vscode.ColorInformation[] = []; + const text = document.getText(); + for (const match of text.matchAll(new RegExp(RE_COLOR_EXPR, "dgs"))) { + const range = new vscode.Range( + document.positionAt(match.index), + document.positionAt(match.index + match[0].length) + ); + const args = this.parseArgs(match.groups.args, match.indices.groups.args[0]); + const color = this.interpretColor({ + text: match[0], + fn: match.groups.fn, + args, + range, + }); + if (color) { + colors.push({ color, range }); + } + // allow for early bail out + await Promise.resolve(); + if (token.isCancellationRequested) { + return colors; + } + } + for (const match of text.matchAll(new RegExp(RE_NAMED_COLOR, "g"))) { + const range = new vscode.Range( + document.positionAt(match.index), + document.positionAt(match.index + match[0].length) + ); + const color = NAMED_COLORS[match.groups.color]; + if (color) { + colors.push({ color, range }); + } + // allow for early bail out + await Promise.resolve(); + if (token.isCancellationRequested) { + return colors; + } + } + return colors; + } + + provideColorPresentations(color: vscode.Color, context: ColorPresentationsContext, token: vscode.CancellationToken): vscode.ColorPresentation[] { + const text = context.document.getText(context.range); + const noEdit = new vscode.TextEdit(context.range, text); // no edit TextEdit + const named_color = text.match(RE_NAMED_COLOR)?.groups.color; + if (named_color) { + return [ + { label: `${this.stringifyColorRGBA(color)} 🔒`, textEdit: noEdit }, + { label: `${this.stringifyColorFromRGBA8(color)} 🔒`, textEdit: noEdit }, + { label: `${this.stringifyColorHTML(color)} 🔒`, textEdit: noEdit }, + { label: `${this.stringifyColorHex(color)} 🔒`, textEdit: noEdit }, + ]; + } + const presentations: vscode.ColorPresentation[] = [ + { label: this.stringifyColorRGBA(color) }, + { label: this.stringifyColorFromRGBA8(color) }, + { label: this.stringifyColorHTML(color) }, + { label: this.stringifyColorHex(color) }, + ]; + const indices = { + "Color": 0, + "Color.from_rgba8": 1, + "Color.hex": 3, + }; + const match = text.match(new RegExp(RE_COLOR_EXPR, "ds")); + if (match) { + const expr: ColorExpression = { + text: match[0], + fn: match.groups.fn, + args: this.parseArgs(match.groups.args, match.indices.groups.args[0]), + range: context.range, + }; + const presentation = this.editColorExpression(expr, color); + if (presentation) { + presentations.splice(indices[expr.fn] ?? -1, 1); + presentations.unshift(presentation); + } + } + return presentations; + } + + private toPrecision(p_value: number): string { + let n = p_value; + if (this.precision > 0) { + if (this.padValues) { + return n.toFixed(this.precision); + } + const e = 10 ** this.precision; + n = Math.round(n * e) / e; + } + return n.toString(10); + } + + private stringifyColorRGBA(color: vscode.Color): string { + const red = this.toPrecision(color.red); + const green = this.toPrecision(color.green); + const blue = this.toPrecision(color.blue); + const alpha = this.toPrecision(color.alpha); + return `Color(${rgbaParams(red, green, blue, alpha, "1")})`; + } + + private stringifyColorHTML(color: vscode.Color): string { + let html = to_html(color); + if (this.uppercaseHex) { + html = html.toUpperCase(); + } + return `Color("#${html}")`; + } + + private stringifyColor8(color: vscode.Color): string { + const { r8, g8, b8, a8 } = color8(color); + return `Color8(${rgbaParams(r8, g8, b8, a8, 255)})`; + } + + private stringifyColorFromRGBA8(color: vscode.Color): string { + const { r8, g8, b8, a8 } = color8(color); + return `Color.from_rgba8(${rgbaParams(r8, g8, b8, a8, 255)})`; + } + + private stringifyColorHex(color: vscode.Color): string { + return `Color.hex(0x${this.colorToHex(color)})`; + } + + private colorToHex(color: vscode.Color): string { + const hex = to_rgba32(color).toString(16).padStart(8, "0"); + return this.uppercaseHex + ? hex.toUpperCase() + : hex; // lowercase by default + } + + private interpretColor(expr: ColorExpression): vscode.Color | undefined { + switch (expr.fn) { + case "Color": + return argMatchRun(expr.args, [interpretColor, interpretColorCode, interpretColorCodeAlpha, interpretColorRGB, interpretColorRGBA], this); + case "Color8": + return argMatchRun(expr.args, [interpretColorFromRGB8, interpretColorFromRGBA8], this); + case "Color.from_rgba8": + return argMatchRun(expr.args, [interpretColorFromRGB8, interpretColorFromRGBA8], this); + case "Color.hex": + return argMatchRun(expr.args, [interpretColorHex], this); + } + } + + private editColorExpression(expr: ColorExpression, color: vscode.Color): vscode.ColorPresentation | undefined { + switch (expr.fn) { + case "Color": + return argMatchRun2(expr, color, [this.editColorCode, this.editColorRGB, this.editColorRGBA], this); + case "Color8": + return argMatchRun2(expr, color, [this.editColorRGB8, this.editColorRGBA8], this); + case "Color.from_rgba8": + return argMatchRun2(expr, color, [this.editColorFromRGB8, this.editColorFromRGBA8], this); + case "Color.hex": + return argMatchRun2(expr, color, [this.editColorHex], this); + } + } + + private editColorCode(expr: ColorExpression, color: vscode.Color, code: Arg): vscode.ColorPresentation { + const newText = replaceArgs(expr.text, [code], [to_html(color)]); + return { + label: this.stringifyColorHTML(color), + textEdit: vscode.TextEdit.replace(expr.range, newText), + }; + } + + private editColorRGB(expr: ColorExpression, color: vscode.Color, red: Arg, green: Arg, blue: Arg): vscode.ColorPresentation { + const presentation: vscode.ColorPresentation = { label: this.stringifyColorRGBA(color) }; + if (color.alpha === 1) { + const newText = replaceArgs(expr.text, [red, green, blue], [color.red, color.green, color.blue]); + presentation.textEdit = vscode.TextEdit.replace(expr.range, newText); + } + return presentation; + } + + private editColorRGBA(expr: ColorExpression, color: vscode.Color, red: Arg, green: Arg, blue: Arg, alpha: Arg): vscode.ColorPresentation { + const newText = replaceArgs(expr.text, [red, green, blue, alpha], [color.red, color.blue, color.green, color.alpha]); + return { + label: this.stringifyColorRGBA(color), + textEdit: vscode.TextEdit.replace(expr.range, newText), + }; + } + + private editColorRGB8(expr: ColorExpression, color: vscode.Color, red8: Arg, green8: Arg, blue8: Arg): vscode.ColorPresentation { + const { r8, g8, b8 } = color8(color); + const presentation: vscode.ColorPresentation = { label: this.stringifyColor8(color) }; + if (color.alpha === 1) { + const newText = replaceArgs(expr.text, [red8, green8, blue8], [r8, g8, b8]); + presentation.textEdit = vscode.TextEdit.replace(expr.range, newText); + } + return presentation; + } + + private editColorRGBA8(expr: ColorExpression, color: vscode.Color, red8: Arg, green8: Arg, blue8: Arg, alpha8: Arg): vscode.ColorPresentation { + const { r8, g8, b8, a8 } = color8(color); + const newText = replaceArgs(expr.text, [red8, green8, blue8, alpha8], [r8, g8, b8, a8]); + return { + label: this.stringifyColor8(color), + textEdit: vscode.TextEdit.replace(expr.range, newText), + }; + } + + private editColorFromRGB8(expr: ColorExpression, color: vscode.Color, red8: Arg, green8: Arg, blue8: Arg): vscode.ColorPresentation { + const { r8, g8, b8 } = color8(color); + const presentation: vscode.ColorPresentation = { label: this.stringifyColorFromRGBA8(color) }; + if (color.alpha === 1) { + const newText = replaceArgs(expr.text, [red8, green8, blue8], [r8, g8, b8]); + presentation.textEdit = vscode.TextEdit.replace(expr.range, newText); + } + return presentation; + } + + private editColorFromRGBA8(expr: ColorExpression, color: vscode.Color, red8: Arg, green8: Arg, blue8: Arg, alpha8: Arg): vscode.ColorPresentation { + const { r8, g8, b8, a8 } = color8(color); + const newText = replaceArgs(expr.text, [red8, green8, blue8, alpha8], [r8, g8, b8, a8]); + return { + textEdit: vscode.TextEdit.replace(expr.range, newText), + label: this.stringifyColorFromRGBA8(color), + }; + } + + private editColorHex(expr: ColorExpression, color: vscode.Color, p_hex: Arg): vscode.ColorPresentation { + const newText = replaceArgs(expr.text, [p_hex], [`0x${this.colorToHex(color)}`]); + return { + textEdit: vscode.TextEdit.replace(expr.range, newText), + label: this.stringifyColorHex(color), + }; + } + + private parseArgs(args: string, offset: number): Arg[] { + const results: Arg[] = []; + for (const match of args.matchAll(new RegExp(RE_ARGUMENTS, "dgs"))) { + const range: ArgRange = [...match.indices.groups.arg]; + range[0] += offset; + range[1] += offset; + const arg = this.parseArg(match.groups.arg, range); + if (!arg) { + break; + } + results.push(arg); + } + return results; + } + + private parseArg(arg: string, range: [number, number]): Arg | undefined { + const string = arg.match(RE_STRING)?.groups.string; + if (string) { + return { + type: "string", + value: string, + range, + }; + } + const hexa = arg.match(RE_HEXADECIMAL)?.groups.hex; + if (hexa) { + return { + type: "hexadecimal", + value: Number.parseInt(hexa, 16), + range, + }; + } + const decimal = arg.match(RE_DECIMAL)?.groups.decimal; + if (decimal) { + return { + type: "decimal", + value: ( + decimal.includes(".") + ? Number.parseFloat(decimal) + : Number.parseInt(decimal, 10) + ), + range, + }; + } + return; + } +} + +/** + * Returns the provided components as a list of arguments but will omit {@link alpha} if equal to {@link alphaMax} + */ +function rgbaParams(red: any, green: any, blue: any, alpha: any, alphaMax: any): string { + return alpha === alphaMax + ? `${red}, ${green}, ${blue}` + : `${red}, ${green}, ${blue}, ${alpha}`; +} + +/** + * Convert from vscode.Color [0-1] range to Color8's [0-255] range + */ +function color8(color: vscode.Color): Color8 { + return { + r8: Math.round(color.red * 255), + g8: Math.round(color.green * 255), + b8: Math.round(color.blue * 255), + a8: Math.round(color.alpha * 255), + }; +} + +/** + * Find and run functions that precisely accept `args` and return the first non-null result. + */ +function argMatchRun(args: T[], fns: ((this: V, ...args: T[]) => U)[], thisArg?: V): U { + for (const fn of fns) { + if (args.length === fn.length) { + const color = fn.apply(thisArg, args); + if (color) { + return color; + } + } + } +} + +/** + * Find and run functions that precisely accept `[expr, color, ...args]` as arguments and return the first non-null result. + */ +function argMatchRun2(expr: ColorExpression, color: vscode.Color, fns: ((this: U, expr: ColorExpression, color: vscode.Color, ...args: Arg[]) => T)[], thisArg?: U): T { + for (const fn of fns) { + if (expr.args.length === fn.length - 2) { + const result = fn.apply(thisArg, [expr, color, ...expr.args]); + if (result) { + return result; + } + } + } +} + +function interpretColor(): vscode.Color { + return COLOR_BLACK; +} + +function interpretColorRGB(red: Arg, green: Arg, blue: Arg): vscode.Color | undefined { + return interpretColorRGBA(red, green, blue, null); +} + +function interpretColorRGBA(red: Arg, green: Arg, blue: Arg, alpha: Arg | null): vscode.Color | undefined { + if (argIsNumber(red) && argIsNumber(green) && argIsNumber(blue) && argIsOptionalNumber(alpha)) { + return new vscode.Color(red.value, green.value, blue.value, alpha?.value ?? 1); + } +} + +function interpretColorCode(code: Arg): vscode.Color | undefined { + return interpretColorCodeAlpha(code, null); +} + +function interpretColorCodeAlpha(code: Arg, alpha: Arg | null): vscode.Color | undefined { + if (argIsString(code) && argIsOptionalNumber(alpha)) { + const color = parseCode(code.value); + return alpha + ? new vscode.Color(color.red, color.green, color.blue, alpha.value) + : color; + } +} + +function interpretColorFromRGB8(red8: Arg, green8: Arg, blue8: Arg): vscode.Color | undefined { + return interpretColorFromRGBA8(red8, green8, blue8, null); +} + +function interpretColorFromRGBA8(red8: Arg, green8: Arg, blue8: Arg, alpha8: Arg | null): vscode.Color | undefined { + if (argIsNumber(red8) && argIsNumber(green8) && argIsNumber(blue8) && argIsOptionalNumber(alpha8)) { + return new vscode.Color( + Math.floor(clamp(red8.value, 0, 255)) / 255, + Math.floor(clamp(green8.value, 0, 255)) / 255, + Math.floor(clamp(blue8.value, 0, 255)) / 255, + alpha8 ? Math.floor(clamp(alpha8.value, 0, 255)) / 255 : 1 + ); + } +} + +function interpretColorHex(arg: Arg): vscode.Color | undefined { + if (argIsNumber(arg)) { + return hex(arg.value); + } +} + +function replaceArgs(expr: string, args: Arg[], newArgs: any[]): string { + let offset = 0; + let newExpr = ""; + for (let i = 0; i < args.length; i++) { + const arg = args[i]; + const newArg = newArgs[i]; + newExpr += expr.slice(offset, arg.range[0]); + newExpr += `${newArg}`; + offset = arg.range[1]; + } + newExpr += expr.slice(offset); + return newExpr; +} + +function parseCode(code: string): vscode.Color { + let html = code.match(RE_COLOR_CODE_HTML)?.groups?.html; + if (html) { + if (html.length === 3) { + // RGB(3) to RRGGBB(6) + html = html.replaceAll(/\w/g, "$&$&"); + } + if (html.length === 4) { + // RBGA(4) to RRGGBBAA(8) + html = html.replaceAll(/\w/g, "$&$&"); + } + if (html.length === 6) { + // RRGGBB(6) to RRGGBBAA(8) + html += "FF"; + } + if (html.length === 8) { + // RRGGBBAA(8) + return hex(Number.parseInt(html, 16)); + } + } + const constant = code.match(RE_COLOR_CODE_CONSTANT)?.groups?.constant; + if (constant && NAMED_COLORS[constant]) { + return NAMED_COLORS[constant]; + } + return COLOR_BLACK; +} + +function clamp(value: number, min: number, max: number): number { + return Math.max(min, Math.min(value, max)); +} diff --git a/src/providers/index.ts b/src/providers/index.ts index 4b50d703b..024f25d37 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1,5 +1,6 @@ export * from "./completions"; export * from "./definition"; +export * from "./document_colors"; export * from "./document_drops"; export * from "./document_link"; export * from "./documentation"; diff --git a/src/utils/colors.ts b/src/utils/colors.ts new file mode 100644 index 000000000..5805cd4be --- /dev/null +++ b/src/utils/colors.ts @@ -0,0 +1,213 @@ +import * as vscode from "vscode"; + +export interface Color8 { + r8: number; + g8: number; + b8: number; + a8: number; +} + +export function vscodeColorsAreEqual(a?: vscode.Color, b?: vscode.Color): boolean { + return ( + a !== null && + a !== undefined && + b !== null && + b !== undefined && + a.red === b.red && + a.green === b.green && + a.blue === b.blue && + a.alpha === b.alpha + ); +} + +export function to_html(color: vscode.Color): string { + let hex = to_rgba32(color); + if (hex & 0xFFn) { + hex >>= 8n; + return hex.toString(16).padStart(6, "0"); + } + return hex.toString(16).padStart(8, "0"); +} + +/** + * Copied from https://github.com/godotengine/godot/blob/1aaea38e7f8dd9e2a94e2e339247fc976a84de75/core/math/color.cpp#L64-L74 + */ +export function to_rgba32(color: vscode.Color): bigint { + /** We need to use BigInts because otherwise JS will interpret `c` as a 32-bit signed integer when doing bitwise-operations */ + let c = BigInt(Math.round(color.red * 255)); + c <<= 8n; + c |= BigInt(Math.round(color.green * 255)); + c <<= 8n; + c |= BigInt(Math.round(color.blue * 255)); + c <<= 8n; + c |= BigInt(Math.round(color.alpha * 255)); + return c; +} + +/** + * Copied from https://github.com/godotengine/godot/blob/1aaea38e7f8dd9e2a94e2e339247fc976a84de75/core/math/color.cpp#L284-L294 + */ +export function hex(p_hex: number | bigint): vscode.Color { + let hex = BigInt(p_hex); + const a = Number(hex & 0xFFn) / 255; + hex >>= 8n; + const b = Number(hex & 0xFFn) / 255; + hex >>= 8n; + const g = Number(hex & 0xFFn) / 255; + hex >>= 8n; + const r = Number(hex & 0xFFn) / 255; + hex >>= 8n; + return new vscode.Color(r, g, b, a); +} + +/** + * Copied and reformated from https://github.com/godotengine/godot/blob/1aaea38e7f8dd9e2a94e2e339247fc976a84de75/core/math/color_names.inc#L49-L196 + */ +export const NAMED_COLORS = { + ALICE_BLUE: hex(0xF0F8FFFF), + ANTIQUE_WHITE: hex(0xFAEBD7FF), + AQUA: hex(0x00FFFFFF), + AQUAMARINE: hex(0x7FFFD4FF), + AZURE: hex(0xF0FFFFFF), + BEIGE: hex(0xF5F5DCFF), + BISQUE: hex(0xFFE4C4FF), + BLACK: hex(0x000000FF), + BLANCHED_ALMOND: hex(0xFFEBCDFF), + BLUE: hex(0x0000FFFF), + BLUE_VIOLET: hex(0x8A2BE2FF), + BROWN: hex(0xA52A2AFF), + BURLYWOOD: hex(0xDEB887FF), + CADET_BLUE: hex(0x5F9EA0FF), + CHARTREUSE: hex(0x7FFF00FF), + CHOCOLATE: hex(0xD2691EFF), + CORAL: hex(0xFF7F50FF), + CORNFLOWER_BLUE: hex(0x6495EDFF), + CORNSILK: hex(0xFFF8DCFF), + CRIMSON: hex(0xDC143CFF), + CYAN: hex(0x00FFFFFF), + DARK_BLUE: hex(0x00008BFF), + DARK_CYAN: hex(0x008B8BFF), + DARK_GOLDENROD: hex(0xB8860BFF), + DARK_GRAY: hex(0xA9A9A9FF), + DARK_GREEN: hex(0x006400FF), + DARK_KHAKI: hex(0xBDB76BFF), + DARK_MAGENTA: hex(0x8B008BFF), + DARK_OLIVE_GREEN: hex(0x556B2FFF), + DARK_ORANGE: hex(0xFF8C00FF), + DARK_ORCHID: hex(0x9932CCFF), + DARK_RED: hex(0x8B0000FF), + DARK_SALMON: hex(0xE9967AFF), + DARK_SEA_GREEN: hex(0x8FBC8FFF), + DARK_SLATE_BLUE: hex(0x483D8BFF), + DARK_SLATE_GRAY: hex(0x2F4F4FFF), + DARK_TURQUOISE: hex(0x00CED1FF), + DARK_VIOLET: hex(0x9400D3FF), + DEEP_PINK: hex(0xFF1493FF), + DEEP_SKY_BLUE: hex(0x00BFFFFF), + DIM_GRAY: hex(0x696969FF), + DODGER_BLUE: hex(0x1E90FFFF), + FIREBRICK: hex(0xB22222FF), + FLORAL_WHITE: hex(0xFFFAF0FF), + FOREST_GREEN: hex(0x228B22FF), + FUCHSIA: hex(0xFF00FFFF), + GAINSBORO: hex(0xDCDCDCFF), + GHOST_WHITE: hex(0xF8F8FFFF), + GOLD: hex(0xFFD700FF), + GOLDENROD: hex(0xDAA520FF), + GRAY: hex(0xBEBEBEFF), + GREEN: hex(0x00FF00FF), + GREEN_YELLOW: hex(0xADFF2FFF), + HONEYDEW: hex(0xF0FFF0FF), + HOT_PINK: hex(0xFF69B4FF), + INDIAN_RED: hex(0xCD5C5CFF), + INDIGO: hex(0x4B0082FF), + IVORY: hex(0xFFFFF0FF), + KHAKI: hex(0xF0E68CFF), + LAVENDER: hex(0xE6E6FAFF), + LAVENDER_BLUSH: hex(0xFFF0F5FF), + LAWN_GREEN: hex(0x7CFC00FF), + LEMON_CHIFFON: hex(0xFFFACDFF), + LIGHT_BLUE: hex(0xADD8E6FF), + LIGHT_CORAL: hex(0xF08080FF), + LIGHT_CYAN: hex(0xE0FFFFFF), + LIGHT_GOLDENROD: hex(0xFAFAD2FF), + LIGHT_GRAY: hex(0xD3D3D3FF), + LIGHT_GREEN: hex(0x90EE90FF), + LIGHT_PINK: hex(0xFFB6C1FF), + LIGHT_SALMON: hex(0xFFA07AFF), + LIGHT_SEA_GREEN: hex(0x20B2AAFF), + LIGHT_SKY_BLUE: hex(0x87CEFAFF), + LIGHT_SLATE_GRAY: hex(0x778899FF), + LIGHT_STEEL_BLUE: hex(0xB0C4DEFF), + LIGHT_YELLOW: hex(0xFFFFE0FF), + LIME: hex(0x00FF00FF), + LIME_GREEN: hex(0x32CD32FF), + LINEN: hex(0xFAF0E6FF), + MAGENTA: hex(0xFF00FFFF), + MAROON: hex(0xB03060FF), + MEDIUM_AQUAMARINE: hex(0x66CDAAFF), + MEDIUM_BLUE: hex(0x0000CDFF), + MEDIUM_ORCHID: hex(0xBA55D3FF), + MEDIUM_PURPLE: hex(0x9370DBFF), + MEDIUM_SEA_GREEN: hex(0x3CB371FF), + MEDIUM_SLATE_BLUE: hex(0x7B68EEFF), + MEDIUM_SPRING_GREEN: hex(0x00FA9AFF), + MEDIUM_TURQUOISE: hex(0x48D1CCFF), + MEDIUM_VIOLET_RED: hex(0xC71585FF), + MIDNIGHT_BLUE: hex(0x191970FF), + MINT_CREAM: hex(0xF5FFFAFF), + MISTY_ROSE: hex(0xFFE4E1FF), + MOCCASIN: hex(0xFFE4B5FF), + NAVAJO_WHITE: hex(0xFFDEADFF), + NAVY_BLUE: hex(0x000080FF), + OLD_LACE: hex(0xFDF5E6FF), + OLIVE: hex(0x808000FF), + OLIVE_DRAB: hex(0x6B8E23FF), + ORANGE: hex(0xFFA500FF), + ORANGE_RED: hex(0xFF4500FF), + ORCHID: hex(0xDA70D6FF), + PALE_GOLDENROD: hex(0xEEE8AAFF), + PALE_GREEN: hex(0x98FB98FF), + PALE_TURQUOISE: hex(0xAFEEEEFF), + PALE_VIOLET_RED: hex(0xDB7093FF), + PAPAYA_WHIP: hex(0xFFEFD5FF), + PEACH_PUFF: hex(0xFFDAB9FF), + PERU: hex(0xCD853FFF), + PINK: hex(0xFFC0CBFF), + PLUM: hex(0xDDA0DDFF), + POWDER_BLUE: hex(0xB0E0E6FF), + PURPLE: hex(0xA020F0FF), + REBECCA_PURPLE: hex(0x663399FF), + RED: hex(0xFF0000FF), + ROSY_BROWN: hex(0xBC8F8FFF), + ROYAL_BLUE: hex(0x4169E1FF), + SADDLE_BROWN: hex(0x8B4513FF), + SALMON: hex(0xFA8072FF), + SANDY_BROWN: hex(0xF4A460FF), + SEA_GREEN: hex(0x2E8B57FF), + SEASHELL: hex(0xFFF5EEFF), + SIENNA: hex(0xA0522DFF), + SILVER: hex(0xC0C0C0FF), + SKY_BLUE: hex(0x87CEEBFF), + SLATE_BLUE: hex(0x6A5ACDFF), + SLATE_GRAY: hex(0x708090FF), + SNOW: hex(0xFFFAFAFF), + SPRING_GREEN: hex(0x00FF7FFF), + STEEL_BLUE: hex(0x4682B4FF), + TAN: hex(0xD2B48CFF), + TEAL: hex(0x008080FF), + THISTLE: hex(0xD8BFD8FF), + TOMATO: hex(0xFF6347FF), + TRANSPARENT: hex(0xFFFFFF00), + TURQUOISE: hex(0x40E0D0FF), + VIOLET: hex(0xEE82EEFF), + WEB_GRAY: hex(0x808080FF), + WEB_GREEN: hex(0x008000FF), + WEB_MAROON: hex(0x800000FF), + WEB_PURPLE: hex(0x800080FF), + WHEAT: hex(0xF5DEB3FF), + WHITE: hex(0xFFFFFFFF), + WHITE_SMOKE: hex(0xF5F5F5FF), + YELLOW: hex(0xFFFF00FF), + YELLOW_GREEN: hex(0x9ACD32FF), +} satisfies Record; diff --git a/src/utils/vscode_utils.ts b/src/utils/vscode_utils.ts index 899669587..8c0a82ab5 100644 --- a/src/utils/vscode_utils.ts +++ b/src/utils/vscode_utils.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; import { globals } from "../extension"; -const EXTENSION_PREFIX = "godotTools"; +export const EXTENSION_PREFIX = "godotTools"; export function get_configuration(name: string, defaultValue?: any) { const configValue = vscode.workspace.getConfiguration(EXTENSION_PREFIX).get(name, null); From 7c5d789a511f3f94a18e2c8355379bff79343d7a Mon Sep 17 00:00:00 2001 From: Paul Marechal Date: Wed, 25 Mar 2026 18:09:56 -0400 Subject: [PATCH 2/3] revert accidental autoformatting --- src/extension.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 829b2205b..abcc9b946 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -104,14 +104,14 @@ async function initial_setup() { break; } case "WRONG_VERSION": { - const message = `The specified Godot executable, '${godotPath}' is the wrong version. + const message = `The specified Godot executable, '${godotPath}' is the wrong version. The current project uses Godot v${projectVersion}, but the specified executable is Godot v${result.version}. Extension features will not work correctly unless this is fixed.`; prompt_for_godot_executable(message, settingName); break; } case "INVALID_EXE": { - const message = `The specified Godot executable, '${godotPath}' is invalid. + const message = `The specified Godot executable, '${godotPath}' is invalid. Extension features will not work correctly unless this is fixed.`; prompt_for_godot_executable(message, settingName); break; From 8a38f548ff01f5fd01d86be927c3980ef7817f7c Mon Sep 17 00:00:00 2001 From: Paul Marechal Date: Wed, 25 Mar 2026 18:21:41 -0400 Subject: [PATCH 3/3] async cleanup The idea is to break the potentially expensive for-loops to potentially catch cancellation requests. After some reading `setImmediate` should be better suited for this than `await Promise.resolve()` --- src/providers/document_colors.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/providers/document_colors.ts b/src/providers/document_colors.ts index aad44ad6d..c942f66ca 100644 --- a/src/providers/document_colors.ts +++ b/src/providers/document_colors.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; -import { Color8, hex, NAMED_COLORS, to_html, to_rgba32 } from "../utils/colors"; import { EXTENSION_PREFIX } from "../utils"; +import { Color8, NAMED_COLORS, hex, to_html, to_rgba32 } from "../utils/colors"; type Arg = | ArgString @@ -144,9 +144,7 @@ export class GDDocumentColorProvider implements vscode.DocumentColorProvider { if (color) { colors.push({ color, range }); } - // allow for early bail out - await Promise.resolve(); - if (token.isCancellationRequested) { + if (await isCancelled(token)) { return colors; } } @@ -159,9 +157,7 @@ export class GDDocumentColorProvider implements vscode.DocumentColorProvider { if (color) { colors.push({ color, range }); } - // allow for early bail out - await Promise.resolve(); - if (token.isCancellationRequested) { + if (await isCancelled(token)) { return colors; } } @@ -543,3 +539,10 @@ function parseCode(code: string): vscode.Color { function clamp(value: number, min: number, max: number): number { return Math.max(min, Math.min(value, max)); } + +/** + * To be used within a loop to not monopolize the eventloop and check for cancellation. + */ +function isCancelled(token: vscode.CancellationToken): Promise { + return new Promise(resolve => setImmediate(() => resolve(token.isCancellationRequested))); +}