@intlayer/lsp 9.0.0-canary.12 → 9.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/callers/patterns.mjs.map +1 -1
- package/dist/esm/dictionaryUtils.mjs +1 -1
- package/dist/esm/dictionaryUtils.mjs.map +1 -1
- package/dist/esm/findFieldInFile.mjs.map +1 -1
- package/dist/esm/findKeyAtOffset.mjs.map +1 -1
- package/dist/esm/findKeyInContentFile.mjs +3 -3
- package/dist/esm/findKeyInContentFile.mjs.map +1 -1
- package/dist/esm/findTCallAtOffset.mjs.map +1 -1
- package/dist/esm/findUsageFieldAtOffset.mjs.map +1 -1
- package/dist/esm/index.mjs +32 -5
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/oxcUtils.mjs.map +1 -1
- package/dist/esm/usageAnalyzer.mjs +86 -23
- package/dist/esm/usageAnalyzer.mjs.map +1 -1
- package/dist/types/callers/index.d.ts +0 -3
- package/dist/types/callers/patterns.d.ts +20 -20
- package/dist/types/callers/patterns.d.ts.map +1 -1
- package/dist/types/dictionaryUtils.d.ts +0 -1
- package/dist/types/dictionaryUtils.d.ts.map +1 -1
- package/dist/types/findFieldInFile.d.ts +18 -19
- package/dist/types/findFieldInFile.d.ts.map +1 -1
- package/dist/types/findKeyAtOffset.d.ts +15 -15
- package/dist/types/findKeyAtOffset.d.ts.map +1 -1
- package/dist/types/findKeyInContentFile.d.ts +20 -20
- package/dist/types/findKeyInContentFile.d.ts.map +1 -1
- package/dist/types/findTCallAtOffset.d.ts +15 -15
- package/dist/types/findTCallAtOffset.d.ts.map +1 -1
- package/dist/types/findUsageFieldAtOffset.d.ts +14 -14
- package/dist/types/findUsageFieldAtOffset.d.ts.map +1 -1
- package/dist/types/index.d.ts +1 -1
- package/dist/types/oxcUtils.d.ts +22 -22
- package/dist/types/oxcUtils.d.ts.map +1 -1
- package/dist/types/usageAnalyzer.d.ts +95 -83
- package/dist/types/usageAnalyzer.d.ts.map +1 -1
- package/package.json +11 -11
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"patterns.mjs","names":[],"sources":["../../../src/callers/patterns.ts"],"sourcesContent":["import { escapeRegularExpression } from '../findFieldInFile';\nimport { getPositionalKeyCallerNames } from './index';\n\n/**\n * Alternation of every caller name whose dictionary key appears as a\n * positional string argument — for building text-search patterns.\n */\nexport const getCallerNamesAlternation = (): string =>\n getPositionalKeyCallerNames().join('|');\n\n/**\n * Regexes matching call sites that reference the dictionary `key` in project\n * source files. Used by \"find all references\" (content file → call sites).\n *\n * Two shapes are covered:\n * 1. Positional key argument, with optional TypeScript generic:\n * `useIntlayer(\"key\")`, `useTranslations<T>(\"key\")`, `getFixedT(\"en\", \"key\")`\n * 2. Options/descriptor object property:\n * `getTranslations({ namespace: 'key' })`, `useI18n({ namespace: 'key' })`,\n * `formatMessage({ id: 'key.field' })`\n *\n * @param key - The dictionary key to search for.\n */\nexport const buildKeyUsagePatterns = (key: string): RegExp[] => {\n const escapedKey = escapeRegularExpression(key);\n\n return [\n new RegExp(\n `\\\\b(?:${getCallerNamesAlternation()})\\\\b\\\\s*(?:<[^<>()]*>)?\\\\s*\\\\(\\\\s*(['\"\\`])${escapedKey}\\\\1`,\n 'g'\n ),\n new RegExp(\n `\\\\{\\\\s*(?:id|namespace)\\\\s*:\\\\s*(['\"\\`])${escapedKey}(?:\\\\.[^'\"\\`\\\\n]*)?\\\\1`,\n 'g'\n ),\n ];\n};\n\n/**\n * Regex testing whether a file contains a caller invoked with the given\n * dictionary key — a cheap pre-filter before running field-level analysis.\n *\n * @param key - The dictionary key.\n */\nexport const buildCallerWithKeyPattern = (key: string): RegExp =>\n new RegExp(\n `\\\\b(?:${getCallerNamesAlternation()})\\\\b(?:<[^<>()]*>)?\\\\s*\\\\(\\\\s*['\"\\`]${escapeRegularExpression(key)}['\"\\`]`\n );\n"],"mappings":";;;;;;;;AAOA,MAAa,
|
|
1
|
+
{"version":3,"file":"patterns.mjs","names":[],"sources":["../../../src/callers/patterns.ts"],"sourcesContent":["import { escapeRegularExpression } from '../findFieldInFile';\nimport { getPositionalKeyCallerNames } from './index';\n\n/**\n * Alternation of every caller name whose dictionary key appears as a\n * positional string argument — for building text-search patterns.\n */\nexport const getCallerNamesAlternation = (): string =>\n getPositionalKeyCallerNames().join('|');\n\n/**\n * Regexes matching call sites that reference the dictionary `key` in project\n * source files. Used by \"find all references\" (content file → call sites).\n *\n * Two shapes are covered:\n * 1. Positional key argument, with optional TypeScript generic:\n * `useIntlayer(\"key\")`, `useTranslations<T>(\"key\")`, `getFixedT(\"en\", \"key\")`\n * 2. Options/descriptor object property:\n * `getTranslations({ namespace: 'key' })`, `useI18n({ namespace: 'key' })`,\n * `formatMessage({ id: 'key.field' })`\n *\n * @param key - The dictionary key to search for.\n */\nexport const buildKeyUsagePatterns = (key: string): RegExp[] => {\n const escapedKey = escapeRegularExpression(key);\n\n return [\n new RegExp(\n `\\\\b(?:${getCallerNamesAlternation()})\\\\b\\\\s*(?:<[^<>()]*>)?\\\\s*\\\\(\\\\s*(['\"\\`])${escapedKey}\\\\1`,\n 'g'\n ),\n new RegExp(\n `\\\\{\\\\s*(?:id|namespace)\\\\s*:\\\\s*(['\"\\`])${escapedKey}(?:\\\\.[^'\"\\`\\\\n]*)?\\\\1`,\n 'g'\n ),\n ];\n};\n\n/**\n * Regex testing whether a file contains a caller invoked with the given\n * dictionary key — a cheap pre-filter before running field-level analysis.\n *\n * @param key - The dictionary key.\n */\nexport const buildCallerWithKeyPattern = (key: string): RegExp =>\n new RegExp(\n `\\\\b(?:${getCallerNamesAlternation()})\\\\b(?:<[^<>()]*>)?\\\\s*\\\\(\\\\s*['\"\\`]${escapeRegularExpression(key)}['\"\\`]`\n );\n"],"mappings":";;;;;;;;AAOA,MAAa,mFACiB,CAAC,CAAC,KAAK,GAAG;;;;;;;;;;;;;;AAexC,MAAa,yBAAyB,QAA0B;CAC9D,MAAM,aAAa,wBAAwB,GAAG;CAE9C,OAAO,CACL,IAAI,OACF,SAAS,0BAA0B,EAAE,4CAA4C,WAAW,MAC5F,GACF,GACA,IAAI,OACF,2CAA2C,WAAW,yBACtD,GACF,CACF;AACF;;;;;;;AAQA,MAAa,6BAA6B,QACxC,IAAI,OACF,SAAS,0BAA0B,EAAE,sCAAsC,wBAAwB,GAAG,EAAE,OAC1G"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dictionaryUtils.mjs","names":[],"sources":["../../src/dictionaryUtils.ts"],"sourcesContent":["import type { Dictionary } from '@intlayer/types/dictionary';\n\ntype ContentObj = Record<string, unknown>;\n\nconst LOCALE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;\nconst META_KEYS: ReadonlySet<string> = new Set([\n 'nodeType',\n 'translation',\n 'enumeration',\n 'condition',\n 'plural',\n 'insertion',\n 'nested',\n 'markdown',\n 'html',\n 'file',\n 'composite',\n 'dictionaryKey',\n]);\n\nconst isTypedNode = (val: unknown): val is ContentObj =>\n typeof val === 'object' &&\n val !== null &&\n typeof (val as ContentObj)['nodeType'] === 'string';\n\nconst getTypedNodeSummary = (node: ContentObj): string => {\n const nodeType = node['nodeType'] as string;\n const inner = node[nodeType];\n\n if (inner && typeof inner === 'object') {\n const map = inner as Record<string, unknown>;\n const localeEntries = Object.entries(map).filter(([key]) =>\n LOCALE_PATTERN.test(key)\n );\n\n const entries =\n localeEntries.length > 0 ? localeEntries : Object.entries(map);\n\n if (entries.length > 0) {\n return entries\n .slice(0, 5)\n .map(\n ([k, v]) =>\n `**${k}**: ${typeof v === 'string' ? v : JSON.stringify(v)}`\n )\n .join('\\n\\n');\n }\n }\n\n return `*(${nodeType})*`;\n};\n\nexport const formatFieldValue = (value: unknown): string => {\n if (value === null || value === undefined) return `\\`${String(value)}\\``;\n\n if (typeof value === 'string') return `\"${value}\"`;\n\n if (typeof value === 'number' || typeof value === 'boolean')\n return String(value);\n\n if (typeof value === 'object') {\n const obj = value as ContentObj;\n\n if (isTypedNode(obj)) return getTypedNodeSummary(obj);\n const fields = Object.keys(obj).filter(\n (key) => !META_KEYS.has(key) && !LOCALE_PATTERN.test(key)\n );\n return fields.length > 0 ? `{ ${fields.slice(0, 8).join(', ')} }` : '{}';\n }\n return JSON.stringify(value);\n};\n\nexport const getFieldByPath = (content: unknown, path: string[]): unknown => {\n let current = content;\n for (const key of path) {\n if (current === null || typeof current !== 'object') return undefined;\n current = (current as ContentObj)[key];\n }\n return current;\n};\n\nconst isNavigableField = (key: string): boolean =>\n !META_KEYS.has(key) && !LOCALE_PATTERN.test(key);\n\nexport const getTopLevelFields = (content: unknown): string[] => {\n if (!content || typeof content !== 'object') return [];\n return Object.keys(content as ContentObj).filter(isNavigableField);\n};\n\nexport const getFieldsAtPath = (content: unknown, path: string[]): string[] => {\n const value = path.length === 0 ? content : getFieldByPath(content, path);\n\n if (!value || typeof value !== 'object') return [];\n\n if (isTypedNode(value as ContentObj)) return [];\n return Object.keys(value as ContentObj).filter(isNavigableField);\n};\n\nexport const formatDictionaryHover = (\n dictionaries: Dictionary[],\n key: string\n): string => {\n const primary = dictionaries[0]!;\n const lines: string[] = [`**Intlayer dictionary** \\`${key}\\``];\n\n if (primary.title) lines.push(`*${primary.title}*`);\n\n if (primary.description) lines.push(primary.description);\n\n const fields = getTopLevelFields(primary.content);\n\n if (fields.length > 0) {\n lines.push('');\n lines.push(\n `**Fields:** ${fields.map((field) => `\\`${field}\\``).join(', ')}`\n );\n }\n\n if (dictionaries.length > 1) {\n lines.push(`*(${dictionaries.length} source files)*`);\n }\n\n return lines.join('\\n\\n');\n};\n\nexport const formatFieldHover = (\n dictionaries: Dictionary[],\n dictionaryKey: string,\n fieldPath: string[]\n): string | null => {\n for (const dict of dictionaries) {\n const value = getFieldByPath(dict.content, fieldPath);\n\n if (value === undefined) continue;\n return [\n `**\\`${dictionaryKey}.${fieldPath.join('.')}\\`**`,\n formatFieldValue(value),\n ].join('\\n\\n');\n }\n return null;\n};\n"],"mappings":";AAIA,MAAM,iBAAiB;AACvB,MAAM,
|
|
1
|
+
{"version":3,"file":"dictionaryUtils.mjs","names":[],"sources":["../../src/dictionaryUtils.ts"],"sourcesContent":["import type { Dictionary } from '@intlayer/types/dictionary';\n\ntype ContentObj = Record<string, unknown>;\n\nconst LOCALE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;\nconst META_KEYS: ReadonlySet<string> = new Set([\n 'nodeType',\n 'translation',\n 'enumeration',\n 'condition',\n 'plural',\n 'insertion',\n 'nested',\n 'markdown',\n 'html',\n 'file',\n 'composite',\n 'dictionaryKey',\n]);\n\nconst isTypedNode = (val: unknown): val is ContentObj =>\n typeof val === 'object' &&\n val !== null &&\n typeof (val as ContentObj)['nodeType'] === 'string';\n\nconst getTypedNodeSummary = (node: ContentObj): string => {\n const nodeType = node['nodeType'] as string;\n const inner = node[nodeType];\n\n if (inner && typeof inner === 'object') {\n const map = inner as Record<string, unknown>;\n const localeEntries = Object.entries(map).filter(([key]) =>\n LOCALE_PATTERN.test(key)\n );\n\n const entries =\n localeEntries.length > 0 ? localeEntries : Object.entries(map);\n\n if (entries.length > 0) {\n return entries\n .slice(0, 5)\n .map(\n ([k, v]) =>\n `**${k}**: ${typeof v === 'string' ? v : JSON.stringify(v)}`\n )\n .join('\\n\\n');\n }\n }\n\n return `*(${nodeType})*`;\n};\n\nexport const formatFieldValue = (value: unknown): string => {\n if (value === null || value === undefined) return `\\`${String(value)}\\``;\n\n if (typeof value === 'string') return `\"${value}\"`;\n\n if (typeof value === 'number' || typeof value === 'boolean')\n return String(value);\n\n if (typeof value === 'object') {\n const obj = value as ContentObj;\n\n if (isTypedNode(obj)) return getTypedNodeSummary(obj);\n const fields = Object.keys(obj).filter(\n (key) => !META_KEYS.has(key) && !LOCALE_PATTERN.test(key)\n );\n return fields.length > 0 ? `{ ${fields.slice(0, 8).join(', ')} }` : '{}';\n }\n return JSON.stringify(value);\n};\n\nexport const getFieldByPath = (content: unknown, path: string[]): unknown => {\n let current = content;\n for (const key of path) {\n if (current === null || typeof current !== 'object') return undefined;\n current = (current as ContentObj)[key];\n }\n return current;\n};\n\nconst isNavigableField = (key: string): boolean =>\n !META_KEYS.has(key) && !LOCALE_PATTERN.test(key);\n\nexport const getTopLevelFields = (content: unknown): string[] => {\n if (!content || typeof content !== 'object') return [];\n return Object.keys(content as ContentObj).filter(isNavigableField);\n};\n\nexport const getFieldsAtPath = (content: unknown, path: string[]): string[] => {\n const value = path.length === 0 ? content : getFieldByPath(content, path);\n\n if (!value || typeof value !== 'object') return [];\n\n if (isTypedNode(value as ContentObj)) return [];\n return Object.keys(value as ContentObj).filter(isNavigableField);\n};\n\nexport const formatDictionaryHover = (\n dictionaries: Dictionary[],\n key: string\n): string => {\n const primary = dictionaries[0]!;\n const lines: string[] = [`**Intlayer dictionary** \\`${key}\\``];\n\n if (primary.title) lines.push(`*${primary.title}*`);\n\n if (primary.description) lines.push(primary.description);\n\n const fields = getTopLevelFields(primary.content);\n\n if (fields.length > 0) {\n lines.push('');\n lines.push(\n `**Fields:** ${fields.map((field) => `\\`${field}\\``).join(', ')}`\n );\n }\n\n if (dictionaries.length > 1) {\n lines.push(`*(${dictionaries.length} source files)*`);\n }\n\n return lines.join('\\n\\n');\n};\n\nexport const formatFieldHover = (\n dictionaries: Dictionary[],\n dictionaryKey: string,\n fieldPath: string[]\n): string | null => {\n for (const dict of dictionaries) {\n const value = getFieldByPath(dict.content, fieldPath);\n\n if (value === undefined) continue;\n return [\n `**\\`${dictionaryKey}.${fieldPath.join('.')}\\`**`,\n formatFieldValue(value),\n ].join('\\n\\n');\n }\n return null;\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findFieldInFile.mjs","names":[],"sources":["../../src/findFieldInFile.ts"],"sourcesContent":["import { Position, Range } from 'vscode-languageserver/node';\nimport { collectMessageUsages } from './usageAnalyzer';\n\nexport const escapeRegularExpression = (content: string): string =>\n content.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nexport const offsetToRange = (\n text: string,\n start: number,\n end: number\n): Range => {\n const beforeStart = text.slice(0, start);\n const startLine = (beforeStart.match(/\\n/g) ?? []).length;\n const startCharacter = start - (beforeStart.lastIndexOf('\\n') + 1);\n const beforeEnd = text.slice(0, end);\n const endLine = (beforeEnd.match(/\\n/g) ?? []).length;\n const endCharacter = end - (beforeEnd.lastIndexOf('\\n') + 1);\n\n return Range.create(\n Position.create(startLine, startCharacter),\n Position.create(endLine, endCharacter)\n );\n};\n\n/**\n * Within a single file's text, find all positions where `fieldPath` (or legacy\n * `fieldName`) is consumed from the dictionary `escapedKey` — through any\n * registered caller.\n *\n * Covers base-intlayer patterns:\n * 1. Direct destructure: const { field } = useIntlayer(\"key\")\n * 2. Nested destructure: const { parent: { child } } = useIntlayer(\"key\")\n * 3. Indirect member access: const { parent } = useIntlayer(\"key\"); parent.child\n * 4. Full chain access: const content = useIntlayer(\"key\"); content.parent.child\n *\n * …and every compat form resolved by `collectMessageUsages`: translation\n * calls (`t('parent.child')`), self calls (`formatMessage({ id })`), JSX\n * message components and lingui tagged templates.\n *\n * `fieldPath` (optional 5th parameter) is the preferred input; when omitted\n * the function derives the single-element path from `escapedField`, preserving\n * backward compatibility.\n */\nexport const findFieldRangesInFile = (\n text: string,\n escapedKey: string,\n _fieldName: string,\n escapedField: string,\n fieldPath?: string[]\n): Range[] => {\n const dictionaryKey = escapedKey.replace(/\\\\([.*+?^${}()|[\\]\\\\])/g, '$1');\n const unescapedLeaf = escapedField.replace(/\\\\([.*+?^${}()|[\\]\\\\])/g, '$1');\n\n // Effective path: use the provided fieldPath when available, else single-element\n const path = fieldPath && fieldPath.length > 0 ? fieldPath : [unescapedLeaf];\n\n const ranges: Range[] = [];\n\n for (const usage of collectMessageUsages(text)) {\n if (usage.dictionaryKey !== dictionaryKey) continue;\n\n // Bare variable references don't spell the field name — one hit per\n // declaration site, not per flow of the variable.\n if (usage.isBareReference) continue;\n\n if (usage.fieldPath.length !== path.length) continue;\n\n if (!usage.fieldPath.every((segment, index) => segment === path[index])) {\n continue;\n }\n\n // Member chains highlight the leaf property; other usages highlight the\n // whole expression (call, JSX element, tagged template, destructure key).\n const lastFieldSpan = usage.fieldSpans?.[usage.fieldSpans.length - 1];\n\n if (usage.kind === 'member' && lastFieldSpan) {\n ranges.push(offsetToRange(text, lastFieldSpan.start, lastFieldSpan.end));\n } else {\n ranges.push(offsetToRange(text, usage.start, usage.end));\n }\n }\n\n return ranges;\n};\n"],"mappings":";;;;AAGA,MAAa,2BAA2B,YACtC,QAAQ,QAAQ,uBAAuB,
|
|
1
|
+
{"version":3,"file":"findFieldInFile.mjs","names":[],"sources":["../../src/findFieldInFile.ts"],"sourcesContent":["import { Position, Range } from 'vscode-languageserver/node';\nimport { collectMessageUsages } from './usageAnalyzer';\n\nexport const escapeRegularExpression = (content: string): string =>\n content.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&');\n\nexport const offsetToRange = (\n text: string,\n start: number,\n end: number\n): Range => {\n const beforeStart = text.slice(0, start);\n const startLine = (beforeStart.match(/\\n/g) ?? []).length;\n const startCharacter = start - (beforeStart.lastIndexOf('\\n') + 1);\n const beforeEnd = text.slice(0, end);\n const endLine = (beforeEnd.match(/\\n/g) ?? []).length;\n const endCharacter = end - (beforeEnd.lastIndexOf('\\n') + 1);\n\n return Range.create(\n Position.create(startLine, startCharacter),\n Position.create(endLine, endCharacter)\n );\n};\n\n/**\n * Within a single file's text, find all positions where `fieldPath` (or legacy\n * `fieldName`) is consumed from the dictionary `escapedKey` — through any\n * registered caller.\n *\n * Covers base-intlayer patterns:\n * 1. Direct destructure: const { field } = useIntlayer(\"key\")\n * 2. Nested destructure: const { parent: { child } } = useIntlayer(\"key\")\n * 3. Indirect member access: const { parent } = useIntlayer(\"key\"); parent.child\n * 4. Full chain access: const content = useIntlayer(\"key\"); content.parent.child\n *\n * …and every compat form resolved by `collectMessageUsages`: translation\n * calls (`t('parent.child')`), self calls (`formatMessage({ id })`), JSX\n * message components and lingui tagged templates.\n *\n * `fieldPath` (optional 5th parameter) is the preferred input; when omitted\n * the function derives the single-element path from `escapedField`, preserving\n * backward compatibility.\n */\nexport const findFieldRangesInFile = (\n text: string,\n escapedKey: string,\n _fieldName: string,\n escapedField: string,\n fieldPath?: string[]\n): Range[] => {\n const dictionaryKey = escapedKey.replace(/\\\\([.*+?^${}()|[\\]\\\\])/g, '$1');\n const unescapedLeaf = escapedField.replace(/\\\\([.*+?^${}()|[\\]\\\\])/g, '$1');\n\n // Effective path: use the provided fieldPath when available, else single-element\n const path = fieldPath && fieldPath.length > 0 ? fieldPath : [unescapedLeaf];\n\n const ranges: Range[] = [];\n\n for (const usage of collectMessageUsages(text)) {\n if (usage.dictionaryKey !== dictionaryKey) continue;\n\n // Bare variable references don't spell the field name — one hit per\n // declaration site, not per flow of the variable.\n if (usage.isBareReference) continue;\n\n if (usage.fieldPath.length !== path.length) continue;\n\n if (!usage.fieldPath.every((segment, index) => segment === path[index])) {\n continue;\n }\n\n // Member chains highlight the leaf property; other usages highlight the\n // whole expression (call, JSX element, tagged template, destructure key).\n const lastFieldSpan = usage.fieldSpans?.[usage.fieldSpans.length - 1];\n\n if (usage.kind === 'member' && lastFieldSpan) {\n ranges.push(offsetToRange(text, lastFieldSpan.start, lastFieldSpan.end));\n } else {\n ranges.push(offsetToRange(text, usage.start, usage.end));\n }\n }\n\n return ranges;\n};\n"],"mappings":";;;;AAGA,MAAa,2BAA2B,YACtC,QAAQ,QAAQ,uBAAuB,MAAM;AAE/C,MAAa,iBACX,MACA,OACA,QACU;CACV,MAAM,cAAc,KAAK,MAAM,GAAG,KAAK;CACvC,MAAM,aAAa,YAAY,MAAM,KAAK,KAAK,CAAC,EAAC,CAAE;CACnD,MAAM,iBAAiB,SAAS,YAAY,YAAY,IAAI,IAAI;CAChE,MAAM,YAAY,KAAK,MAAM,GAAG,GAAG;CACnC,MAAM,WAAW,UAAU,MAAM,KAAK,KAAK,CAAC,EAAC,CAAE;CAC/C,MAAM,eAAe,OAAO,UAAU,YAAY,IAAI,IAAI;CAE1D,OAAO,MAAM,OACX,SAAS,OAAO,WAAW,cAAc,GACzC,SAAS,OAAO,SAAS,YAAY,CACvC;AACF;;;;;;;;;;;;;;;;;;;;AAqBA,MAAa,yBACX,MACA,YACA,YACA,cACA,cACY;CACZ,MAAM,gBAAgB,WAAW,QAAQ,2BAA2B,IAAI;CACxE,MAAM,gBAAgB,aAAa,QAAQ,2BAA2B,IAAI;CAG1E,MAAM,OAAO,aAAa,UAAU,SAAS,IAAI,YAAY,CAAC,aAAa;CAE3E,MAAM,SAAkB,CAAC;CAEzB,KAAK,MAAM,SAAS,qBAAqB,IAAI,GAAG;EAC9C,IAAI,MAAM,kBAAkB,eAAe;EAI3C,IAAI,MAAM,iBAAiB;EAE3B,IAAI,MAAM,UAAU,WAAW,KAAK,QAAQ;EAE5C,IAAI,CAAC,MAAM,UAAU,OAAO,SAAS,UAAU,YAAY,KAAK,MAAM,GACpE;EAKF,MAAM,gBAAgB,MAAM,aAAa,MAAM,WAAW,SAAS;EAEnE,IAAI,MAAM,SAAS,YAAY,eAC7B,OAAO,KAAK,cAAc,MAAM,cAAc,OAAO,cAAc,GAAG,CAAC;OAEvE,OAAO,KAAK,cAAc,MAAM,MAAM,OAAO,MAAM,GAAG,CAAC;CAE3D;CAEA,OAAO;AACT"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findKeyAtOffset.mjs","names":[],"sources":["../../src/findKeyAtOffset.ts"],"sourcesContent":["import { collectNamespaceReferences } from './usageAnalyzer';\n\n/**\n * Scan `text` for a caller whose namespace (dictionary key) is addressed at\n * `offset`. Returns the namespace string when the cursor is on the caller\n * name or on the namespace argument itself, `null` otherwise.\n *\n * Handles every registered caller with a positional or options-object\n * namespace:\n * useIntlayer(\"my-key\") ← cursor on \"my-key\" or the callee\n * useTranslations(\"namespace\") ← idem\n * getFixedT(\"en\", \"namespace\") ← cursor on \"namespace\", not \"en\"\n * useI18n({ namespace: \"ns\" }) ← cursor on \"ns\" or the callee\n * getTranslations({ namespace: \"ns\" }) ← idem\n *\n * Self callers (`formatMessage`, lingui `t`/`_`) are message *usages*, not\n * namespace declarations — they are resolved by `findMessageUsageAtOffset`.\n */\nexport const findKeyAtOffset = (\n text: string,\n offset: number\n): string | null => {\n for (const reference of collectNamespaceReferences(text)) {\n const isOnCalleeName =\n offset >= reference.calleeStart && offset <= reference.calleeEnd;\n const isOnNamespaceArgument =\n offset >= reference.namespaceStart && offset <= reference.namespaceEnd;\n\n if (isOnCalleeName || isOnNamespaceArgument) {\n return reference.dictionaryKey;\n }\n }\n\n return null;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,MAAa,mBACX,MACA,WACkB;
|
|
1
|
+
{"version":3,"file":"findKeyAtOffset.mjs","names":[],"sources":["../../src/findKeyAtOffset.ts"],"sourcesContent":["import { collectNamespaceReferences } from './usageAnalyzer';\n\n/**\n * Scan `text` for a caller whose namespace (dictionary key) is addressed at\n * `offset`. Returns the namespace string when the cursor is on the caller\n * name or on the namespace argument itself, `null` otherwise.\n *\n * Handles every registered caller with a positional or options-object\n * namespace:\n * useIntlayer(\"my-key\") ← cursor on \"my-key\" or the callee\n * useTranslations(\"namespace\") ← idem\n * getFixedT(\"en\", \"namespace\") ← cursor on \"namespace\", not \"en\"\n * useI18n({ namespace: \"ns\" }) ← cursor on \"ns\" or the callee\n * getTranslations({ namespace: \"ns\" }) ← idem\n *\n * Self callers (`formatMessage`, lingui `t`/`_`) are message *usages*, not\n * namespace declarations — they are resolved by `findMessageUsageAtOffset`.\n */\nexport const findKeyAtOffset = (\n text: string,\n offset: number\n): string | null => {\n for (const reference of collectNamespaceReferences(text)) {\n const isOnCalleeName =\n offset >= reference.calleeStart && offset <= reference.calleeEnd;\n const isOnNamespaceArgument =\n offset >= reference.namespaceStart && offset <= reference.namespaceEnd;\n\n if (isOnCalleeName || isOnNamespaceArgument) {\n return reference.dictionaryKey;\n }\n }\n\n return null;\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,MAAa,mBACX,MACA,WACkB;CAClB,KAAK,MAAM,aAAa,2BAA2B,IAAI,GAAG;EACxD,MAAM,iBACJ,UAAU,UAAU,eAAe,UAAU,UAAU;EACzD,MAAM,wBACJ,UAAU,UAAU,kBAAkB,UAAU,UAAU;EAE5D,IAAI,kBAAkB,uBACpB,OAAO,UAAU;CAErB;CAEA,OAAO;AACT"}
|
|
@@ -61,7 +61,7 @@ const findContentFieldAtOffset = (text, offset, fileExtension) => {
|
|
|
61
61
|
* `file` – file reference ("./path/to/file.txt")
|
|
62
62
|
* `html` – html content ("<p>…</p>")
|
|
63
63
|
*/
|
|
64
|
-
const INTLAYER_LEAF_CALL_NAMES = new Set([
|
|
64
|
+
const INTLAYER_LEAF_CALL_NAMES = /* @__PURE__ */ new Set([
|
|
65
65
|
"t",
|
|
66
66
|
"enu",
|
|
67
67
|
"cond",
|
|
@@ -93,7 +93,7 @@ const getStringValue = (node) => {
|
|
|
93
93
|
* e.g. { "nodeType": "translation", "translation": { "en": "…" } }
|
|
94
94
|
* None of these are navigable content fields.
|
|
95
95
|
*/
|
|
96
|
-
const JSON_INTLAYER_NODE_KEYS = new Set([
|
|
96
|
+
const JSON_INTLAYER_NODE_KEYS = /* @__PURE__ */ new Set([
|
|
97
97
|
"nodeType",
|
|
98
98
|
"translation",
|
|
99
99
|
"enumeration",
|
|
@@ -213,7 +213,7 @@ const LOCALE_CODE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;
|
|
|
213
213
|
* YAML / Markdown meta-keys that appear at the top level or inside typed nodes.
|
|
214
214
|
* These should not be treated as navigable content fields.
|
|
215
215
|
*/
|
|
216
|
-
const YAML_META_KEYS = new Set([
|
|
216
|
+
const YAML_META_KEYS = /* @__PURE__ */ new Set([
|
|
217
217
|
"nodeType",
|
|
218
218
|
"translation",
|
|
219
219
|
"enumeration",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findKeyInContentFile.mjs","names":[],"sources":["../../src/findKeyInContentFile.ts"],"sourcesContent":["import { type OxcNode, parseText, walkAst } from './oxcUtils';\n\n// Group 2: quoted value — key: \"foo\" | key: 'foo' | key: `foo` (TS/JS/JSON)\n// Group 3: unquoted value — key: foo-bar (YAML / Markdown frontmatter)\nconst KEY_DECLARATION_REGULAR_EXPRESSION =\n /(?:^|[{,\\s])\"?key\"?\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^ \\t'\"`{},\\r\\n]+))/g;\n\n/**\n * If `offset` falls within a `key:` declaration in a content file,\n * return the declared key string; otherwise return null.\n * Handles:\n * - Quoted values: key: \"foo\" | key: 'foo' | key: `foo` (TS/JS)\n * - JSON values: \"key\": \"foo\" (JSON/JSON5/JSONC)\n * - Bare values: key: foo-bar (YAML / Markdown frontmatter)\n */\nexport const findKeyInContentFile = (\n text: string,\n offset: number\n): string | null => {\n for (const match of text.matchAll(KEY_DECLARATION_REGULAR_EXPRESSION)) {\n const start = match.index!;\n const end = start + match[0].length;\n\n if (offset >= start && offset <= end) {\n return match[2] ?? match[3] ?? null;\n }\n }\n\n return null;\n};\n\n/**\n * If `offset` falls on a content field property key inside an intlayer content\n * file, return the dictionary key, the leaf field name, and the full field path.\n *\n * The `fileExtension` parameter controls which parser is used:\n *\n * - `.json` / `.json5` / `.jsonc` → dedicated JSON parser\n * - `.yaml` / `.yml` → YAML regex heuristic\n * - `.md` / `.mdx` → always null (body is the content value)\n * - everything else → Oxc AST (TS/JS/TSX/JSX)\n *\n * When `fileExtension` is omitted the function falls back to Oxc, then to\n * JSON/YAML heuristics based on text shape.\n */\nexport const findContentFieldAtOffset = (\n text: string,\n offset: number,\n fileExtension?: string\n): FieldResult | null => {\n const ext = fileExtension?.toLowerCase() ?? '';\n\n // Markdown has no individual navigable content fields\n if (ext === '.md' || ext === '.mdx') return null;\n\n // JSON / JSON5 / JSONC\n if (ext === '.json' || ext === '.json5' || ext === '.jsonc') {\n return findContentFieldAtOffsetJson(text, offset);\n }\n\n // YAML / YML\n if (ext === '.yaml' || ext === '.yml') {\n return findContentFieldAtOffsetYaml(text, offset);\n }\n\n // TS / JS / TSX / JSX — use Oxc AST\n const program = parseText(text);\n if (program) {\n return findContentFieldAtOffsetOxc(text, offset, program);\n }\n\n // Oxc failed to parse — fall back on content-shape heuristics\n // JSON: starts with optional whitespace then `{`\n if (/^\\s*\\{/.test(text)) {\n return findContentFieldAtOffsetJson(text, offset);\n }\n\n // Markdown: YAML frontmatter fence with no `content:` block\n if (/^---\\s*\\r?\\n/.test(text)) {\n if (!/^content\\s*:/m.test(text)) return null;\n }\n\n // YAML (or anything else with a content: block)\n return findContentFieldAtOffsetYaml(text, offset);\n};\n\n// ---------------------------------------------------------------------------\n// Helpers shared by the Oxc and YAML/JSON fallback paths\n// ---------------------------------------------------------------------------\n\n/**\n * All Intlayer helper function names whose arguments are locale-map or\n * control-flow data — NOT navigable content field names. The walker must\n * treat these calls as opaque leaf values and never descend into them.\n *\n * `t` – translation ({ en: \"…\", fr: \"…\" })\n * `enu` – enumeration ({ \"0\": \"…\", \">5\": \"…\" })\n * `cond` – condition ({ true: \"…\", false: \"…\" })\n * `plural` – plural rules ({ one: \"…\", other: \"…\" })\n * `gender` – gender rules ({ male: \"…\", female: \"…\", other: \"…\" })\n * `insert` – insertion (\"Hello {{name}}!\")\n * `nest` – nested dict (\"navbar\")\n * `md` – markdown (\"# …\")\n * `file` – file reference (\"./path/to/file.txt\")\n * `html` – html content (\"<p>…</p>\")\n */\nconst INTLAYER_LEAF_CALL_NAMES: ReadonlySet<string> = new Set([\n 't',\n 'enu',\n 'cond',\n 'plural',\n 'gender',\n 'insert',\n 'nest',\n 'md',\n 'file',\n 'html',\n]);\n\n/** Returns true when `node` is a call to any intlayer leaf helper function. */\nconst isIntlayerLeafCall = (node: OxcNode): boolean => {\n if (node['type'] !== 'CallExpression') return false;\n const callee = node['callee'] as OxcNode | undefined;\n return (\n callee?.['type'] === 'Identifier' &&\n INTLAYER_LEAF_CALL_NAMES.has(callee['name'] as string)\n );\n};\n\n/** Extract a string value from a Literal / StringLiteral / TemplateLiteral node. */\nconst getStringValue = (node: OxcNode): string | null => {\n if (\n (node['type'] === 'Literal' || node['type'] === 'StringLiteral') &&\n typeof node['value'] === 'string'\n ) {\n return node['value'] as string;\n }\n if (node['type'] === 'TemplateLiteral') {\n const quasis = node['quasis'] as OxcNode[] | undefined;\n const valueNode = quasis?.[0]?.['value'] as OxcNode | undefined;\n return (\n (valueNode?.['cooked'] as string | undefined) ??\n (valueNode?.['raw'] as string | undefined) ??\n null\n );\n }\n return null;\n};\n\n// ---------------------------------------------------------------------------\n// JSON content-file support\n// ---------------------------------------------------------------------------\n\n/**\n * Well-known meta-keys that appear inside Intlayer typed nodes in JSON files.\n * e.g. { \"nodeType\": \"translation\", \"translation\": { \"en\": \"…\" } }\n * None of these are navigable content fields.\n */\nconst JSON_INTLAYER_NODE_KEYS: ReadonlySet<string> = new Set([\n 'nodeType',\n 'translation',\n 'enumeration',\n 'condition',\n 'plural',\n 'insertion',\n 'nested',\n 'markdown',\n 'html',\n 'file',\n 'composite',\n 'dictionaryKey',\n // JSON schema / system properties\n '$schema',\n 'id',\n 'projectIds',\n 'localId',\n 'localIds',\n 'filePath',\n 'versions',\n 'filled',\n // Top-level dictionary meta-properties (not inside content)\n 'key',\n 'title',\n 'description',\n 'tags',\n 'format',\n 'locale',\n 'schema',\n 'location',\n 'fill',\n 'priority',\n 'version',\n 'importMode',\n]);\n\n/**\n * Find the byte offset of `fieldName` (as a JSON property key) inside `text`,\n * searching within the range [searchFrom, searchTo).\n *\n * Matches `\"fieldName\"` surrounded by typical JSON delimiters.\n */\nconst findJsonKeyOffset = (\n text: string,\n fieldName: string,\n searchFrom: number,\n searchTo: number\n): number => {\n const pattern = new RegExp(\n `\"${fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\"`,\n 'g'\n );\n pattern.lastIndex = searchFrom;\n let match: RegExpExecArray | null;\n while (true) {\n match = pattern.exec(text);\n if (match === null) break;\n if (match.index >= searchTo) break;\n return match.index;\n }\n return -1;\n};\n\n/**\n * Strip JSON5/JSONC single-line (//) and multi-line (/* *\\/\\/) comments,\n * and trailing commas, producing valid JSON that JSON.parse can handle.\n */\nconst stripJsonComments = (text: string): string =>\n text\n // Remove single-line comments (// …) EXCEPT in URLs (e.g., https://)\n .replace(/(?<!:)\\/\\/[^\\r\\n]*/g, '')\n // Remove block comments (/* … */)\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n // Remove trailing commas before } or ]\n .replace(/,(\\s*[}\\]])/g, '$1');\n\ntype FieldResult = {\n dictionaryKey: string;\n fieldName: string;\n fieldPath: string[];\n};\n\n/**\n * Parse a JSON/JSON5/JSONC content file and, if `offset` falls on a content\n * field key, return the dictionary key, leaf field name, and full field path.\n *\n * Returns null when:\n * - The text is not valid JSON (even after comment stripping)\n * - The cursor is not on a navigable content field key\n * - The field is a meta/system key (nodeType, translation, locale codes, …)\n */\nconst findContentFieldAtOffsetJson = (\n text: string,\n offset: number\n): FieldResult | null => {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(stripJsonComments(text)) as Record<string, unknown>;\n } catch {\n return null;\n }\n\n const dictionaryKey =\n typeof parsed['key'] === 'string' ? parsed['key'] : null;\n if (!dictionaryKey) return null;\n\n const contentObj = parsed['content'];\n if (\n !contentObj ||\n typeof contentObj !== 'object' ||\n Array.isArray(contentObj)\n )\n return null;\n\n // Find the byte range of the \"content\" block in raw text so we can scope\n // offset comparisons to content fields only.\n const contentKeyIndex = text.indexOf('\"content\"');\n if (contentKeyIndex === -1) return null;\n\n /**\n * Recursively walk the parsed content object.\n * For each property key, check whether the raw text position of that key\n * (within the remaining text slice) overlaps with `offset`.\n *\n * `textFrom` is where we start searching for each key in the raw text —\n * we advance it as we consume properties to avoid false matches on\n * identically-named sibling keys.\n */\n const walk = (\n obj: Record<string, unknown>,\n path: string[],\n searchFrom: number,\n searchTo: number\n ): FieldResult | null => {\n for (const [fieldName, fieldValue] of Object.entries(obj)) {\n // Skip Intlayer-internal node keys — they are not navigable content fields\n if (JSON_INTLAYER_NODE_KEYS.has(fieldName)) continue;\n\n // Skip keys that look like locale codes (e.g. \"en\", \"fr\", \"en-GB\")\n if (/^[a-z]{2}(-[A-Z]{2})?$/.test(fieldName)) continue;\n\n const keyStart = findJsonKeyOffset(text, fieldName, searchFrom, searchTo);\n if (keyStart === -1) continue;\n const keyEnd = keyStart + fieldName.length + 2; // +2 for surrounding quotes\n\n if (offset >= keyStart && offset <= keyEnd) {\n return {\n dictionaryKey,\n fieldName,\n fieldPath: [...path, fieldName],\n };\n }\n\n // Recurse into plain nested objects (skip typed nodes — they have nodeType)\n if (\n fieldValue !== null &&\n typeof fieldValue === 'object' &&\n !Array.isArray(fieldValue)\n ) {\n const nested = fieldValue as Record<string, unknown>;\n // Skip Intlayer typed nodes — their children are not content fields\n if ('nodeType' in nested) continue;\n\n const result = walk(nested, [...path, fieldName], keyEnd, searchTo);\n if (result) return result;\n }\n }\n return null;\n };\n\n return walk(\n contentObj as Record<string, unknown>,\n [],\n contentKeyIndex,\n text.length\n );\n};\n\n// ---------------------------------------------------------------------------\n// YAML / Markdown frontmatter fallback\n// ---------------------------------------------------------------------------\n\n/**\n * Locale-code pattern — keys matching this inside YAML content blocks are\n * translation locale identifiers, not navigable content fields.\n */\nconst LOCALE_CODE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;\n\n/**\n * YAML / Markdown meta-keys that appear at the top level or inside typed nodes.\n * These should not be treated as navigable content fields.\n */\nconst YAML_META_KEYS: ReadonlySet<string> = new Set([\n 'nodeType',\n 'translation',\n 'enumeration',\n 'condition',\n 'plural',\n 'insertion',\n 'nested',\n 'markdown',\n 'html',\n 'file',\n 'composite',\n 'dictionaryKey',\n // Frontmatter / top-level keys\n 'key',\n 'title',\n 'description',\n 'tags',\n 'format',\n 'locale',\n 'schema',\n 'location',\n 'fill',\n 'priority',\n 'version',\n 'importMode',\n '$schema',\n]);\n\n/**\n * Improved regex fallback for YAML and Markdown content files.\n *\n * Strategy:\n * 1. Extract the dictionary key from the frontmatter / top-level `key:` line.\n * 2. Locate the `content:` section (YAML files only; Markdown has no `content:` block).\n * 3. Within the content section, find all `fieldName:` patterns at consistent\n * indentation levels, skipping known meta-keys and locale codes.\n * 4. Check whether `offset` falls within one of those key spans.\n * 5. Return a single-element `fieldPath` (deep path tracking is impractical\n * without a full YAML parser, but single-level is enough for most use cases).\n *\n * For Markdown files, `findContentFieldAtOffset` always returns null because\n * the body text is the content value — individual content \"fields\" are not\n * declared as separate properties.\n */\nconst findContentFieldAtOffsetYaml = (\n text: string,\n offset: number\n): FieldResult | null => {\n // Extract dictionary key\n const keyMatch =\n /^key\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^\\s'\"`{},\\r\\n]+))/m.exec(text);\n if (!keyMatch) return null;\n const dictionaryKey = keyMatch[2] ?? keyMatch[3];\n if (!dictionaryKey) return null;\n\n // Find the content: section start\n const contentSectionMatch = /^content\\s*:/m.exec(text);\n if (!contentSectionMatch) return null;\n const contentStart =\n contentSectionMatch.index + contentSectionMatch[0].length;\n\n // Find the end of the content section: next top-level key (no leading whitespace)\n // or end of file.\n const afterContent = text.slice(contentStart);\n const nextTopLevelMatch = /\\r?\\n[a-zA-Z_$][a-zA-Z0-9_$]*\\s*:/m.exec(\n afterContent\n );\n const contentEnd = nextTopLevelMatch\n ? contentStart + nextTopLevelMatch.index\n : text.length;\n\n // The cursor must be within the content block\n if (offset < contentStart || offset > contentEnd) return null;\n\n // Find all ` fieldName:` patterns within the content block.\n // We look for lines that have at least 2 spaces of indentation and a\n // word-character key followed by a colon.\n const contentText = text.slice(contentStart, contentEnd);\n const fieldPattern = /^([ \\t]+)([a-zA-Z_$][a-zA-Z0-9_$]*)[ \\t]*:/gm;\n\n let bestMatch: FieldResult | null = null;\n let fieldMatch: RegExpExecArray | null;\n\n while (true) {\n fieldMatch = fieldPattern.exec(contentText);\n if (fieldMatch === null) break;\n\n const fieldName = fieldMatch[2]!;\n\n // Skip meta-keys and locale codes\n if (YAML_META_KEYS.has(fieldName)) continue;\n if (LOCALE_CODE_PATTERN.test(fieldName)) continue;\n\n const fieldKeyStart =\n contentStart + fieldMatch.index + fieldMatch[1]!.length; // skip indentation\n const fieldKeyEnd = fieldKeyStart + fieldName.length;\n\n if (offset >= fieldKeyStart && offset <= fieldKeyEnd) {\n bestMatch = { dictionaryKey, fieldName, fieldPath: [fieldName] };\n break;\n }\n }\n\n return bestMatch;\n};\n\n// ---------------------------------------------------------------------------\n// Oxc (TS / JS) AST path\n// ---------------------------------------------------------------------------\n\nconst findContentFieldAtOffsetOxc = (\n _text: string,\n offset: number,\n program: OxcNode\n): FieldResult | null => {\n let foundResult: FieldResult | null = null;\n\n walkAst(program, (node) => {\n if (foundResult) return true;\n if (node['type'] !== 'ObjectExpression') return;\n\n const properties = node['properties'] as OxcNode[] | undefined;\n if (!properties?.length) return;\n\n // Look for sibling `key:` and `content:` properties in the same object\n let dictionaryKey: string | null = null;\n let contentNode: OxcNode | null = null;\n\n for (const prop of properties) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey || !propValue) continue;\n\n const keyName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n\n if (keyName === 'key') dictionaryKey = getStringValue(propValue);\n if (keyName === 'content') contentNode = propValue;\n }\n\n if (!dictionaryKey || !contentNode) return;\n\n // Walk the `content:` object tree, tracking the path to each property key\n const walkContent = (obj: OxcNode, path: string[]): boolean => {\n if (obj['type'] !== 'ObjectExpression') return false;\n const props = obj['properties'] as OxcNode[] | undefined;\n if (!props) return false;\n\n for (const prop of props) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey) continue;\n\n const fieldName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n if (!fieldName) continue;\n\n const keyStart = propKey['start'] as number;\n const keyEnd = propKey['end'] as number;\n\n // Cursor is on this property key\n if (offset >= keyStart && offset <= keyEnd) {\n const fieldPath = [...path, fieldName];\n foundResult = { dictionaryKey: dictionaryKey!, fieldName, fieldPath };\n return true;\n }\n\n if (!propValue) continue;\n // Skip all intlayer leaf calls — their arguments are not navigable content fields\n if (isIntlayerLeafCall(propValue)) continue;\n // Recurse into nested plain objects\n if (propValue['type'] === 'ObjectExpression') {\n if (walkContent(propValue, [...path, fieldName])) return true;\n }\n }\n\n return false;\n };\n\n if (walkContent(contentNode, [])) return true;\n });\n\n return foundResult;\n};\n"],"mappings":";;;AAIA,MAAM,qCACJ;;;;;;;;;AAUF,MAAa,wBACX,MACA,WACkB;AAClB,MAAK,MAAM,SAAS,KAAK,SAAS,mCAAmC,EAAE;EACrE,MAAM,QAAQ,MAAM;EACpB,MAAM,MAAM,QAAQ,MAAM,GAAG;AAE7B,MAAI,UAAU,SAAS,UAAU,IAC/B,QAAO,MAAM,MAAM,MAAM,MAAM;;AAInC,QAAO;;;;;;;;;;;;;;;;AAiBT,MAAa,4BACX,MACA,QACA,kBACuB;CACvB,MAAM,MAAM,eAAe,aAAa,IAAI;AAG5C,KAAI,QAAQ,SAAS,QAAQ,OAAQ,QAAO;AAG5C,KAAI,QAAQ,WAAW,QAAQ,YAAY,QAAQ,SACjD,QAAO,6BAA6B,MAAM,OAAO;AAInD,KAAI,QAAQ,WAAW,QAAQ,OAC7B,QAAO,6BAA6B,MAAM,OAAO;CAInD,MAAM,UAAU,UAAU,KAAK;AAC/B,KAAI,QACF,QAAO,4BAA4B,MAAM,QAAQ,QAAQ;AAK3D,KAAI,SAAS,KAAK,KAAK,CACrB,QAAO,6BAA6B,MAAM,OAAO;AAInD,KAAI,eAAe,KAAK,KAAK,EAC3B;MAAI,CAAC,gBAAgB,KAAK,KAAK,CAAE,QAAO;;AAI1C,QAAO,6BAA6B,MAAM,OAAO;;;;;;;;;;;;;;;;;;AAuBnD,MAAM,2BAAgD,IAAI,IAAI;CAC5D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;AAGF,MAAM,sBAAsB,SAA2B;AACrD,KAAI,KAAK,YAAY,iBAAkB,QAAO;CAC9C,MAAM,SAAS,KAAK;AACpB,QACE,SAAS,YAAY,gBACrB,yBAAyB,IAAI,OAAO,QAAkB;;;AAK1D,MAAM,kBAAkB,SAAiC;AACvD,MACG,KAAK,YAAY,aAAa,KAAK,YAAY,oBAChD,OAAO,KAAK,aAAa,SAEzB,QAAO,KAAK;AAEd,KAAI,KAAK,YAAY,mBAAmB;EAEtC,MAAM,YADS,KAAK,YACO,KAAK;AAChC,SACG,YAAY,aACZ,YAAY,UACb;;AAGJ,QAAO;;;;;;;AAYT,MAAM,0BAA+C,IAAI,IAAI;CAC3D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;AAQF,MAAM,qBACJ,MACA,WACA,YACA,aACW;CACX,MAAM,UAAU,IAAI,OAClB,IAAI,UAAU,QAAQ,uBAAuB,OAAO,CAAC,IACrD,IACD;AACD,SAAQ,YAAY;CACpB,IAAI;AACJ,QAAO,MAAM;AACX,UAAQ,QAAQ,KAAK,KAAK;AAC1B,MAAI,UAAU,KAAM;AACpB,MAAI,MAAM,SAAS,SAAU;AAC7B,SAAO,MAAM;;AAEf,QAAO;;;;;;AAOT,MAAM,qBAAqB,SACzB,KAEG,QAAQ,uBAAuB,GAAG,CAElC,QAAQ,qBAAqB,GAAG,CAEhC,QAAQ,gBAAgB,KAAK;;;;;;;;;;AAiBlC,MAAM,gCACJ,MACA,WACuB;CACvB,IAAI;AACJ,KAAI;AACF,WAAS,KAAK,MAAM,kBAAkB,KAAK,CAAC;SACtC;AACN,SAAO;;CAGT,MAAM,gBACJ,OAAO,OAAO,WAAW,WAAW,OAAO,SAAS;AACtD,KAAI,CAAC,cAAe,QAAO;CAE3B,MAAM,aAAa,OAAO;AAC1B,KACE,CAAC,cACD,OAAO,eAAe,YACtB,MAAM,QAAQ,WAAW,CAEzB,QAAO;CAIT,MAAM,kBAAkB,KAAK,QAAQ,cAAY;AACjD,KAAI,oBAAoB,GAAI,QAAO;;;;;;;;;;CAWnC,MAAM,QACJ,KACA,MACA,YACA,aACuB;AACvB,OAAK,MAAM,CAAC,WAAW,eAAe,OAAO,QAAQ,IAAI,EAAE;AAEzD,OAAI,wBAAwB,IAAI,UAAU,CAAE;AAG5C,OAAI,yBAAyB,KAAK,UAAU,CAAE;GAE9C,MAAM,WAAW,kBAAkB,MAAM,WAAW,YAAY,SAAS;AACzE,OAAI,aAAa,GAAI;GACrB,MAAM,SAAS,WAAW,UAAU,SAAS;AAE7C,OAAI,UAAU,YAAY,UAAU,OAClC,QAAO;IACL;IACA;IACA,WAAW,CAAC,GAAG,MAAM,UAAU;IAChC;AAIH,OACE,eAAe,QACf,OAAO,eAAe,YACtB,CAAC,MAAM,QAAQ,WAAW,EAC1B;IACA,MAAM,SAAS;AAEf,QAAI,cAAc,OAAQ;IAE1B,MAAM,SAAS,KAAK,QAAQ,CAAC,GAAG,MAAM,UAAU,EAAE,QAAQ,SAAS;AACnE,QAAI,OAAQ,QAAO;;;AAGvB,SAAO;;AAGT,QAAO,KACL,YACA,EAAE,EACF,iBACA,KAAK,OACN;;;;;;AAWH,MAAM,sBAAsB;;;;;AAM5B,MAAM,iBAAsC,IAAI,IAAI;CAClD;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CAEA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACD,CAAC;;;;;;;;;;;;;;;;;AAkBF,MAAM,gCACJ,MACA,WACuB;CAEvB,MAAM,WACJ,4DAA4D,KAAK,KAAK;AACxE,KAAI,CAAC,SAAU,QAAO;CACtB,MAAM,gBAAgB,SAAS,MAAM,SAAS;AAC9C,KAAI,CAAC,cAAe,QAAO;CAG3B,MAAM,sBAAsB,gBAAgB,KAAK,KAAK;AACtD,KAAI,CAAC,oBAAqB,QAAO;CACjC,MAAM,eACJ,oBAAoB,QAAQ,oBAAoB,GAAG;CAIrD,MAAM,eAAe,KAAK,MAAM,aAAa;CAC7C,MAAM,oBAAoB,qCAAqC,KAC7D,aACD;CACD,MAAM,aAAa,oBACf,eAAe,kBAAkB,QACjC,KAAK;AAGT,KAAI,SAAS,gBAAgB,SAAS,WAAY,QAAO;CAKzD,MAAM,cAAc,KAAK,MAAM,cAAc,WAAW;CACxD,MAAM,eAAe;CAErB,IAAI,YAAgC;CACpC,IAAI;AAEJ,QAAO,MAAM;AACX,eAAa,aAAa,KAAK,YAAY;AAC3C,MAAI,eAAe,KAAM;EAEzB,MAAM,YAAY,WAAW;AAG7B,MAAI,eAAe,IAAI,UAAU,CAAE;AACnC,MAAI,oBAAoB,KAAK,UAAU,CAAE;EAEzC,MAAM,gBACJ,eAAe,WAAW,QAAQ,WAAW,GAAI;EACnD,MAAM,cAAc,gBAAgB,UAAU;AAE9C,MAAI,UAAU,iBAAiB,UAAU,aAAa;AACpD,eAAY;IAAE;IAAe;IAAW,WAAW,CAAC,UAAU;IAAE;AAChE;;;AAIJ,QAAO;;AAOT,MAAM,+BACJ,OACA,QACA,YACuB;CACvB,IAAI,cAAkC;AAEtC,SAAQ,UAAU,SAAS;AACzB,MAAI,YAAa,QAAO;AACxB,MAAI,KAAK,YAAY,mBAAoB;EAEzC,MAAM,aAAa,KAAK;AACxB,MAAI,CAAC,YAAY,OAAQ;EAGzB,IAAI,gBAA+B;EACnC,IAAI,cAA8B;AAElC,OAAK,MAAM,QAAQ,YAAY;AAC7B,OAAI,KAAK,YAAY,WAAY;GACjC,MAAM,UAAU,KAAK;GACrB,MAAM,YAAY,KAAK;AACvB,OAAI,CAAC,WAAW,CAAC,UAAW;GAE5B,MAAM,UACH,QAAQ,WACR,QAAQ;AAEX,OAAI,YAAY,MAAO,iBAAgB,eAAe,UAAU;AAChE,OAAI,YAAY,UAAW,eAAc;;AAG3C,MAAI,CAAC,iBAAiB,CAAC,YAAa;EAGpC,MAAM,eAAe,KAAc,SAA4B;AAC7D,OAAI,IAAI,YAAY,mBAAoB,QAAO;GAC/C,MAAM,QAAQ,IAAI;AAClB,OAAI,CAAC,MAAO,QAAO;AAEnB,QAAK,MAAM,QAAQ,OAAO;AACxB,QAAI,KAAK,YAAY,WAAY;IACjC,MAAM,UAAU,KAAK;IACrB,MAAM,YAAY,KAAK;AACvB,QAAI,CAAC,QAAS;IAEd,MAAM,YACH,QAAQ,WACR,QAAQ;AACX,QAAI,CAAC,UAAW;IAEhB,MAAM,WAAW,QAAQ;IACzB,MAAM,SAAS,QAAQ;AAGvB,QAAI,UAAU,YAAY,UAAU,QAAQ;KAC1C,MAAM,YAAY,CAAC,GAAG,MAAM,UAAU;AACtC,mBAAc;MAAiB;MAAgB;MAAW;MAAW;AACrE,YAAO;;AAGT,QAAI,CAAC,UAAW;AAEhB,QAAI,mBAAmB,UAAU,CAAE;AAEnC,QAAI,UAAU,YAAY,oBACxB;SAAI,YAAY,WAAW,CAAC,GAAG,MAAM,UAAU,CAAC,CAAE,QAAO;;;AAI7D,UAAO;;AAGT,MAAI,YAAY,aAAa,EAAE,CAAC,CAAE,QAAO;GACzC;AAEF,QAAO"}
|
|
1
|
+
{"version":3,"file":"findKeyInContentFile.mjs","names":[],"sources":["../../src/findKeyInContentFile.ts"],"sourcesContent":["import { type OxcNode, parseText, walkAst } from './oxcUtils';\n\n// Group 2: quoted value — key: \"foo\" | key: 'foo' | key: `foo` (TS/JS/JSON)\n// Group 3: unquoted value — key: foo-bar (YAML / Markdown frontmatter)\nconst KEY_DECLARATION_REGULAR_EXPRESSION =\n /(?:^|[{,\\s])\"?key\"?\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^ \\t'\"`{},\\r\\n]+))/g;\n\n/**\n * If `offset` falls within a `key:` declaration in a content file,\n * return the declared key string; otherwise return null.\n * Handles:\n * - Quoted values: key: \"foo\" | key: 'foo' | key: `foo` (TS/JS)\n * - JSON values: \"key\": \"foo\" (JSON/JSON5/JSONC)\n * - Bare values: key: foo-bar (YAML / Markdown frontmatter)\n */\nexport const findKeyInContentFile = (\n text: string,\n offset: number\n): string | null => {\n for (const match of text.matchAll(KEY_DECLARATION_REGULAR_EXPRESSION)) {\n const start = match.index!;\n const end = start + match[0].length;\n\n if (offset >= start && offset <= end) {\n return match[2] ?? match[3] ?? null;\n }\n }\n\n return null;\n};\n\n/**\n * If `offset` falls on a content field property key inside an intlayer content\n * file, return the dictionary key, the leaf field name, and the full field path.\n *\n * The `fileExtension` parameter controls which parser is used:\n *\n * - `.json` / `.json5` / `.jsonc` → dedicated JSON parser\n * - `.yaml` / `.yml` → YAML regex heuristic\n * - `.md` / `.mdx` → always null (body is the content value)\n * - everything else → Oxc AST (TS/JS/TSX/JSX)\n *\n * When `fileExtension` is omitted the function falls back to Oxc, then to\n * JSON/YAML heuristics based on text shape.\n */\nexport const findContentFieldAtOffset = (\n text: string,\n offset: number,\n fileExtension?: string\n): FieldResult | null => {\n const ext = fileExtension?.toLowerCase() ?? '';\n\n // Markdown has no individual navigable content fields\n if (ext === '.md' || ext === '.mdx') return null;\n\n // JSON / JSON5 / JSONC\n if (ext === '.json' || ext === '.json5' || ext === '.jsonc') {\n return findContentFieldAtOffsetJson(text, offset);\n }\n\n // YAML / YML\n if (ext === '.yaml' || ext === '.yml') {\n return findContentFieldAtOffsetYaml(text, offset);\n }\n\n // TS / JS / TSX / JSX — use Oxc AST\n const program = parseText(text);\n if (program) {\n return findContentFieldAtOffsetOxc(text, offset, program);\n }\n\n // Oxc failed to parse — fall back on content-shape heuristics\n // JSON: starts with optional whitespace then `{`\n if (/^\\s*\\{/.test(text)) {\n return findContentFieldAtOffsetJson(text, offset);\n }\n\n // Markdown: YAML frontmatter fence with no `content:` block\n if (/^---\\s*\\r?\\n/.test(text)) {\n if (!/^content\\s*:/m.test(text)) return null;\n }\n\n // YAML (or anything else with a content: block)\n return findContentFieldAtOffsetYaml(text, offset);\n};\n\n// ---------------------------------------------------------------------------\n// Helpers shared by the Oxc and YAML/JSON fallback paths\n// ---------------------------------------------------------------------------\n\n/**\n * All Intlayer helper function names whose arguments are locale-map or\n * control-flow data — NOT navigable content field names. The walker must\n * treat these calls as opaque leaf values and never descend into them.\n *\n * `t` – translation ({ en: \"…\", fr: \"…\" })\n * `enu` – enumeration ({ \"0\": \"…\", \">5\": \"…\" })\n * `cond` – condition ({ true: \"…\", false: \"…\" })\n * `plural` – plural rules ({ one: \"…\", other: \"…\" })\n * `gender` – gender rules ({ male: \"…\", female: \"…\", other: \"…\" })\n * `insert` – insertion (\"Hello {{name}}!\")\n * `nest` – nested dict (\"navbar\")\n * `md` – markdown (\"# …\")\n * `file` – file reference (\"./path/to/file.txt\")\n * `html` – html content (\"<p>…</p>\")\n */\nconst INTLAYER_LEAF_CALL_NAMES: ReadonlySet<string> = new Set([\n 't',\n 'enu',\n 'cond',\n 'plural',\n 'gender',\n 'insert',\n 'nest',\n 'md',\n 'file',\n 'html',\n]);\n\n/** Returns true when `node` is a call to any intlayer leaf helper function. */\nconst isIntlayerLeafCall = (node: OxcNode): boolean => {\n if (node['type'] !== 'CallExpression') return false;\n const callee = node['callee'] as OxcNode | undefined;\n return (\n callee?.['type'] === 'Identifier' &&\n INTLAYER_LEAF_CALL_NAMES.has(callee['name'] as string)\n );\n};\n\n/** Extract a string value from a Literal / StringLiteral / TemplateLiteral node. */\nconst getStringValue = (node: OxcNode): string | null => {\n if (\n (node['type'] === 'Literal' || node['type'] === 'StringLiteral') &&\n typeof node['value'] === 'string'\n ) {\n return node['value'] as string;\n }\n if (node['type'] === 'TemplateLiteral') {\n const quasis = node['quasis'] as OxcNode[] | undefined;\n const valueNode = quasis?.[0]?.['value'] as OxcNode | undefined;\n return (\n (valueNode?.['cooked'] as string | undefined) ??\n (valueNode?.['raw'] as string | undefined) ??\n null\n );\n }\n return null;\n};\n\n// ---------------------------------------------------------------------------\n// JSON content-file support\n// ---------------------------------------------------------------------------\n\n/**\n * Well-known meta-keys that appear inside Intlayer typed nodes in JSON files.\n * e.g. { \"nodeType\": \"translation\", \"translation\": { \"en\": \"…\" } }\n * None of these are navigable content fields.\n */\nconst JSON_INTLAYER_NODE_KEYS: ReadonlySet<string> = new Set([\n 'nodeType',\n 'translation',\n 'enumeration',\n 'condition',\n 'plural',\n 'insertion',\n 'nested',\n 'markdown',\n 'html',\n 'file',\n 'composite',\n 'dictionaryKey',\n // JSON schema / system properties\n '$schema',\n 'id',\n 'projectIds',\n 'localId',\n 'localIds',\n 'filePath',\n 'versions',\n 'filled',\n // Top-level dictionary meta-properties (not inside content)\n 'key',\n 'title',\n 'description',\n 'tags',\n 'format',\n 'locale',\n 'schema',\n 'location',\n 'fill',\n 'priority',\n 'version',\n 'importMode',\n]);\n\n/**\n * Find the byte offset of `fieldName` (as a JSON property key) inside `text`,\n * searching within the range [searchFrom, searchTo).\n *\n * Matches `\"fieldName\"` surrounded by typical JSON delimiters.\n */\nconst findJsonKeyOffset = (\n text: string,\n fieldName: string,\n searchFrom: number,\n searchTo: number\n): number => {\n const pattern = new RegExp(\n `\"${fieldName.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}\"`,\n 'g'\n );\n pattern.lastIndex = searchFrom;\n let match: RegExpExecArray | null;\n while (true) {\n match = pattern.exec(text);\n if (match === null) break;\n if (match.index >= searchTo) break;\n return match.index;\n }\n return -1;\n};\n\n/**\n * Strip JSON5/JSONC single-line (//) and multi-line (/* *\\/\\/) comments,\n * and trailing commas, producing valid JSON that JSON.parse can handle.\n */\nconst stripJsonComments = (text: string): string =>\n text\n // Remove single-line comments (// …) EXCEPT in URLs (e.g., https://)\n .replace(/(?<!:)\\/\\/[^\\r\\n]*/g, '')\n // Remove block comments (/* … */)\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, '')\n // Remove trailing commas before } or ]\n .replace(/,(\\s*[}\\]])/g, '$1');\n\ntype FieldResult = {\n dictionaryKey: string;\n fieldName: string;\n fieldPath: string[];\n};\n\n/**\n * Parse a JSON/JSON5/JSONC content file and, if `offset` falls on a content\n * field key, return the dictionary key, leaf field name, and full field path.\n *\n * Returns null when:\n * - The text is not valid JSON (even after comment stripping)\n * - The cursor is not on a navigable content field key\n * - The field is a meta/system key (nodeType, translation, locale codes, …)\n */\nconst findContentFieldAtOffsetJson = (\n text: string,\n offset: number\n): FieldResult | null => {\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(stripJsonComments(text)) as Record<string, unknown>;\n } catch {\n return null;\n }\n\n const dictionaryKey =\n typeof parsed['key'] === 'string' ? parsed['key'] : null;\n if (!dictionaryKey) return null;\n\n const contentObj = parsed['content'];\n if (\n !contentObj ||\n typeof contentObj !== 'object' ||\n Array.isArray(contentObj)\n )\n return null;\n\n // Find the byte range of the \"content\" block in raw text so we can scope\n // offset comparisons to content fields only.\n const contentKeyIndex = text.indexOf('\"content\"');\n if (contentKeyIndex === -1) return null;\n\n /**\n * Recursively walk the parsed content object.\n * For each property key, check whether the raw text position of that key\n * (within the remaining text slice) overlaps with `offset`.\n *\n * `textFrom` is where we start searching for each key in the raw text —\n * we advance it as we consume properties to avoid false matches on\n * identically-named sibling keys.\n */\n const walk = (\n obj: Record<string, unknown>,\n path: string[],\n searchFrom: number,\n searchTo: number\n ): FieldResult | null => {\n for (const [fieldName, fieldValue] of Object.entries(obj)) {\n // Skip Intlayer-internal node keys — they are not navigable content fields\n if (JSON_INTLAYER_NODE_KEYS.has(fieldName)) continue;\n\n // Skip keys that look like locale codes (e.g. \"en\", \"fr\", \"en-GB\")\n if (/^[a-z]{2}(-[A-Z]{2})?$/.test(fieldName)) continue;\n\n const keyStart = findJsonKeyOffset(text, fieldName, searchFrom, searchTo);\n if (keyStart === -1) continue;\n const keyEnd = keyStart + fieldName.length + 2; // +2 for surrounding quotes\n\n if (offset >= keyStart && offset <= keyEnd) {\n return {\n dictionaryKey,\n fieldName,\n fieldPath: [...path, fieldName],\n };\n }\n\n // Recurse into plain nested objects (skip typed nodes — they have nodeType)\n if (\n fieldValue !== null &&\n typeof fieldValue === 'object' &&\n !Array.isArray(fieldValue)\n ) {\n const nested = fieldValue as Record<string, unknown>;\n // Skip Intlayer typed nodes — their children are not content fields\n if ('nodeType' in nested) continue;\n\n const result = walk(nested, [...path, fieldName], keyEnd, searchTo);\n if (result) return result;\n }\n }\n return null;\n };\n\n return walk(\n contentObj as Record<string, unknown>,\n [],\n contentKeyIndex,\n text.length\n );\n};\n\n// ---------------------------------------------------------------------------\n// YAML / Markdown frontmatter fallback\n// ---------------------------------------------------------------------------\n\n/**\n * Locale-code pattern — keys matching this inside YAML content blocks are\n * translation locale identifiers, not navigable content fields.\n */\nconst LOCALE_CODE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;\n\n/**\n * YAML / Markdown meta-keys that appear at the top level or inside typed nodes.\n * These should not be treated as navigable content fields.\n */\nconst YAML_META_KEYS: ReadonlySet<string> = new Set([\n 'nodeType',\n 'translation',\n 'enumeration',\n 'condition',\n 'plural',\n 'insertion',\n 'nested',\n 'markdown',\n 'html',\n 'file',\n 'composite',\n 'dictionaryKey',\n // Frontmatter / top-level keys\n 'key',\n 'title',\n 'description',\n 'tags',\n 'format',\n 'locale',\n 'schema',\n 'location',\n 'fill',\n 'priority',\n 'version',\n 'importMode',\n '$schema',\n]);\n\n/**\n * Improved regex fallback for YAML and Markdown content files.\n *\n * Strategy:\n * 1. Extract the dictionary key from the frontmatter / top-level `key:` line.\n * 2. Locate the `content:` section (YAML files only; Markdown has no `content:` block).\n * 3. Within the content section, find all `fieldName:` patterns at consistent\n * indentation levels, skipping known meta-keys and locale codes.\n * 4. Check whether `offset` falls within one of those key spans.\n * 5. Return a single-element `fieldPath` (deep path tracking is impractical\n * without a full YAML parser, but single-level is enough for most use cases).\n *\n * For Markdown files, `findContentFieldAtOffset` always returns null because\n * the body text is the content value — individual content \"fields\" are not\n * declared as separate properties.\n */\nconst findContentFieldAtOffsetYaml = (\n text: string,\n offset: number\n): FieldResult | null => {\n // Extract dictionary key\n const keyMatch =\n /^key\\s*:\\s*(?:(['\"`])([^'\"`\\r\\n]+)\\1|([^\\s'\"`{},\\r\\n]+))/m.exec(text);\n if (!keyMatch) return null;\n const dictionaryKey = keyMatch[2] ?? keyMatch[3];\n if (!dictionaryKey) return null;\n\n // Find the content: section start\n const contentSectionMatch = /^content\\s*:/m.exec(text);\n if (!contentSectionMatch) return null;\n const contentStart =\n contentSectionMatch.index + contentSectionMatch[0].length;\n\n // Find the end of the content section: next top-level key (no leading whitespace)\n // or end of file.\n const afterContent = text.slice(contentStart);\n const nextTopLevelMatch = /\\r?\\n[a-zA-Z_$][a-zA-Z0-9_$]*\\s*:/m.exec(\n afterContent\n );\n const contentEnd = nextTopLevelMatch\n ? contentStart + nextTopLevelMatch.index\n : text.length;\n\n // The cursor must be within the content block\n if (offset < contentStart || offset > contentEnd) return null;\n\n // Find all ` fieldName:` patterns within the content block.\n // We look for lines that have at least 2 spaces of indentation and a\n // word-character key followed by a colon.\n const contentText = text.slice(contentStart, contentEnd);\n const fieldPattern = /^([ \\t]+)([a-zA-Z_$][a-zA-Z0-9_$]*)[ \\t]*:/gm;\n\n let bestMatch: FieldResult | null = null;\n let fieldMatch: RegExpExecArray | null;\n\n while (true) {\n fieldMatch = fieldPattern.exec(contentText);\n if (fieldMatch === null) break;\n\n const fieldName = fieldMatch[2]!;\n\n // Skip meta-keys and locale codes\n if (YAML_META_KEYS.has(fieldName)) continue;\n if (LOCALE_CODE_PATTERN.test(fieldName)) continue;\n\n const fieldKeyStart =\n contentStart + fieldMatch.index + fieldMatch[1]!.length; // skip indentation\n const fieldKeyEnd = fieldKeyStart + fieldName.length;\n\n if (offset >= fieldKeyStart && offset <= fieldKeyEnd) {\n bestMatch = { dictionaryKey, fieldName, fieldPath: [fieldName] };\n break;\n }\n }\n\n return bestMatch;\n};\n\n// ---------------------------------------------------------------------------\n// Oxc (TS / JS) AST path\n// ---------------------------------------------------------------------------\n\nconst findContentFieldAtOffsetOxc = (\n _text: string,\n offset: number,\n program: OxcNode\n): FieldResult | null => {\n let foundResult: FieldResult | null = null;\n\n walkAst(program, (node) => {\n if (foundResult) return true;\n if (node['type'] !== 'ObjectExpression') return;\n\n const properties = node['properties'] as OxcNode[] | undefined;\n if (!properties?.length) return;\n\n // Look for sibling `key:` and `content:` properties in the same object\n let dictionaryKey: string | null = null;\n let contentNode: OxcNode | null = null;\n\n for (const prop of properties) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey || !propValue) continue;\n\n const keyName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n\n if (keyName === 'key') dictionaryKey = getStringValue(propValue);\n if (keyName === 'content') contentNode = propValue;\n }\n\n if (!dictionaryKey || !contentNode) return;\n\n // Walk the `content:` object tree, tracking the path to each property key\n const walkContent = (obj: OxcNode, path: string[]): boolean => {\n if (obj['type'] !== 'ObjectExpression') return false;\n const props = obj['properties'] as OxcNode[] | undefined;\n if (!props) return false;\n\n for (const prop of props) {\n if (prop['type'] !== 'Property') continue;\n const propKey = prop['key'] as OxcNode | undefined;\n const propValue = prop['value'] as OxcNode | undefined;\n if (!propKey) continue;\n\n const fieldName =\n (propKey['name'] as string | undefined) ??\n (propKey['value'] as string | undefined);\n if (!fieldName) continue;\n\n const keyStart = propKey['start'] as number;\n const keyEnd = propKey['end'] as number;\n\n // Cursor is on this property key\n if (offset >= keyStart && offset <= keyEnd) {\n const fieldPath = [...path, fieldName];\n foundResult = { dictionaryKey: dictionaryKey!, fieldName, fieldPath };\n return true;\n }\n\n if (!propValue) continue;\n // Skip all intlayer leaf calls — their arguments are not navigable content fields\n if (isIntlayerLeafCall(propValue)) continue;\n // Recurse into nested plain objects\n if (propValue['type'] === 'ObjectExpression') {\n if (walkContent(propValue, [...path, fieldName])) return true;\n }\n }\n\n return false;\n };\n\n if (walkContent(contentNode, [])) return true;\n });\n\n return foundResult;\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findTCallAtOffset.mjs","names":[],"sources":["../../src/findTCallAtOffset.ts"],"sourcesContent":["import { findMessageUsageAtOffset } from './usageAnalyzer';\n\n/**\n * Detect when the cursor is on a message usage carrying an explicit id: a\n * `t('key')` call from any translator caller, a self call\n * (`formatMessage({ id })`, `i18n._('id')`), a JSX message component or a\n * lingui tagged template.\n *\n * Thin compatibility wrapper over `findMessageUsageAtOffset` — kept for the\n * VS Code extension. `fieldName` is the dotted field path.\n *\n * Handles all assignment forms:\n * const t = useTranslations('ns'); t('field')\n * const { t } = useTranslation('ns'); t('field')\n * const { t: translate } = useTranslation('ns'); translate('field')\n * const t = await getFixedT('en', 'ns'); t('field')\n * const { t } = useI18n({ namespace: 'ns' }); t('field')\n */\nexport const findTCallAtOffset = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string } | null => {\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (!usage) return null;\n\n if (\n usage.kind !== 'call' &&\n usage.kind !== 'jsx' &&\n usage.kind !== 'tagged-template'\n ) {\n return null;\n }\n\n if (usage.fieldPath.length === 0) return null;\n\n return {\n dictionaryKey: usage.dictionaryKey,\n fieldName: usage.fieldPath.join('.'),\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,MAAa,qBACX,MACA,WACwD;CACxD,MAAM,QAAQ,yBAAyB,MAAM,
|
|
1
|
+
{"version":3,"file":"findTCallAtOffset.mjs","names":[],"sources":["../../src/findTCallAtOffset.ts"],"sourcesContent":["import { findMessageUsageAtOffset } from './usageAnalyzer';\n\n/**\n * Detect when the cursor is on a message usage carrying an explicit id: a\n * `t('key')` call from any translator caller, a self call\n * (`formatMessage({ id })`, `i18n._('id')`), a JSX message component or a\n * lingui tagged template.\n *\n * Thin compatibility wrapper over `findMessageUsageAtOffset` — kept for the\n * VS Code extension. `fieldName` is the dotted field path.\n *\n * Handles all assignment forms:\n * const t = useTranslations('ns'); t('field')\n * const { t } = useTranslation('ns'); t('field')\n * const { t: translate } = useTranslation('ns'); translate('field')\n * const t = await getFixedT('en', 'ns'); t('field')\n * const { t } = useI18n({ namespace: 'ns' }); t('field')\n */\nexport const findTCallAtOffset = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string } | null => {\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (!usage) return null;\n\n if (\n usage.kind !== 'call' &&\n usage.kind !== 'jsx' &&\n usage.kind !== 'tagged-template'\n ) {\n return null;\n }\n\n if (usage.fieldPath.length === 0) return null;\n\n return {\n dictionaryKey: usage.dictionaryKey,\n fieldName: usage.fieldPath.join('.'),\n };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;;AAkBA,MAAa,qBACX,MACA,WACwD;CACxD,MAAM,QAAQ,yBAAyB,MAAM,MAAM;CAEnD,IAAI,CAAC,OAAO,OAAO;CAEnB,IACE,MAAM,SAAS,UACf,MAAM,SAAS,SACf,MAAM,SAAS,mBAEf,OAAO;CAGT,IAAI,MAAM,UAAU,WAAW,GAAG,OAAO;CAEzC,OAAO;EACL,eAAe,MAAM;EACrB,WAAW,MAAM,UAAU,KAAK,GAAG;CACrC;AACF"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findUsageFieldAtOffset.mjs","names":[],"sources":["../../src/findUsageFieldAtOffset.ts"],"sourcesContent":["import { findMessageUsageAtOffset } from './usageAnalyzer';\n\n/**\n * Detect when the cursor is on a field name that was obtained from a\n * useIntlayer / getIntlayer call in the same file. Two patterns:\n *\n * Pattern 1 — destructuring:\n * const { localeSwitcherLabel } = useIntlayer(\"locale-switcher\")\n * ^^^^^^^^^^^^^^^^^^ ← cursor here\n *\n * Pattern 2 — member access:\n * const t = useIntlayer(\"locale-switcher\")\n * t.localeSwitcherLabel ← cursor on the property\n *\n * Thin compatibility wrapper over `findMessageUsageAtOffset`.\n * Returns { dictionaryKey, fieldName } when matched, null otherwise.\n */\nexport const findUsageFieldAtOffset = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string } | null => {\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (!usage) return null;\n\n if (usage.kind !== 'member' && usage.kind !== 'destructure') return null;\n\n const fieldName = usage.fieldPath[usage.fieldPath.length - 1];\n\n if (!fieldName) return null;\n\n return { dictionaryKey: usage.dictionaryKey, fieldName };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,MAAa,0BACX,MACA,WACwD;CACxD,MAAM,QAAQ,yBAAyB,MAAM,
|
|
1
|
+
{"version":3,"file":"findUsageFieldAtOffset.mjs","names":[],"sources":["../../src/findUsageFieldAtOffset.ts"],"sourcesContent":["import { findMessageUsageAtOffset } from './usageAnalyzer';\n\n/**\n * Detect when the cursor is on a field name that was obtained from a\n * useIntlayer / getIntlayer call in the same file. Two patterns:\n *\n * Pattern 1 — destructuring:\n * const { localeSwitcherLabel } = useIntlayer(\"locale-switcher\")\n * ^^^^^^^^^^^^^^^^^^ ← cursor here\n *\n * Pattern 2 — member access:\n * const t = useIntlayer(\"locale-switcher\")\n * t.localeSwitcherLabel ← cursor on the property\n *\n * Thin compatibility wrapper over `findMessageUsageAtOffset`.\n * Returns { dictionaryKey, fieldName } when matched, null otherwise.\n */\nexport const findUsageFieldAtOffset = (\n text: string,\n offset: number\n): { dictionaryKey: string; fieldName: string } | null => {\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (!usage) return null;\n\n if (usage.kind !== 'member' && usage.kind !== 'destructure') return null;\n\n const fieldName = usage.fieldPath[usage.fieldPath.length - 1];\n\n if (!fieldName) return null;\n\n return { dictionaryKey: usage.dictionaryKey, fieldName };\n};\n"],"mappings":";;;;;;;;;;;;;;;;;;AAiBA,MAAa,0BACX,MACA,WACwD;CACxD,MAAM,QAAQ,yBAAyB,MAAM,MAAM;CAEnD,IAAI,CAAC,OAAO,OAAO;CAEnB,IAAI,MAAM,SAAS,YAAY,MAAM,SAAS,eAAe,OAAO;CAEpE,MAAM,YAAY,MAAM,UAAU,MAAM,UAAU,SAAS;CAE3D,IAAI,CAAC,WAAW,OAAO;CAEvB,OAAO;EAAE,eAAe,MAAM;EAAe;CAAU;AACzD"}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -11,7 +11,7 @@ import { existsSync } from "node:fs";
|
|
|
11
11
|
import { readFile } from "node:fs/promises";
|
|
12
12
|
import { dirname, isAbsolute, join } from "node:path";
|
|
13
13
|
import { pathToFileURL } from "node:url";
|
|
14
|
-
import { getConfiguration } from "@intlayer/config/node";
|
|
14
|
+
import { getConfiguration, searchConfigurationFile } from "@intlayer/config/node";
|
|
15
15
|
import { buildComponentFilesList } from "@intlayer/engine/utils";
|
|
16
16
|
import { getUnmergedDictionaries } from "@intlayer/unmerged-dictionaries-entry";
|
|
17
17
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
@@ -36,14 +36,37 @@ const projectDictionaries = /* @__PURE__ */ new Map();
|
|
|
36
36
|
const DICTIONARY_CACHE_TTL_MS = 5e3;
|
|
37
37
|
const projectSourceFiles = /* @__PURE__ */ new Map();
|
|
38
38
|
const SOURCE_FILES_CACHE_TTL_MS = 3e4;
|
|
39
|
+
/**
|
|
40
|
+
* Directory of the file → directory holding the closest intlayer config.
|
|
41
|
+
* Populated lazily by `getProjectRoot`.
|
|
42
|
+
*/
|
|
43
|
+
const projectRoots = /* @__PURE__ */ new Map();
|
|
39
44
|
const invalidateConfigCaches = () => {
|
|
40
45
|
projectConfigs.clear();
|
|
41
46
|
projectDictionaries.clear();
|
|
47
|
+
projectRoots.clear();
|
|
42
48
|
projectSourceFiles.clear();
|
|
43
49
|
};
|
|
50
|
+
/**
|
|
51
|
+
* Resolve the project root for a file — the directory that holds the closest
|
|
52
|
+
* intlayer config file.
|
|
53
|
+
*
|
|
54
|
+
* `getConfiguration({ baseDir })` treats `baseDir` as the project root and
|
|
55
|
+
* derives every `system.*` path from it, so passing the file's own directory
|
|
56
|
+
* would resolve `.intlayer/` next to the file (e.g. `src/.intlayer/…`) instead
|
|
57
|
+
* of at the project root — leaving every dictionary lookup empty.
|
|
58
|
+
*/
|
|
59
|
+
const getProjectRoot = (fileDirectory) => {
|
|
60
|
+
const cached = projectRoots.get(fileDirectory);
|
|
61
|
+
if (cached !== void 0) return cached;
|
|
62
|
+
const { configurationFilePath } = searchConfigurationFile(fileDirectory);
|
|
63
|
+
const root = configurationFilePath ? dirname(configurationFilePath) : workspaceRoot ?? fileDirectory;
|
|
64
|
+
projectRoots.set(fileDirectory, root);
|
|
65
|
+
return root;
|
|
66
|
+
};
|
|
44
67
|
const getProjectConfig = (absolutePath) => {
|
|
45
68
|
try {
|
|
46
|
-
const { system, build, content, compiler } = getConfiguration({ baseDir: dirname(absolutePath) });
|
|
69
|
+
const { system, build, content, compiler } = getConfiguration({ baseDir: getProjectRoot(dirname(absolutePath)) });
|
|
47
70
|
const cacheKey = system.baseDir;
|
|
48
71
|
if (!projectConfigs.has(cacheKey)) {
|
|
49
72
|
projectConfigs.set(cacheKey, {
|
|
@@ -272,7 +295,8 @@ const computeDiagnostics = (text, config) => {
|
|
|
272
295
|
return diagnostics;
|
|
273
296
|
};
|
|
274
297
|
const sendDiagnosticsForDocument = (uri, text, version) => {
|
|
275
|
-
const
|
|
298
|
+
const absolutePath = decodeURIComponent(new URL(uri).pathname);
|
|
299
|
+
const config = getProjectConfig(absolutePath);
|
|
276
300
|
const diagnostics = config && !isContentFile(uri, config) ? computeDiagnostics(text, config) : [];
|
|
277
301
|
connection.sendDiagnostics({
|
|
278
302
|
uri,
|
|
@@ -366,7 +390,8 @@ connection.onDefinition(async (parameters) => {
|
|
|
366
390
|
}
|
|
367
391
|
const text = document.getText();
|
|
368
392
|
const offset = document.offsetAt(position);
|
|
369
|
-
|
|
393
|
+
const shortenedUri = uri.replace(/^file:\/\//, "").split("/").slice(-2).join("/");
|
|
394
|
+
log(`onDefinition — ${shortenedUri} ${position.line}:${position.character}`);
|
|
370
395
|
const config = getProjectConfig(absolutePath);
|
|
371
396
|
if (config && isContentFile(uri, config)) {
|
|
372
397
|
log("onDefinition — content file: delegating to extension provider");
|
|
@@ -410,7 +435,8 @@ connection.onReferences(async (parameters) => {
|
|
|
410
435
|
if (!document) return null;
|
|
411
436
|
const text = document.getText();
|
|
412
437
|
const offset = document.offsetAt(position);
|
|
413
|
-
|
|
438
|
+
const shortenedUri = uri.replace(/^file:\/\//, "").split("/").slice(-2).join("/");
|
|
439
|
+
log(`onReferences — ${shortenedUri} ${position.line}:${position.character}`);
|
|
414
440
|
const config = getProjectConfig(absolutePath);
|
|
415
441
|
if (config && isContentFile(uri, config)) {
|
|
416
442
|
const key = findKeyInContentFile(text, offset);
|
|
@@ -571,6 +597,7 @@ documents.onDidClose((event) => {
|
|
|
571
597
|
connection.onDidChangeWatchedFiles(() => {
|
|
572
598
|
invalidateConfigCaches();
|
|
573
599
|
log("watched files changed — source file list and dictionary cache invalidated");
|
|
600
|
+
for (const document of documents.all()) scheduleDiagnostics(document.uri, document.getText(), document.version);
|
|
574
601
|
});
|
|
575
602
|
documents.listen(connection);
|
|
576
603
|
connection.listen();
|