diff --git a/packages/core/src/processor/colorizer/Colorizer.ts b/packages/core/src/processor/colorizer/Colorizer.ts index 8848824f2..9138267dc 100644 --- a/packages/core/src/processor/colorizer/Colorizer.ts +++ b/packages/core/src/processor/colorizer/Colorizer.ts @@ -60,6 +60,7 @@ export const ColorTokenTypes = Object.freeze( 'comment', 'enum', 'enumMember', + 'escape', 'function', 'keyword', 'modifier', diff --git a/packages/core/src/symbol/Symbol.ts b/packages/core/src/symbol/Symbol.ts index 5fb8118d2..d03f99656 100644 --- a/packages/core/src/symbol/Symbol.ts +++ b/packages/core/src/symbol/Symbol.ts @@ -238,6 +238,7 @@ export const AssetsMiscCategories = Object.freeze( [ 'texture_slot', 'shader_target', + 'translation_key', ] as const, ) export type AssetsMiscCategory = (typeof AssetsMiscCategories)[number] diff --git a/packages/discord-bot/src/index.ts b/packages/discord-bot/src/index.ts index f598dc7fa..f248b1023 100644 --- a/packages/discord-bot/src/index.ts +++ b/packages/discord-bot/src/index.ts @@ -232,6 +232,7 @@ const ColorTokenTypeLegend: Record> = { enum: new Set(['foreground_white']), enumMember: new Set(['foreground_white']), error: new Set(['foreground_red', 'underline']), + escape: new Set(['foreground_green']), function: new Set(['foreground_yellow']), keyword: new Set(['foreground_pink']), literal: new Set(['foreground_blue']), diff --git a/packages/java-edition/src/json/binder/index.ts b/packages/java-edition/src/json/binder/index.ts new file mode 100644 index 000000000..480b75dcc --- /dev/null +++ b/packages/java-edition/src/json/binder/index.ts @@ -0,0 +1,49 @@ +import * as core from '@spyglassmc/core' +import * as json from '@spyglassmc/json' + +function bindDeprecated(node: json.JsonObjectNode, ctx: core.BinderContext) { + const renamed = node.children.find(p => p.key?.value === 'renamed')?.value + if (json.JsonObjectNode.is(renamed)) { + for (const pair of renamed.children) { + if (json.JsonStringNode.is(pair.value)) { + const range = core.Range.translate(pair.value.range, 1, -1) + ctx.symbols.query(ctx.doc, 'translation_key', pair.value.value) + .enter({ + usage: { type: 'definition', range, fullRange: pair }, + }) + } + } + } +} + +function bindLanguage(node: json.JsonObjectNode, ctx: core.BinderContext) { + const isEnglish = ctx.doc.uri.endsWith('/en_us.json') + for (const pair of node.children) { + if (pair.key) { + const desc = json.JsonStringNode.is(pair.value) ? pair.value.value : undefined + const range = core.Range.translate(pair.key.range, 1, -1) + ctx.symbols.query(ctx.doc, 'translation_key', pair.key.value) + .enter({ + data: { desc: isEnglish ? desc : undefined }, + usage: { type: 'definition', range, fullRange: pair }, + }) + } + } +} + +const file: core.SyncBinder = (node, ctx) => { + if (ctx.doc.uri.match(/\/lang\/[a-z_]+.json$/)) { + const child = node.children[0] + if (json.JsonObjectNode.is(child)) { + if (ctx.doc.uri.endsWith('/deprecated.json')) { + bindDeprecated(child, ctx) + } else { + bindLanguage(child, ctx) + } + } + } +} + +export function register(meta: core.MetaRegistry) { + meta.registerBinder('json:file', file) +} diff --git a/packages/java-edition/src/json/index.ts b/packages/java-edition/src/json/index.ts index d61202bc5..53d01e7f3 100644 --- a/packages/java-edition/src/json/index.ts +++ b/packages/java-edition/src/json/index.ts @@ -1,6 +1,7 @@ /* istanbul ignore file */ import type * as core from '@spyglassmc/core' +import * as binder from './binder/index.js' import * as checker from './checker/index.js' import * as completer from './completer/index.js' import { registerMcdocAttributes } from './mcdocAttributes.js' @@ -8,6 +9,7 @@ import { registerMcdocAttributes } from './mcdocAttributes.js' export const initialize = (ctx: core.ProjectInitializerContext) => { registerMcdocAttributes(ctx.meta) + binder.register(ctx.meta) checker.register(ctx.meta) completer.register(ctx.meta) } diff --git a/packages/java-edition/src/json/mcdocAttributes.ts b/packages/java-edition/src/json/mcdocAttributes.ts index 08177b391..7cfb55863 100644 --- a/packages/java-edition/src/json/mcdocAttributes.ts +++ b/packages/java-edition/src/json/mcdocAttributes.ts @@ -2,7 +2,7 @@ import * as core from '@spyglassmc/core' import * as mcdoc from '@spyglassmc/mcdoc' import { dissectUri } from '../binder/index.js' import type { TextureSlotKind, TextureSlotNode } from './node/index.js' -import { textureSlotParser } from './parser/index.js' +import { textureSlotParser, translationValueParser } from './parser/index.js' const validator = mcdoc.runtime.attribute.validator @@ -23,6 +23,13 @@ const textureSlotValidator = validator.alternatives( () => ({ kind: 'value' }), ) +const translationKeyValidator = validator.alternatives( + validator.tree({ + definition: validator.boolean, + }), + () => ({ definition: false }), +) + export function registerMcdocAttributes(meta: core.MetaRegistry) { mcdoc.runtime.registerAttribute(meta, 'criterion', criterionValidator, { stringParser: (config, _, ctx) => { @@ -62,4 +69,21 @@ export function registerMcdocAttributes(meta: core.MetaRegistry) { } satisfies TextureSlotNode }, }) + mcdoc.runtime.registerAttribute(meta, 'translation_key', translationKeyValidator, { + stringParser: (config, _, ctx) => { + return core.symbol({ + category: 'translation_key', + usageType: config.definition ? 'definition' : 'reference', + }) + }, + stringMocker: (config, _, ctx) => { + return core.SymbolNode.mock(ctx.offset, { + category: 'translation_key', + usageType: config.definition ? 'definition' : 'reference', + }) + }, + }) + mcdoc.runtime.registerAttribute(meta, 'translation_value', () => undefined, { + stringParser: () => translationValueParser, + }) } diff --git a/packages/java-edition/src/json/node/index.ts b/packages/java-edition/src/json/node/index.ts index 74c0979e5..8a1d6fb79 100644 --- a/packages/java-edition/src/json/node/index.ts +++ b/packages/java-edition/src/json/node/index.ts @@ -15,3 +15,15 @@ export namespace TextureSlotNode { return (node as TextureSlotNode)?.type === 'java_edition:texture_slot' } } + +export interface TranslationValueNode extends core.AstNode { + type: 'java_edition:translation_value' + children: core.LiteralNode[] + value: string +} + +export namespace TranslationValueNode { + export function is(node: core.AstNode): node is TranslationValueNode { + return (node as TranslationValueNode)?.type === 'java_edition:translation_value' + } +} diff --git a/packages/java-edition/src/json/parser/index.ts b/packages/java-edition/src/json/parser/index.ts index 5c877deab..85fff9ca6 100644 --- a/packages/java-edition/src/json/parser/index.ts +++ b/packages/java-edition/src/json/parser/index.ts @@ -1,6 +1,6 @@ import * as core from '@spyglassmc/core' -import { localize } from '@spyglassmc/locales' -import type { TextureSlotKind, TextureSlotNode } from '../node/index.js' +import { localeQuote, localize } from '@spyglassmc/locales' +import type { TextureSlotKind, TextureSlotNode, TranslationValueNode } from '../node/index.js' export function textureSlotParser(kind: TextureSlotKind): core.InfallibleParser { return (src, ctx) => { @@ -21,7 +21,7 @@ export function textureSlotParser(kind: TextureSlotKind): core.InfallibleParser< ans.children.push(slot) ans.slot = slot } else if (kind === 'reference') { - ctx.err.report(localize('expected', '#'), ans) + ctx.err.report(localize('expected', localeQuote('#')), src) } else { const id = core.resourceLocation({ category: 'texture', usageType: 'reference' })(src, ctx) ans.children.push(id) @@ -31,3 +31,50 @@ export function textureSlotParser(kind: TextureSlotKind): core.InfallibleParser< return ans } } + +export const translationValueParser: core.InfallibleParser = (src, ctx) => { + const start = src.cursor + const ans: TranslationValueNode = { + type: 'java_edition:translation_value', + range: core.Range.create(start), + children: [], + value: '', + } + while (src.canRead()) { + src.skipUntilOrEnd('%') + const argStart = src.cursor + if (src.trySkip('%')) { + if (src.trySkip('%')) { + const token = src.sliceToCursor(argStart) + ans.children.push({ + type: 'literal', + range: core.Range.create(argStart, src), + options: { pool: [token], colorTokenType: 'escape' }, + value: token, + }) + continue + } + let hasInteger = false + while (src.canRead() && core.Source.isDigit(src.peek())) { + src.skip() + hasInteger = true + } + if (hasInteger && !src.trySkip('$')) { + ctx.err.report(localize('expected', localeQuote('$')), src) + } + if (!src.trySkip('s')) { + ctx.err.report(localize('expected', localeQuote('s')), src) + } + const token = src.sliceToCursor(argStart) + ans.children.push({ + type: 'literal', + range: core.Range.create(argStart, src), + options: { pool: [token] }, + value: token, + }) + } + } + ans.value = src.sliceToCursor(start) + ans.range = core.Range.create(start, src) + return ans +} diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index e77057176..647cbf18c 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -277,6 +277,9 @@ "error": [ "invalid.illegal" ], + "escape": [ + "constant.character.escape" + ], "literal": [ "keyword.other" ],