diff --git a/package-lock.json b/package-lock.json index b6e4914..f3eed10 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "mapcompletevscode", - "version": "0.0.1", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "mapcompletevscode", - "version": "0.0.1", + "version": "1.0.0", "license": "MIT", "dependencies": { + "colortranslator": "^4.1.0", "jsonc-parser": "^3.3.1" }, "devDependencies": { @@ -673,6 +674,12 @@ "dev": true, "license": "MIT" }, + "node_modules/colortranslator": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/colortranslator/-/colortranslator-4.1.0.tgz", + "integrity": "sha512-bwa5awaMnQ6dpm9D3nbsFwUr6x6FrTKmxPdolNtSYfxCNR7ZM93GG1OF5Y3Sy1LvYdalb3riKC9uTn0X5NB36g==", + "license": "Apache-2.0" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", diff --git a/package.json b/package.json index 5e5c5a5..6438eba 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "typescript-eslint": "^8.16.0" }, "dependencies": { + "colortranslator": "^4.1.0", "jsonc-parser": "^3.3.1" } -} \ No newline at end of file +} diff --git a/src/extension.ts b/src/extension.ts index 235d6d3..cf0b5d1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,6 +1,6 @@ import * as vscode from "vscode"; import { layerCompletionProvider, layerDefinitionProvider } from "./theme"; -import { iconDefinitionProvider } from "./generic"; +import { colorProvider, iconDefinitionProvider } from "./generic"; import { filterCompletionProvider, filterDefinitionProvider, @@ -25,5 +25,5 @@ export function activate(context: vscode.ExtensionContext) { context.subscriptions.push(pathDefinitionProvider); // Activate all generic features - context.subscriptions.push(iconDefinitionProvider); + context.subscriptions.push(iconDefinitionProvider, colorProvider); } diff --git a/src/generic.ts b/src/generic.ts index e98278d..fa4fb8f 100644 --- a/src/generic.ts +++ b/src/generic.ts @@ -8,7 +8,9 @@ import { getRawCursorPath, getStartEnd, getValueFromPath, + pathToJSONPath, } from "./utils"; +import { ColorTranslator } from "colortranslator"; import * as path from "path"; /** @@ -78,3 +80,114 @@ export const iconDefinitionProvider = }, } ); + +/** + * Color provider + * + * Some fields in MapComplete actually represent colors, this provider will provide a color presentation for these fields + * Colours can be represented in two ways: + * - As a hex color, like #ff0000ff or #ff0000 + * - As a color name, like red + * + * JSON paths: + * - (layers.{index}.)lineRendering.{index}.color + * - (layers.{index}.)lineRendering.{index}.color.render + * - (layers.{index}.)lineRendering.{index}.color.mappings.{index}.then + * - (layers.{index}.)lineRendering.{index}.fillColor + * - (layers.{index}.)lineRendering.{index}.fillColor.render + * - (layers.{index}.)lineRendering.{index}.fillColor.mappings.{index}.then + * - (layers.{index}.)pointRendering.{index}.marker.{index}.color + * - (layers.{index}.)pointRendering.{index}.marker.{index}.color.render + * - (layers.{index}.)pointRendering.{index}.marker.{index}.color.mappings.{index}.then + * + */ +export const colorProvider = vscode.languages.registerColorProvider( + { + language: "json", + scheme: "file", + pattern: "**/assets/*/*/*.json", + }, + { + provideColorPresentations(color, _context, _token) { + console.log("colorProvider.provideColorPresentations"); + + const colorHex = new ColorTranslator( + `rgba(${color.red * 255}, ${color.green * 255}, ${color.blue * 255}, ${ + color.alpha + })` + ).HEXA; + + // If the color is fully opaque (AKA it ends with FF), we can remove the alpha channel + const colorHexString = colorHex.endsWith("FF") + ? colorHex.substring(0, 7) + : colorHex; + + // TODO: Add color names, maybe convert to short-hand hex + + return [ + { + label: colorHexString, + }, + ]; + }, + provideDocumentColors(document, _token) { + console.log("colorProvider.provideDocumentColors"); + + const text = document.getText(); + const colors: vscode.ColorInformation[] = []; + const jsonParsed = JSON.parse(text); + const allPaths: string[] = []; + + // Find all paths in the JSON + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const findPaths = (obj: any, path = "") => { + for (const key in obj) { + if (typeof obj[key] === "object") { + findPaths(obj[key], path + key + "."); + } else { + allPaths.push(path + key); + } + } + }; + findPaths(jsonParsed); + + const regexes = [ + /^(layers.\d+.)?lineRendering.\d+.color((.render)|(.mappings.\d+.then))?$/, + /^(layers.\d+.)?lineRendering.\d+.fillColor((.render)|(.mappings.\d+.then))?$/, + /^(layers.\d+.)?pointRendering.\d+.marker.\d+.color((.render)|(.mappings.\d+.then))?$/, + ]; + + regexes.forEach((regex) => { + allPaths.forEach((path) => { + if (regex.exec(path)) { + const colorValue = getValueFromPath(text, pathToJSONPath(path)); + + let colorRgba; + try { + colorRgba = new ColorTranslator(colorValue).RGBAObject; + } catch (error) { + console.error("Error translating color value:", error); + return; + } + + console.log( + `Output color: ${colorRgba.R}, ${colorRgba.G}, ${colorRgba.B}, ${colorRgba.A}` + ); + + colors.push({ + color: new vscode.Color( + colorRgba.R / 255, + colorRgba.G / 255, + colorRgba.B / 255, + colorRgba.A ?? 1 + ), + range: getStartEnd(text, pathToJSONPath(path)), + }); + } + }); + }); + + return colors; + }, + } +); diff --git a/src/utils.ts b/src/utils.ts index 15c1346..c1324e4 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -219,3 +219,15 @@ export async function getFilters(): Promise { return filtersList; } + +/** + * Utility function to convert a string path to a JSON path + * + * @param path String path, separated by dots + * @returns JSON path + */ +export function pathToJSONPath(path: string): JSONPath { + return path.split(".").map((str) => { + return isNaN(parseInt(str)) ? str : parseInt(str); + }); +}