diff --git a/package-lock.json b/package-lock.json index 262c02c4f..f6bfadfcd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,31 +1,31 @@ { "name": "bridge", - "version": "2.4.0", + "version": "2.4.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "bridge", - "version": "2.4.0", + "version": "2.4.1", "dependencies": { "@mdi/font": "^6.9.96", "@types/lz-string": "^1.3.34", "bridge-common-utils": "^0.3.3", "bridge-iframe-api": "^0.4.11", - "bridge-js-runtime": "^0.3.6", + "bridge-js-runtime": "^0.4.1", "bridge-model-viewer": "^0.7.7", "buffer": "^6.0.3", "color-convert": "^2.0.1", "comlink": "^4.3.0", "compare-versions": "^3.6.0", "core-js": "^3.6.5", - "dash-compiler": "^0.10.7", + "dash-compiler": "^0.10.8", "escape-string-regexp": "^5.0.0", "fflate": "^0.6.7", "idb-keyval": "^5.1.3", "is-glob": "^4.0.1", "json5": "^2.1.3", - "jsonc-parser": "^3.0.0", + "jsonc-parser": "^3.2.0", "lodash-es": "^4.17.20", "lz-string": "^1.4.4", "mc-project-core": "^0.3.22", @@ -2519,9 +2519,9 @@ "integrity": "sha512-+gt/4+KkEhtg9vvWI99empq8jOukIAe9nUH9CVdFTXYj4ZLxIqjfBi0XdxyAFKWIapmQDCQA17Wx5d8bBuG4rw==" }, "node_modules/bridge-js-runtime": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/bridge-js-runtime/-/bridge-js-runtime-0.3.7.tgz", - "integrity": "sha512-ACJYg3grVdtkQRERcTNOojTydwOsYf4354g6H5MW6lnO/WM+hQCdOxVOW02ciaXpRvE5R0t3xCVh1KBQ1PWYdg==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/bridge-js-runtime/-/bridge-js-runtime-0.4.1.tgz", + "integrity": "sha512-SAH7CXMdufqRrGrN3pJ+LBiGctQNTwamrLK+DiZCQKFMX4doESNvPbbfBTJlcqWB/rlpgL5hXI/NBGPm4nP3Jw==", "dependencies": { "@swc/wasm-web": "^1.2.218", "json5": "^2.2.1", @@ -2831,13 +2831,13 @@ "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, "node_modules/dash-compiler": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.10.7.tgz", - "integrity": "sha512-oZLiyaU9Jh2qEYdCkC9s8el1qCww3IZ4MxlM3tpmNRqvHNEtYZmUnsK93PgZiyj8SzMaXtwwS6LEQCsWwpt6nQ==", + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.10.8.tgz", + "integrity": "sha512-sWQdC2T99TSG5Gaj/ZIpMF1jCSD0TIllVzZi/166ZHOjrjcKeDjZhryveowvu5HhrVOOtdsnfpzYRtKqqlummQ==", "dependencies": { "@swc/wasm-web": "^1.2.218", "bridge-common-utils": "^0.3.0", - "bridge-js-runtime": "^0.3.7", + "bridge-js-runtime": "^0.4.1", "is-glob": "^4.0.3", "json5": "^2.2.0", "mc-project-core": "^0.3.22", @@ -4371,8 +4371,9 @@ } }, "node_modules/jsonc-parser": { - "version": "3.0.0", - "license": "MIT" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "node_modules/jsonfile": { "version": "6.1.0", @@ -7997,9 +7998,9 @@ "integrity": "sha512-+gt/4+KkEhtg9vvWI99empq8jOukIAe9nUH9CVdFTXYj4ZLxIqjfBi0XdxyAFKWIapmQDCQA17Wx5d8bBuG4rw==" }, "bridge-js-runtime": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/bridge-js-runtime/-/bridge-js-runtime-0.3.7.tgz", - "integrity": "sha512-ACJYg3grVdtkQRERcTNOojTydwOsYf4354g6H5MW6lnO/WM+hQCdOxVOW02ciaXpRvE5R0t3xCVh1KBQ1PWYdg==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/bridge-js-runtime/-/bridge-js-runtime-0.4.1.tgz", + "integrity": "sha512-SAH7CXMdufqRrGrN3pJ+LBiGctQNTwamrLK+DiZCQKFMX4doESNvPbbfBTJlcqWB/rlpgL5hXI/NBGPm4nP3Jw==", "requires": { "@swc/wasm-web": "^1.2.218", "json5": "^2.2.1", @@ -8196,13 +8197,13 @@ "integrity": "sha512-uX1KG+x9h5hIJsaKR9xHUeUraxf8IODOwq9JLNPq6BwB04a/xgpq3rcx47l5BZu5zBPlgD342tdke3Hom/nJRA==" }, "dash-compiler": { - "version": "0.10.7", - "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.10.7.tgz", - "integrity": "sha512-oZLiyaU9Jh2qEYdCkC9s8el1qCww3IZ4MxlM3tpmNRqvHNEtYZmUnsK93PgZiyj8SzMaXtwwS6LEQCsWwpt6nQ==", + "version": "0.10.8", + "resolved": "https://registry.npmjs.org/dash-compiler/-/dash-compiler-0.10.8.tgz", + "integrity": "sha512-sWQdC2T99TSG5Gaj/ZIpMF1jCSD0TIllVzZi/166ZHOjrjcKeDjZhryveowvu5HhrVOOtdsnfpzYRtKqqlummQ==", "requires": { "@swc/wasm-web": "^1.2.218", "bridge-common-utils": "^0.3.0", - "bridge-js-runtime": "^0.3.7", + "bridge-js-runtime": "^0.4.1", "is-glob": "^4.0.3", "json5": "^2.2.0", "mc-project-core": "^0.3.22", @@ -9063,7 +9064,9 @@ "version": "2.2.1" }, "jsonc-parser": { - "version": "3.0.0" + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==" }, "jsonfile": { "version": "6.1.0", diff --git a/package.json b/package.json index 3340d7bb7..7e76a1ee6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "bridge", - "version": "2.4.0", + "version": "2.4.1", "private": true, "scripts": { "dev": "vite", @@ -16,20 +16,20 @@ "@types/lz-string": "^1.3.34", "bridge-common-utils": "^0.3.3", "bridge-iframe-api": "^0.4.11", - "bridge-js-runtime": "^0.3.6", + "bridge-js-runtime": "^0.4.1", "bridge-model-viewer": "^0.7.7", "buffer": "^6.0.3", "color-convert": "^2.0.1", "comlink": "^4.3.0", "compare-versions": "^3.6.0", "core-js": "^3.6.5", - "dash-compiler": "^0.10.7", + "dash-compiler": "^0.10.8", "escape-string-regexp": "^5.0.0", "fflate": "^0.6.7", "idb-keyval": "^5.1.3", "is-glob": "^4.0.1", "json5": "^2.1.3", - "jsonc-parser": "^3.0.0", + "jsonc-parser": "^3.2.0", "lodash-es": "^4.17.20", "lz-string": "^1.4.4", "mc-project-core": "^0.3.22", diff --git a/src/components/Extensions/Scripts/JsRuntime.ts b/src/components/Extensions/Scripts/JsRuntime.ts index 81aae2753..8e2968fd2 100644 --- a/src/components/Extensions/Scripts/JsRuntime.ts +++ b/src/components/Extensions/Scripts/JsRuntime.ts @@ -7,9 +7,8 @@ export class JsRuntime extends Runtime { const app = await App.getApp() const file = await app.fileSystem.readFile(filePath) - const fileContent = await file.text() - return fileContent + return file } run(filePath: string, env: any = {}, fileContent?: string) { diff --git a/src/components/Languages/Json/ColorPicker/Color.ts b/src/components/Languages/Json/ColorPicker/Color.ts new file mode 100644 index 000000000..4e3a4560b --- /dev/null +++ b/src/components/Languages/Json/ColorPicker/Color.ts @@ -0,0 +1,81 @@ +import type { languages } from 'monaco-editor' +import { toTwoDigitHex } from './parse/hex' + +export class Color { + constructor(public colorInfo: languages.IColor) {} + + /** + * Formats the color as #RRGGBB + */ + toHex() { + return `#${toTwoDigitHex(this.colorInfo.red * 255)}${toTwoDigitHex( + this.colorInfo.green * 255 + )}${toTwoDigitHex(this.colorInfo.blue * 255)}` + } + + /** + * Formats the color as #RRGGBBAA + */ + toHexA() { + return `#${toTwoDigitHex(this.colorInfo.red * 255)}${toTwoDigitHex( + this.colorInfo.green * 255 + )}${toTwoDigitHex(this.colorInfo.blue * 255)}${toTwoDigitHex( + Math.round(this.colorInfo.alpha * 255) + )}` + } + + /** + * Formats the color as #AARRGGBB + */ + toAHex() { + return `#${toTwoDigitHex( + Math.round(this.colorInfo.alpha * 255) + )}${toTwoDigitHex(this.colorInfo.red * 255)}${toTwoDigitHex( + this.colorInfo.green * 255 + )}${toTwoDigitHex(this.colorInfo.blue * 255)}` + } + + /** + * Formats the color as [r, g, b], 0-1 + */ + toDecRgbArray() { + return [ + +this.colorInfo.red.toFixed(5), + +this.colorInfo.green.toFixed(5), + +this.colorInfo.blue.toFixed(5), + ] + } + /** + * Formats the color as [r, g, b], 0-255 + */ + toRgbArray() { + return [ + Math.round(this.colorInfo.red * 255), + Math.round(this.colorInfo.green * 255), + Math.round(this.colorInfo.blue * 255), + ] + } + + /** + * Formats the color as [r, g, b, a] 0-1 + */ + toDecRgbaArray() { + return [ + +this.colorInfo.red.toFixed(5), + +this.colorInfo.green.toFixed(5), + +this.colorInfo.blue.toFixed(5), + this.colorInfo.alpha, + ] + } + /** + * Formats the color as [r, g, b, a], 0-255 + */ + toRgbaArray() { + return [ + Math.round(this.colorInfo.red * 255), + Math.round(this.colorInfo.green * 255), + Math.round(this.colorInfo.blue * 255), + Math.round(this.colorInfo.alpha * 255), + ] + } +} diff --git a/src/components/Languages/Json/ColorPicker/ColorPicker.ts b/src/components/Languages/Json/ColorPicker/ColorPicker.ts new file mode 100644 index 000000000..af93a545d --- /dev/null +++ b/src/components/Languages/Json/ColorPicker/ColorPicker.ts @@ -0,0 +1,146 @@ +import { useMonaco } from '/@/utils/libs/useMonaco' +import type { editor, CancellationToken, languages } from 'monaco-editor' +import { Color } from './Color' +import { findColors } from './findColors' +import { parseColor } from './parse/main' + +export async function registerColorPicker() { + const { languages, Position, Range } = await useMonaco() + + languages.registerColorProvider('json', { + provideDocumentColors: async ( + model: editor.ITextModel, + token: CancellationToken + ) => { + return await findColors(model) + }, + provideColorPresentations: async ( + model: editor.ITextModel, + colorInfo: languages.IColorInformation, + token: CancellationToken + ) => { + const newColor = new Color(colorInfo.color) + const value = model.getValueInRange(colorInfo.range) + const position = new Position( + colorInfo.range.startLineNumber, + colorInfo.range.startColumn + 2 + ) + + // We need to decide which format this color is supposed to be in by doing 2 things: + // 1. Parse the value to find the format + // 2. Check the json path against valid locations to confirm the format, if necessary + + /** + * Takes an array as a string and inserts values into the array while preserving whitespace + */ + const insertToStringArray = (arr: string, newValues: any[]) => { + const valueTest = /(\d+(?:\.\d*)?)/gim + const split = arr.split(',') + let newArr = '' + // If the number of values to replace != the number of values available replace just return the orignal value + if (split.length !== newValues.length) return arr + for (const [i, value] of newValues.entries()) { + // Get the first value to replace + const toReplace = split[i].match(valueTest) + // If there is something to replace, replace it + if (toReplace && toReplace[0]) + newArr += `${split[i].replace(toReplace[0], value)}${ + i + 1 === newValues.length ? '' : ',' // Comma after value if not last element + }` + } + return newArr + } + + const { format } = await parseColor(value, { + model, + position, + }) + + switch (format) { + case 'hex': + return [ + { + label: `"${newColor.toHex().toUpperCase()}"`, + }, + ] + case 'hexa': + return [ + { + label: `"${newColor.toHexA().toUpperCase()}"`, + }, + ] + case 'ahex': + return [ + { + label: `"${newColor.toAHex().toUpperCase()}"`, + }, + ] + + case 'rgbDec': + return [ + { + label: `[${newColor.toDecRgbArray().join(', ')}]`, + textEdit: { + range: colorInfo.range, + text: insertToStringArray( + value, + newColor.toDecRgbArray() + ), + }, + }, + ] + case 'rgb': + return [ + { + label: `[${newColor.toRgbArray().join(', ')}]`, + textEdit: { + range: colorInfo.range, + text: insertToStringArray( + value, + newColor.toRgbArray() + ), + }, + }, + ] + case 'rgbaDec': + return [ + { + label: `[${newColor.toDecRgbaArray().join(', ')}]`, + textEdit: { + range: colorInfo.range, + text: insertToStringArray( + value, + newColor.toDecRgbaArray() + ), + }, + }, + ] + case 'rgba': + return [ + { + label: `[${newColor.toRgbaArray().join(', ')}]`, + textEdit: { + range: colorInfo.range, + text: insertToStringArray( + value, + newColor.toRgbaArray() + ), + }, + }, + ] + + default: + // If all fails, don't do anything to be safe + return [ + { + label: '', + textEdit: { + range: new Range(0, 0, 0, 0), + text: '', + }, + }, + ] + } + }, + }) +} diff --git a/src/components/Languages/Json/ColorPicker/Data.ts b/src/components/Languages/Json/ColorPicker/Data.ts new file mode 100644 index 000000000..c1269f01d --- /dev/null +++ b/src/components/Languages/Json/ColorPicker/Data.ts @@ -0,0 +1,34 @@ +import { markRaw } from 'vue' +import { App } from '/@/App' +import { Signal } from '/@/components/Common/Event/Signal' + +export class ColorData extends Signal { + protected _data?: any + + async loadColorData() { + const app = await App.getApp() + + this._data = markRaw( + await app.dataLoader.readJSON( + `data/packages/minecraftBedrock/location/validColor.json` + ) + ) + + this.dispatch() + } + + async getDataForCurrentTab() { + await this.fired + + const app = await App.getApp() + + const currentTab = app.project.tabSystem?.selectedTab + if (!currentTab) return {} + + // Get the file definition id of the currently opened tab + const id = App.fileType.getId(currentTab.getPath()) + + // Get the color locations for this file type + return this._data[id] + } +} diff --git a/src/components/Languages/Json/ColorPicker/findColors.ts b/src/components/Languages/Json/ColorPicker/findColors.ts new file mode 100644 index 000000000..0cc664fd2 --- /dev/null +++ b/src/components/Languages/Json/ColorPicker/findColors.ts @@ -0,0 +1,134 @@ +import { isMatch } from 'bridge-common-utils' +import type { JSONPath } from 'jsonc-parser' +import type { editor, languages } from 'monaco-editor' +import { useJsoncParser } from '/@/utils/libs/useJsoncParser' +import { useMonaco } from '/@/utils/libs/useMonaco' +import { getJsonWordAtPosition } from '/@/utils/monaco/getJsonWord' +import { getArrayValueAtOffset } from '/@/utils/monaco/getArrayValue' +import { parseColor } from './parse/main' +import { App } from '/@/App' +import { BedrockProject } from '/@/components/Projects/Project/BedrockProject' + +/** + * Takes a text model and detects the locations of colors in the file + */ +export async function findColors(model: editor.ITextModel) { + const { visit } = await useJsoncParser() + const { Range } = await useMonaco() + + const content = model.getValue() + + const app = await App.getApp() + const project = app.project + if (!(project instanceof BedrockProject)) return + + const locationPatterns = await project.colorData.getDataForCurrentTab() + const colorInfo: Promise[] = [] + + if (!locationPatterns) return [] + + // Walk through the json file + visit(content, { + // When we reach any literal value, e.g. a string, ... + onLiteralValue: async ( + value: any, + offset: number, + length: number, + startLine: number, + startCharacter: number, + pathSupplier: () => JSONPath + ) => { + // Call the path supplier and join the JSON segments into a path + const path = pathSupplier().join('/') + + // Iterate each color format for this file type + // Filter down to formats that are string only, as this is for literal values + for (const format of ['hex', 'hexa', 'ahex']) { + // Check whether the value at this JSON path matches a pattern in the valid colors file + if (!locationPatterns[format]) continue + const isValidColor = isMatch(path, locationPatterns[format]) + + // If this is a valid color, create a promise that will resolve when the color has been parsed and the range has been determined + if (isValidColor) { + colorInfo.push( + new Promise(async (resolve) => { + const position = model.getPositionAt(offset + 2) + const { color } = await parseColor(value, { + model, + position, + }) + const { range } = await getJsonWordAtPosition( + model, + position + ) + if (!color) return resolve(null) + + resolve({ + color: color.colorInfo, + range: new Range( + range.startLineNumber, + range.startColumn, + range.endLineNumber, + range.endColumn + 2 + ), + }) + }) + ) + break + } + } + }, + onArrayBegin( + offset: number, + length: number, + startLine: number, + startCharacter: number, + pathSupplier: () => JSONPath + ) { + // Handle similarly to when approaching a literal value + + // Call the path supplier and join the JSON segments into a path + const path = pathSupplier().join('/') + + // Iterate each color format for this file type + for (const format of ['rgb', 'rgba', 'rgbDec', 'rgbaDec']) { + // Check whether the value at this JSON path matches a pattern in the valid colors file + if (!locationPatterns[format]) continue + const isValidColor = isMatch(path, locationPatterns[format]) + + // If this is a valid color, create a promise that will resolve when the color has been parsed and the range has been determined + if (isValidColor) { + colorInfo.push( + new Promise(async (resolve) => { + const { range, word } = await getArrayValueAtOffset( + model, + offset + ) + const { color } = await parseColor(word, { + model, + position: model.getPositionAt(offset), + }) + if (!color) return resolve(null) + + resolve({ + color: color.colorInfo, + range: new Range( + range.startLineNumber, + range.startColumn, + range.endLineNumber, + range.endColumn + ), + }) + }) + ) + break + } + } + }, + }) + + // Await all promises for the color info and filter to ensure each color info contains the correct data + return ( + (await Promise.all(colorInfo)).filter((info) => info !== null) + ) +} diff --git a/src/components/Languages/Json/ColorPicker/parse/hex.ts b/src/components/Languages/Json/ColorPicker/parse/hex.ts new file mode 100644 index 000000000..fcbfa8a35 --- /dev/null +++ b/src/components/Languages/Json/ColorPicker/parse/hex.ts @@ -0,0 +1,51 @@ +import { Color } from '../Color' + +/** + * Convert a single hex digit to denary value + * @param index The index of the hex string to convert + */ +const fromHex = (hex: string, index: number) => + parseInt(hex.slice(index, index + 1), 16) + +export function parseHex(hex: string) { + const r = 16 * fromHex(hex, 1) + fromHex(hex, 2) + const g = 16 * fromHex(hex, 3) + fromHex(hex, 4) + const b = 16 * fromHex(hex, 5) + fromHex(hex, 6) + return new Color({ + red: r / 255, + green: g / 255, + blue: b / 255, + alpha: 1, + }) +} + +export function parseAHex(hex: string) { + const a = 16 * fromHex(hex, 1) + fromHex(hex, 2) + const r = 16 * fromHex(hex, 3) + fromHex(hex, 4) + const g = 16 * fromHex(hex, 5) + fromHex(hex, 6) + const b = 16 * fromHex(hex, 7) + fromHex(hex, 8) + return new Color({ + red: r / 255, + green: g / 255, + blue: b / 255, + alpha: a / 255, + }) +} + +export function parseHexA(hex: string) { + const r = 16 * fromHex(hex, 1) + fromHex(hex, 2) + const g = 16 * fromHex(hex, 3) + fromHex(hex, 4) + const b = 16 * fromHex(hex, 5) + fromHex(hex, 6) + const a = 16 * fromHex(hex, 7) + fromHex(hex, 8) + return new Color({ + red: r / 255, + green: g / 255, + blue: b / 255, + alpha: a / 255, + }) +} + +export function toTwoDigitHex(value: number) { + const hex = value.toString(16) + return hex.length !== 2 ? '0' + hex : hex +} diff --git a/src/components/Languages/Json/ColorPicker/parse/main.ts b/src/components/Languages/Json/ColorPicker/parse/main.ts new file mode 100644 index 000000000..2a293899c --- /dev/null +++ b/src/components/Languages/Json/ColorPicker/parse/main.ts @@ -0,0 +1,161 @@ +import { Color } from '../Color' +import { parseAHex, parseHex, parseHexA } from './hex' +import { parseRgbDec, parseRgb, parseRgbaDec, parseRgba } from './rgb' +import { getLocation } from '/@/utils/monaco/getLocation' +import type { editor, Position } from 'monaco-editor' +import { isMatch } from 'bridge-common-utils' +import { App } from '/@/App' +import { BedrockProject } from '/@/components/Projects/Project/BedrockProject' + +/** + * Takes a color value and some file context to figure out the format and color info + * @param value The string value of the color + * @param context The file context, including the text model and the position of the color + */ +export async function parseColor( + value: any, + context: { + model: editor.ITextModel + position: Position + } +): Promise<{ format: string; color?: Color }> { + const app = await App.getApp() + const project = app.project + if (!(project instanceof BedrockProject)) return { format: 'unknown' } + const colorData = project.colorData + + // Hex formats #RGB and #RGBA exist but don't appear in Minecraft, so we don't support parsing them + + // We should expect the value to have either no quotes or surrounding quotes + if ( + typeof value == 'string' && + value[0] === '"' && + value[value.length - 1] === '"' && + value[1] === '#' + ) + value = value.slice(1, -1) + + // Parse as a hex string, if this fails, parse as RGB array + if (value.startsWith('#')) { + switch (value.length) { + case 7: + return { + format: 'hex', + color: parseHex(value), + } + case 9: { + // Could either be hexa or ahex here, so we check with valid color data + const validColors = await colorData.getDataForCurrentTab() + // If either are valid in the file... + if (validColors) { + const location = await getLocation( + context.model, + context.position, + false + ) + // Check if hexa is valid at this location + if ( + validColors['hexa'] && + isMatch(location, validColors['hexa']) + ) + return { + format: 'hexa', + color: parseHexA(value), + } + // Check if ahex is valid at this location + if ( + validColors['ahex'] && + isMatch(location, validColors['ahex']) + ) + return { + format: 'ahex', + color: parseAHex(value), + } + } else { + // Otherwise, just default to hexa + return { + format: 'hexa', + color: parseHexA(value), + } + } + } + } + } + + // Parse as RGB, if this fails, just return unknown format + let raw + try { + raw = JSON.parse(value) + } catch { + return { + format: 'unknown', + } + } + + // Ignore if not an array or each value isn't a number + if (!Array.isArray(raw) || !raw.every((val) => typeof val === 'number')) + return { + format: 'unknown', + } + + if (raw.length === 3) { + // Could either be rgb or rgbDec here, so we check with valid color data + const validColors = await colorData.getDataForCurrentTab() + if (validColors) { + const location = await getLocation(context.model, context.position) + // Check if rgb is valid at this location + if (validColors['rgb'] && isMatch(location, validColors['rgb'])) + return { + format: 'rgb', + color: parseRgb(raw), + } + // Check if rgbDec is valid at this location + if ( + validColors['rgbDec'] && + isMatch(location, validColors['rgbDec']) + ) + return { + format: 'rgbDec', + color: parseRgbDec(raw), + } + } else { + // Otherwise, just default to rgb + return { + format: 'rgb', + color: parseRgb(raw), + } + } + } + if (raw.length === 4) { + // Could either be rgba or rgbaDec here, so we check with valid color data + const validColors = await colorData.getDataForCurrentTab() + if (validColors) { + const location = await getLocation(context.model, context.position) + // Check if rgba is valid at this location + if (validColors['rgba'] && isMatch(location, validColors['rgba'])) + return { + format: 'rgba', + color: parseRgba(raw), + } + // Check if rgbaDec is valid at this location + if ( + validColors['rgbaDec'] && + isMatch(location, validColors['rgbaDec']) + ) + return { + format: 'rgbaDec', + color: parseRgbaDec(raw), + } + } else { + // Otherwise, just default to rgba + return { + format: 'rgba', + color: parseRgba(raw), + } + } + } + + return { + format: 'unknown', + } +} diff --git a/src/components/Languages/Json/ColorPicker/parse/rgb.ts b/src/components/Languages/Json/ColorPicker/parse/rgb.ts new file mode 100644 index 000000000..c44f52f62 --- /dev/null +++ b/src/components/Languages/Json/ColorPicker/parse/rgb.ts @@ -0,0 +1,37 @@ +import { Color } from '../Color' + +export function parseRgb(rgbArr: number[]) { + return new Color({ + red: rgbArr[0] / 255, + green: rgbArr[1] / 255, + blue: rgbArr[2] / 255, + alpha: 1, + }) +} + +export function parseRgba(rgbArr: number[]) { + return new Color({ + red: rgbArr[0] / 255, + green: rgbArr[1] / 255, + blue: rgbArr[2] / 255, + alpha: rgbArr[3] / 255, + }) +} + +export function parseRgbDec(rgbArr: number[]) { + return new Color({ + red: rgbArr[0], + green: rgbArr[1], + blue: rgbArr[2], + alpha: 1, + }) +} + +export function parseRgbaDec(rgbArr: number[]) { + return new Color({ + red: rgbArr[0], + green: rgbArr[1], + blue: rgbArr[2], + alpha: rgbArr[3], + }) +} diff --git a/src/components/Languages/Json/Main.ts b/src/components/Languages/Json/Main.ts index d81a37d73..0a1766c1b 100644 --- a/src/components/Languages/Json/Main.ts +++ b/src/components/Languages/Json/Main.ts @@ -2,6 +2,7 @@ import { registerJsonSnippetProvider } from '/@/components/Snippets/Monaco' import { registerEmbeddedMcfunctionProvider } from '/@/components/Languages/Mcfunction/WithinJson' import { ConfiguredJsonHighlighter } from './Highlighter' import { useMonaco } from '../../../utils/libs/useMonaco' +import { registerColorPicker } from './ColorPicker/ColorPicker' export class ConfiguredJsonLanguage { protected highlighter = new ConfiguredJsonHighlighter() @@ -10,6 +11,7 @@ export class ConfiguredJsonLanguage { this.setModeConfiguration() registerEmbeddedMcfunctionProvider() registerJsonSnippetProvider() + registerColorPicker() } getHighlighter() { diff --git a/src/components/Projects/Project/BedrockProject.ts b/src/components/Projects/Project/BedrockProject.ts index 5a2e6ceee..673c34c09 100644 --- a/src/components/Projects/Project/BedrockProject.ts +++ b/src/components/Projects/Project/BedrockProject.ts @@ -11,6 +11,7 @@ import { CommandData } from '/@/components/Languages/Mcfunction/Data' import { FileTab } from '../../TabSystem/FileTab' import { HTMLPreviewTab } from '../../Editors/HTMLPreview/HTMLPreview' import { LangData } from '/@/components/Languages/Lang/Data' +import { ColorData } from '../../Languages/Json/ColorPicker/Data' const bedrockPreviews: ITabPreviewConfig[] = [ { @@ -56,6 +57,7 @@ const bedrockPreviews: ITabPreviewConfig[] = [ export class BedrockProject extends Project { commandData = new CommandData() langData = new LangData() + colorData = new ColorData() onCreate() { bedrockPreviews.forEach((tabPreview) => @@ -87,6 +89,7 @@ export class BedrockProject extends Project { this.commandData.loadCommandData('minecraftBedrock') this.langData.loadLangData('minecraftBedrock') + this.colorData.loadColorData() } getCurrentDataPackage() { diff --git a/src/components/UIElements/DirectoryViewer/DirectoryView/DirectoryView.vue b/src/components/UIElements/DirectoryViewer/DirectoryView/DirectoryView.vue index 8f1efe0d3..d1b6eb3a2 100644 --- a/src/components/UIElements/DirectoryViewer/DirectoryView/DirectoryView.vue +++ b/src/components/UIElements/DirectoryViewer/DirectoryView/DirectoryView.vue @@ -16,7 +16,7 @@ 0; i--) { + if (content[i] === '[') return i + } + return 0 +} diff --git a/src/utils/monaco/getJsonWord.ts b/src/utils/monaco/getJsonWord.ts index ca034eba6..195e1002d 100644 --- a/src/utils/monaco/getJsonWord.ts +++ b/src/utils/monaco/getJsonWord.ts @@ -1,6 +1,12 @@ import type { editor, Position } from 'monaco-editor' import { useMonaco } from '../libs/useMonaco' +/** + * Gets the range and word of a json string from a position in a text model + * @param model The text model that the string is in + * @param position The position inside of the json string to get + * @returns An object with 'word' and 'range' properties, containing the word in the json string and the range, in the model, of the string. NOTE - the column is zero-based so when using this to set monaco editor markers the columns should be adjusted to represent the entire json word + */ export async function getJsonWordAtPosition( model: editor.ITextModel, position: Position diff --git a/src/utils/monaco/getLocation.ts b/src/utils/monaco/getLocation.ts index cf6a6b2aa..a46bd4511 100644 --- a/src/utils/monaco/getLocation.ts +++ b/src/utils/monaco/getLocation.ts @@ -3,7 +3,8 @@ import { useJsoncParser } from '../libs/useJsoncParser' export async function getLocation( model: editor.ITextModel, - position: Position + position: Position, + removeFinalIndex = true ): Promise { const { getLocation: jsoncGetLocation } = await useJsoncParser() const locationArr = jsoncGetLocation( @@ -12,7 +13,10 @@ export async function getLocation( ).path // Lightning cache definition implicitly indexes arrays so we need to remove indexes if they are at the last path position - if (!isNaN(Number(locationArr[locationArr.length - 1]))) { + if ( + removeFinalIndex && + !isNaN(Number(locationArr[locationArr.length - 1])) + ) { locationArr.pop() }