@intlayer/lsp 9.0.0-canary.12 → 9.0.0
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 +6 -3
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/oxcUtils.mjs.map +1 -1
- package/dist/esm/usageAnalyzer.mjs +7 -4
- 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":"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"}
|
|
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
|
@@ -272,7 +272,8 @@ const computeDiagnostics = (text, config) => {
|
|
|
272
272
|
return diagnostics;
|
|
273
273
|
};
|
|
274
274
|
const sendDiagnosticsForDocument = (uri, text, version) => {
|
|
275
|
-
const
|
|
275
|
+
const absolutePath = decodeURIComponent(new URL(uri).pathname);
|
|
276
|
+
const config = getProjectConfig(absolutePath);
|
|
276
277
|
const diagnostics = config && !isContentFile(uri, config) ? computeDiagnostics(text, config) : [];
|
|
277
278
|
connection.sendDiagnostics({
|
|
278
279
|
uri,
|
|
@@ -366,7 +367,8 @@ connection.onDefinition(async (parameters) => {
|
|
|
366
367
|
}
|
|
367
368
|
const text = document.getText();
|
|
368
369
|
const offset = document.offsetAt(position);
|
|
369
|
-
|
|
370
|
+
const shortenedUri = uri.replace(/^file:\/\//, "").split("/").slice(-2).join("/");
|
|
371
|
+
log(`onDefinition — ${shortenedUri} ${position.line}:${position.character}`);
|
|
370
372
|
const config = getProjectConfig(absolutePath);
|
|
371
373
|
if (config && isContentFile(uri, config)) {
|
|
372
374
|
log("onDefinition — content file: delegating to extension provider");
|
|
@@ -410,7 +412,8 @@ connection.onReferences(async (parameters) => {
|
|
|
410
412
|
if (!document) return null;
|
|
411
413
|
const text = document.getText();
|
|
412
414
|
const offset = document.offsetAt(position);
|
|
413
|
-
|
|
415
|
+
const shortenedUri = uri.replace(/^file:\/\//, "").split("/").slice(-2).join("/");
|
|
416
|
+
log(`onReferences — ${shortenedUri} ${position.line}:${position.character}`);
|
|
414
417
|
const config = getProjectConfig(absolutePath);
|
|
415
418
|
if (config && isContentFile(uri, config)) {
|
|
416
419
|
const key = findKeyInContentFile(text, offset);
|
package/dist/esm/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { buildComponentFilesList } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { getUnmergedDictionaries } from '@intlayer/unmerged-dictionaries-entry';\nimport {\n type CompletionItem,\n CompletionItemKind,\n type CompletionParams,\n createConnection,\n type Diagnostic,\n DiagnosticSeverity,\n type Hover,\n type HoverParams,\n type InitializeParams,\n type InitializeResult,\n Location,\n MarkupKind,\n Position,\n ProposedFeatures,\n Range,\n type ReferenceParams,\n TextDocumentSyncKind,\n TextDocuments,\n} from 'vscode-languageserver/node';\nimport { TextDocument } from 'vscode-languageserver-textdocument';\nimport {\n buildCallerWithKeyPattern,\n buildKeyUsagePatterns,\n getCallerNamesAlternation,\n} from './callers/patterns';\nimport {\n formatDictionaryHover,\n formatFieldHover,\n formatFieldValue,\n getFieldByPath,\n getFieldsAtPath,\n} from './dictionaryUtils';\nimport {\n escapeRegularExpression,\n findFieldRangesInFile,\n offsetToRange,\n} from './findFieldInFile';\nimport { findKeyAtOffset } from './findKeyAtOffset';\nimport {\n findContentFieldAtOffset,\n findKeyInContentFile,\n} from './findKeyInContentFile';\nimport { type OxcNode, parseText, walkAst } from './oxcUtils';\nimport {\n collectNamespaceReferences,\n findMessageUsageAtOffset,\n matchCallNamespace,\n} from './usageAnalyzer';\n\n// Create a connection for the server, using Node's IPC as a transport.\n// Also include all preview / proposed LSP features.\nconst connection = createConnection(ProposedFeatures.all);\n\n// Create a simple text document manager.\nconst documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);\n\n/**\n * Send a log line to the client's Output panel (View → Output → \"Intlayer LSP\").\n * Uses connection.console.log so it travels over the JSON-RPC channel and never\n * touches stdout (which is reserved for the LSP protocol).\n */\nconst log = (message: string): void =>\n connection.console.log(`[Intlayer LSP] ${message}`);\n\nlet workspaceRoot: string | null | undefined;\n\ntype WorkspaceConfig = Pick<\n IntlayerConfig,\n 'system' | 'build' | 'content' | 'compiler'\n>;\n\n/**\n * Resolve (and cache) the Intlayer configuration for the current workspace.\n * The LSP process cwd is not the project root, so we must resolve the config\n * from `workspaceRoot` to get the right `system`/`build`/`content` paths.\n */\nconst projectConfigs = new Map<string, WorkspaceConfig>();\n\ntype DictionariesCache = {\n dictionaries: ReturnType<typeof getUnmergedDictionaries>;\n cachedAt: number;\n};\nconst projectDictionaries = new Map<string, DictionariesCache>();\nconst DICTIONARY_CACHE_TTL_MS = 5_000;\n\ntype SourceFilesCache = {\n files: string[];\n cachedAt: number;\n};\nconst projectSourceFiles = new Map<string, SourceFilesCache>();\nconst SOURCE_FILES_CACHE_TTL_MS = 30_000;\n\nconst invalidateConfigCaches = () => {\n projectConfigs.clear();\n projectDictionaries.clear();\n\n projectSourceFiles.clear();\n};\n\nconst getProjectConfig = (absolutePath: string): WorkspaceConfig | null => {\n try {\n const baseDir = dirname(absolutePath);\n // Find the closest intlayer config relative to the file.\n const { system, build, content, compiler } = getConfiguration({ baseDir });\n const cacheKey = system.baseDir;\n\n if (!projectConfigs.has(cacheKey)) {\n projectConfigs.set(cacheKey, { system, build, content, compiler });\n log(\n `getProjectConfig — OK baseDir=${system.baseDir} unmergedDictionariesDir=${system.unmergedDictionariesDir}`\n );\n }\n return projectConfigs.get(cacheKey)!;\n } catch (error) {\n log(`getProjectConfig — FAILED: ${error}`);\n return null;\n }\n};\n\nconst getUnmergedDictionariesCached = (\n config: WorkspaceConfig\n): ReturnType<typeof getUnmergedDictionaries> => {\n const cacheKey = config.system.baseDir;\n const cached = projectDictionaries.get(cacheKey);\n const now = Date.now();\n\n if (cached && now - cached.cachedAt < DICTIONARY_CACHE_TTL_MS) {\n return cached.dictionaries;\n }\n\n const dictionaries = getUnmergedDictionaries(config);\n projectDictionaries.set(cacheKey, { dictionaries, cachedAt: now });\n return dictionaries;\n};\n\n// ---------------------------------------------------------------------------\n// Pattern helpers — derived lazily from the live config.\n// ---------------------------------------------------------------------------\n\nconst isContentFile = (uri: string, config: WorkspaceConfig): boolean => {\n const fileExtensions = config.content.fileExtensions ?? [];\n return fileExtensions.some((extension) => uri.endsWith(extension));\n};\n\n// ---------------------------------------------------------------------------\n// Async I/O helpers\n// ---------------------------------------------------------------------------\n\n/** Read a file as UTF-8, returning null on any error. */\nconst tryReadFile = async (filePath: string): Promise<string | null> => {\n try {\n return await readFile(filePath, 'utf-8');\n } catch {\n return null;\n }\n};\n\n/**\n * Run `callback` over every item with at most `limit` tasks in-flight at once.\n * JS is single-threaded so `results.push` inside callback is race-free.\n */\nconst runConcurrent = async <T>(\n items: T[],\n callback: (item: T) => Promise<void>,\n limit = 50\n): Promise<void> => {\n if (items.length === 0) return;\n let index = 0;\n const worker = async () => {\n while (index < items.length) {\n await callback(items[index++]!);\n }\n };\n await Promise.all(\n Array.from({ length: Math.min(limit, items.length) }, worker)\n );\n};\n\n/** Find the `key: \"<key>\"` line inside a content file so the cursor lands on it. */\nconst getKeyRange = async (\n absolutePath: string,\n key: string\n): Promise<Range> => {\n try {\n const content = await readFile(absolutePath, 'utf-8');\n const lines = content.split(/\\r?\\n/);\n const keyRegularExpression = new RegExp(\n `key\\\\s*:\\\\s*['\"\\`]${key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}['\"\\`]`\n );\n\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const column = lines[lineIndex]?.search(keyRegularExpression);\n\n if (column !== undefined && column !== -1) {\n return Range.create(\n Position.create(lineIndex, column),\n Position.create(lineIndex, lines[lineIndex]!.length)\n );\n }\n }\n } catch {\n // fall through to the top of the file\n }\n\n return Range.create(Position.create(0, 0), Position.create(0, 0));\n};\n\n/**\n * Return every `.content` file location that declares the given key.\n * A key can be split across several content files (merged dictionaries), so we\n * return one `Location` per source file, pointing at the `key:` declaration.\n */\nconst getContentDeclarationLocations = async (\n key: string,\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config) return [];\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const dictionaries = Object.values(unmergedDictionaries)\n .flat()\n .filter((dictionary) => dictionary?.key === key && dictionary.filePath);\n\n const locations: Location[] = [];\n\n await runConcurrent(dictionaries, async (dictionary) => {\n const filePath = dictionary.filePath!;\n const absolutePath = isAbsolute(filePath)\n ? filePath\n : join(config.system.baseDir, filePath);\n\n if (!existsSync(absolutePath)) return;\n\n locations.push(\n Location.create(\n pathToFileURL(absolutePath).href,\n await getKeyRange(absolutePath, key)\n )\n );\n });\n\n return locations;\n};\n\n/**\n * Return the cached source-file list, re-scanning only when the cache has\n * expired or been explicitly invalidated (config change, watched-file event).\n */\nconst getSourceFiles = async (\n directory: string,\n config: WorkspaceConfig\n): Promise<string[]> => {\n const now = Date.now();\n const cached = projectSourceFiles.get(directory);\n\n if (cached && now - cached.cachedAt < SOURCE_FILES_CACHE_TTL_MS) {\n return cached.files;\n }\n const startTime = Date.now();\n const files = buildComponentFilesList(config as unknown as IntlayerConfig, [\n '**/.output/**',\n '**/.svelte-kit/**',\n '**/src-tauri/**',\n '**/.next/**',\n '**/.nuxt/**',\n '**/.expo/**',\n '**/.vercel/**',\n '**/.turbo/**',\n '**/.tanstack/**',\n '**/dist/**',\n '**/build/**',\n '**/node_modules/**',\n ]);\n projectSourceFiles.set(directory, { files, cachedAt: Date.now() });\n log(\n `source files scanned: ${files.length} file(s) in ${Date.now() - startTime}ms`\n );\n return files;\n};\n\n/**\n * Return every source-file location that calls useIntlayer / getIntlayer with\n * the given key.\n */\nconst getKeyUsageLocations = async (\n key: string,\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config) return [];\n\n const usagePatterns = buildKeyUsagePatterns(key);\n\n const filePaths = await getSourceFiles(config.system.baseDir, config);\n const locations: Location[] = [];\n const startTime = Date.now();\n\n await runConcurrent(filePaths, async (filePath) => {\n const text = await tryReadFile(filePath);\n // Quick string check before running the regexes — skips the vast majority of files\n\n if (!text?.includes(key)) return;\n\n const seenStarts = new Set<number>();\n\n for (const usagePattern of usagePatterns) {\n for (const match of text.matchAll(usagePattern)) {\n const matchStart = match.index!;\n\n if (seenStarts.has(matchStart)) continue;\n\n seenStarts.add(matchStart);\n const matchEnd = matchStart + match[0].length;\n locations.push(\n Location.create(\n pathToFileURL(filePath).href,\n offsetToRange(text, matchStart, matchEnd)\n )\n );\n }\n }\n });\n\n log(\n `getKeyUsageLocations(\"${key}\") — ${filePaths.length} file(s) searched in ${Date.now() - startTime}ms → ${locations.length} hit(s)`\n );\n return locations;\n};\n\n/**\n * Return every source-file location that accesses the field at `fieldPath` on\n * the result of `useIntlayer(dictionaryKey)` / `getIntlayer(dictionaryKey)`.\n *\n * `fieldPath` is an array of property names from the content root to the\n * target field (e.g. `['searchInput', 'text']` for a nested field).\n * Single-element paths (e.g. `['greet']`) behave like the previous\n * single-`fieldName` API.\n */\nconst getFieldUsageLocations = async (\n dictionaryKey: string,\n fieldPath: string[],\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config || fieldPath.length === 0) return [];\n\n const leafFieldName = fieldPath[fieldPath.length - 1]!;\n const escapedKey = escapeRegularExpression(dictionaryKey);\n const escapedLeaf = escapeRegularExpression(leafFieldName);\n const useCallRegularExpression = buildCallerWithKeyPattern(dictionaryKey);\n\n const filePaths = await getSourceFiles(config.system.baseDir, config);\n const locations: Location[] = [];\n const startTime = Date.now();\n\n await runConcurrent(filePaths, async (filePath) => {\n const text = await tryReadFile(filePath);\n\n if (!text) return;\n // Pre-filter: dictionaryKey and every segment of the path must appear in the file\n\n if (!text.includes(dictionaryKey)) return;\n\n if (fieldPath.some((segment) => !text.includes(segment))) return;\n\n if (!useCallRegularExpression.test(text)) return;\n\n for (const range of findFieldRangesInFile(\n text,\n escapedKey,\n leafFieldName,\n escapedLeaf,\n fieldPath\n )) {\n locations.push(Location.create(pathToFileURL(filePath).href, range));\n }\n });\n\n log(\n `getFieldUsageLocations(\"${dictionaryKey}.${fieldPath.join('.')}\") — ${filePaths.length} file(s) searched in ${Date.now() - startTime}ms → ${locations.length} hit(s)`\n );\n return locations;\n};\n\n/**\n * Find the line/column of the field's leaf name as a property key inside a\n * content file. Looks for `fieldName:` or `'field.name':` (flat keys with\n * dots, as used by lingui catalogs), not just any occurrence. Falls back to\n * the top of the file if the field cannot be located.\n */\nconst getFieldRangeInContentFile = async (\n absolutePath: string,\n fieldPath: string[]\n): Promise<Range> => {\n const content = await tryReadFile(absolutePath);\n const fieldName = fieldPath[fieldPath.length - 1];\n\n if (!content || !fieldName)\n return Range.create(Position.create(0, 0), Position.create(0, 0));\n\n // Match `fieldName:` at an object property key position (not a value),\n // allowing an optional quoted form for keys containing dots or dashes.\n const fieldRegularExpression = new RegExp(\n `(?<![.\\\\w])(['\"\\`]?)${escapeRegularExpression(fieldName)}\\\\1\\\\s*:`\n );\n const lines = content.split(/\\r?\\n/);\n\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex]!;\n const matchPosition = line.search(fieldRegularExpression);\n\n if (matchPosition === -1) continue;\n const column = line.indexOf(fieldName, matchPosition);\n\n if (column === -1) continue;\n return Range.create(\n Position.create(lineIndex, column),\n Position.create(lineIndex, column + fieldName.length)\n );\n }\n\n return Range.create(Position.create(0, 0), Position.create(0, 0));\n};\n\n/**\n * Return every content-file location where the field at `fieldPath` is\n * defined as a property of the dictionary with the given `dictionaryKey`.\n */\nconst getContentFieldLocations = async (\n dictionaryKey: string,\n fieldPath: string[],\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config) return [];\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const dictionaries = Object.values(unmergedDictionaries)\n .flat()\n .filter(\n (dictionary) => dictionary?.key === dictionaryKey && dictionary.filePath\n );\n\n const locations: Location[] = [];\n\n await runConcurrent(dictionaries, async (dictionary) => {\n const filePath = dictionary.filePath!;\n const absolutePath = isAbsolute(filePath)\n ? filePath\n : join(config.system.baseDir, filePath);\n\n if (!existsSync(absolutePath)) return;\n\n locations.push(\n Location.create(\n pathToFileURL(absolutePath).href,\n await getFieldRangeInContentFile(absolutePath, fieldPath)\n )\n );\n });\n\n return locations;\n};\n\n// ---------------------------------------------------------------------------\n// Diagnostics — push-based (no capability needed)\n// ---------------------------------------------------------------------------\n\nconst DIAGNOSTIC_DEBOUNCE_MS = 300;\nconst diagnosticTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\nconst computeDiagnostics = (\n text: string,\n config: WorkspaceConfig\n): Diagnostic[] => {\n const namespaceReferences = collectNamespaceReferences(text);\n\n if (namespaceReferences.length === 0) return [];\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const knownKeys = new Set(\n Object.values(unmergedDictionaries)\n .flat()\n .map((dictionary) => dictionary?.key)\n .filter((key): key is string => typeof key === 'string')\n );\n\n const diagnostics: Diagnostic[] = [];\n\n for (const reference of namespaceReferences) {\n if (knownKeys.has(reference.dictionaryKey)) continue;\n\n diagnostics.push({\n range: offsetToRange(\n text,\n reference.namespaceStart,\n reference.namespaceEnd\n ),\n severity: DiagnosticSeverity.Warning,\n source: 'intlayer',\n message: `Dictionary key \"${reference.dictionaryKey}\" is not declared in any content file`,\n });\n }\n\n return diagnostics;\n};\n\nconst sendDiagnosticsForDocument = (\n uri: string,\n text: string,\n version: number | null\n): void => {\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const config = getProjectConfig(absolutePath);\n\n const diagnostics =\n config && !isContentFile(uri, config)\n ? computeDiagnostics(text, config)\n : [];\n\n connection.sendDiagnostics({\n uri,\n version: version ?? undefined,\n diagnostics,\n });\n};\n\nconst scheduleDiagnostics = (\n uri: string,\n text: string,\n version: number | null\n): void => {\n const existing = diagnosticTimers.get(uri);\n\n if (existing !== undefined) clearTimeout(existing);\n\n diagnosticTimers.set(\n uri,\n setTimeout(() => {\n diagnosticTimers.delete(uri);\n sendDiagnosticsForDocument(uri, text, version);\n }, DIAGNOSTIC_DEBOUNCE_MS)\n );\n};\n\n// ---------------------------------------------------------------------------\n// Completion context detection\n// ---------------------------------------------------------------------------\n\ntype CompletionCtx =\n | { kind: 'key' }\n | { kind: 'field'; dictionaryKey: string; fieldPath: string[] }\n | null;\n\n/** Find the dictionary key for a simple variable assigned from any registered caller. */\nconst resolveVariableToDictionaryKey = (\n program: OxcNode,\n varName: string\n): string | null => {\n let result: string | null = null;\n\n walkAst(program, (node) => {\n if (result) return true;\n\n if (node['type'] !== 'VariableDeclarator') return;\n const id = node['id'] as OxcNode | undefined;\n\n if (id?.['type'] !== 'Identifier' || (id['name'] as string) !== varName)\n return;\n const init = node['init'] as OxcNode | undefined;\n\n if (!init) return;\n result = matchCallNamespace(init);\n return true;\n });\n return result;\n};\n\n/**\n * Detect what kind of completion is appropriate at the given offset.\n *\n * Handles:\n * - key completion: cursor inside first string arg of useIntlayer/getIntlayer\n * - field (member): cursor after `variable.` where variable came from useIntlayer\n * - field (destructure): cursor inside `const { | } = useIntlayer(\"key\")`\n */\nconst getCompletionContext = (\n textBefore: string,\n textAfter: string,\n program: OxcNode | null\n): CompletionCtx => {\n // Key completion\n\n if (\n new RegExp(\n `\\\\b(?:${getCallerNamesAlternation()})\\\\b(?:<[^<>]*>)?\\\\s*\\\\(\\\\s*['\"\\`][^'\"\\`\\\\n]*$`\n ).test(textBefore)\n ) {\n return { kind: 'key' };\n }\n\n // Field completion via member access: rootVar.path.to.\n const memberMatch = /\\b(\\w+)((?:\\.\\w+)*)\\.$/.exec(textBefore);\n\n if (memberMatch && program) {\n const rootVar = memberMatch[1]!;\n const chainStr = memberMatch[2]!;\n const dictKey = resolveVariableToDictionaryKey(program, rootVar);\n\n if (dictKey) {\n const fieldPath = chainStr ? chainStr.slice(1).split('.') : [];\n return { kind: 'field', dictionaryKey: dictKey, fieldPath };\n }\n }\n\n // Field completion inside destructuring: const { | } = useIntlayer(\"key\")\n\n if (/(?:const|let|var)\\s*\\{[^}]*$/.test(textBefore)) {\n const destructMatch = new RegExp(\n `^[^}]*\\\\}\\\\s*=\\\\s*(?:${getCallerNamesAlternation()})(?:<[^<>]*>)?\\\\s*\\\\(\\\\s*['\"\\`]([^'\"\\`\\\\n]+)['\"\\`]`\n ).exec(textAfter);\n\n if (destructMatch) {\n return { kind: 'field', dictionaryKey: destructMatch[1]!, fieldPath: [] };\n }\n }\n\n return null;\n};\n\nconnection.onInitialize((parameters: InitializeParams): InitializeResult => {\n workspaceRoot =\n parameters.rootPath ||\n (parameters.workspaceFolders?.[0]?.uri\n ? decodeURIComponent(new URL(parameters.workspaceFolders[0].uri).pathname)\n : undefined);\n invalidateConfigCaches();\n\n log(`initialized — workspaceRoot: ${workspaceRoot ?? '(none)'}`);\n\n if (parameters.workspaceFolders?.length) {\n log(\n `workspaceFolders: ${parameters.workspaceFolders.map((folder) => folder.name).join(', ')}`\n );\n }\n\n return {\n capabilities: {\n textDocumentSync: TextDocumentSyncKind.Incremental,\n definitionProvider: true,\n referencesProvider: true,\n hoverProvider: true,\n completionProvider: {\n triggerCharacters: ['\"', \"'\", '`', '.'],\n resolveProvider: false,\n },\n },\n };\n});\n\nconnection.onDefinition(async (parameters) => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const position = parameters.position;\n const document = documents.get(uri);\n\n if (!document) {\n log(`onDefinition — document not found in cache: ${uri}`);\n return null;\n }\n\n const text = document.getText();\n const offset = document.offsetAt(position);\n const shortenedUri = uri\n .replace(/^file:\\/\\//, '')\n .split('/')\n .slice(-2)\n .join('/');\n\n log(`onDefinition — ${shortenedUri} ${position.line}:${position.character}`);\n const config = getProjectConfig(absolutePath);\n\n if (config && isContentFile(uri, config)) {\n // Content-file → component navigation is handled exclusively by the VS Code\n // extension's intlayerContentDefinitionProvider (which returns DefinitionLink[]\n // with a proper originSelectionRange). Returning null here prevents the LSP\n // from emitting a duplicate set of Location[] results for the same request.\n log('onDefinition — content file: delegating to extension provider');\n return null;\n }\n\n // Cursor on the key string / function name → navigate to content declaration.\n const key = findKeyAtOffset(text, offset);\n\n if (key) {\n log(\n `onDefinition — useIntlayer key: \"${key}\" → searching content declarations`\n );\n const locations = await getContentDeclarationLocations(key, absolutePath);\n log(\n `onDefinition — found ${locations.length} content declaration(s) for key \"${key}\"`\n );\n return locations.length ? locations : null;\n }\n\n // Cursor on any message usage: destructured property, member access,\n // t('path.to.field') call, formatMessage({ id }), <Trans>, lingui t`…`, …\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (usage && usage.fieldPath.length > 0) {\n log(\n `onDefinition — usage field: \"${usage.dictionaryKey}.${usage.fieldPath.join('.')}\" (${usage.callerName}) → searching content field`\n );\n const locations = await getContentFieldLocations(\n usage.dictionaryKey,\n usage.fieldPath,\n absolutePath\n );\n log(\n `onDefinition — found ${locations.length} content file(s) with field \"${usage.fieldPath.join('.')}\"`\n );\n return locations.length ? locations : null;\n }\n\n if (usage) {\n // Dictionary-level usage (e.g. bare content variable) → content files.\n const locations = await getContentDeclarationLocations(\n usage.dictionaryKey,\n absolutePath\n );\n return locations.length ? locations : null;\n }\n\n log('onDefinition — no Intlayer key at cursor, no result');\n return null;\n});\n\n/**\n * \"Find All References\" handler (Shift+F12).\n *\n * From a content file, returns every source-file location that uses the key or\n * field at the cursor — the reverse direction of Go-to-Definition.\n *\n * From a source file, returns all content-file declarations for the key or\n * field at the cursor — complementing what Go-to-Definition already handles.\n */\nconnection.onReferences(async (parameters: ReferenceParams) => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const position = parameters.position;\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(position);\n const shortenedUri = uri\n .replace(/^file:\\/\\//, '')\n .split('/')\n .slice(-2)\n .join('/');\n\n log(`onReferences — ${shortenedUri} ${position.line}:${position.character}`);\n const config = getProjectConfig(absolutePath);\n\n if (config && isContentFile(uri, config)) {\n // Cursor on `key: \"…\"` → all useIntlayer(key) call sites\n const key = findKeyInContentFile(text, offset);\n\n if (key) {\n const locations = await getKeyUsageLocations(key, absolutePath);\n log(`onReferences — key \"${key}\" → ${locations.length} call site(s)`);\n return locations.length ? locations : null;\n }\n\n // Cursor on a content field → all field usage sites\n const extMatch = uri.match(/\\.[^.]+$/);\n const ext = extMatch ? extMatch[0] : '';\n const fieldInfo = findContentFieldAtOffset(text, offset, ext);\n\n if (fieldInfo) {\n const locations = await getFieldUsageLocations(\n fieldInfo.dictionaryKey,\n fieldInfo.fieldPath,\n absolutePath\n );\n log(\n `onReferences — field \"${fieldInfo.fieldPath.join('.')}\" → ${locations.length} usage(s)`\n );\n return locations.length ? locations : null;\n }\n\n return null;\n }\n\n // Source file: key string → content-file declarations\n const key = findKeyAtOffset(text, offset);\n\n if (key) {\n const locations = await getContentDeclarationLocations(key, absolutePath);\n log(`onReferences — key \"${key}\" → ${locations.length} declaration(s)`);\n return locations.length ? locations : null;\n }\n\n // Source file: field property, t('field') call, JSX message component, …\n // → content-file field definitions\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (usage && usage.fieldPath.length > 0) {\n const locations = await getContentFieldLocations(\n usage.dictionaryKey,\n usage.fieldPath,\n absolutePath\n );\n log(\n `onReferences — field \"${usage.fieldPath.join('.')}\" → ${locations.length} content definition(s)`\n );\n return locations.length ? locations : null;\n }\n\n return null;\n});\n\n// ---------------------------------------------------------------------------\n// Hover\n// ---------------------------------------------------------------------------\n\nconnection.onHover(async (parameters: HoverParams): Promise<Hover | null> => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(parameters.position);\n const config = getProjectConfig(absolutePath);\n\n if (!config) return null;\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const getDicts = (key: string) =>\n Object.values(unmergedDictionaries)\n .flat()\n .filter((d) => d?.key === key);\n\n if (isContentFile(uri, config)) {\n // Cursor on `key: \"...\"` declaration\n const key = findKeyInContentFile(text, offset);\n\n if (key) {\n const dicts = getDicts(key);\n\n if (dicts.length > 0) {\n return {\n contents: {\n kind: MarkupKind.Markdown,\n value: formatDictionaryHover(dicts, key),\n },\n };\n }\n }\n\n // Cursor on a content field\n const ext = uri.match(/\\.[^.]+$/)?.[0] ?? '';\n const fieldInfo = findContentFieldAtOffset(text, offset, ext);\n\n if (fieldInfo) {\n const dicts = getDicts(fieldInfo.dictionaryKey);\n\n if (dicts.length > 0) {\n const hoverText = formatFieldHover(\n dicts,\n fieldInfo.dictionaryKey,\n fieldInfo.fieldPath\n );\n\n if (hoverText) {\n return {\n contents: { kind: MarkupKind.Markdown, value: hoverText },\n };\n }\n }\n }\n\n return null;\n }\n\n // Source file: cursor on key string inside useIntlayer(\"key\")\n const key = findKeyAtOffset(text, offset);\n\n if (key) {\n const dicts = getDicts(key);\n\n if (dicts.length > 0) {\n return {\n contents: {\n kind: MarkupKind.Markdown,\n value: formatDictionaryHover(dicts, key),\n },\n };\n }\n return null;\n }\n\n // Source file: cursor on any message usage (field property, t('field'),\n // formatMessage({ id }), <Trans>, lingui t`…`, …)\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (usage) {\n const dicts = getDicts(usage.dictionaryKey);\n\n if (dicts.length > 0) {\n if (usage.fieldPath.length === 0) {\n return {\n contents: {\n kind: MarkupKind.Markdown,\n value: formatDictionaryHover(dicts, usage.dictionaryKey),\n },\n };\n }\n\n const hoverText = formatFieldHover(\n dicts,\n usage.dictionaryKey,\n usage.fieldPath\n );\n\n if (hoverText) {\n return {\n contents: { kind: MarkupKind.Markdown, value: hoverText },\n };\n }\n }\n }\n\n return null;\n});\n\n// ---------------------------------------------------------------------------\n// Completion\n// ---------------------------------------------------------------------------\n\nconnection.onCompletion(\n async (parameters: CompletionParams): Promise<CompletionItem[] | null> => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(parameters.position);\n const config = getProjectConfig(absolutePath);\n\n if (!config) return null;\n\n const program = parseText(text);\n const context = getCompletionContext(\n text.slice(0, offset),\n text.slice(offset),\n program\n );\n\n if (!context) return null;\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n\n if (context.kind === 'key') {\n const seen = new Set<string>();\n const items: CompletionItem[] = [];\n for (const dictionary of Object.values(unmergedDictionaries).flat()) {\n if (!dictionary?.key || seen.has(dictionary.key)) continue;\n\n seen.add(dictionary.key);\n items.push({\n label: dictionary.key,\n kind: CompletionItemKind.Value,\n detail: dictionary.title ?? undefined,\n documentation: dictionary.description\n ? { kind: MarkupKind.Markdown, value: dictionary.description }\n : undefined,\n });\n }\n log(`onCompletion — key: ${items.length} dictionary keys`);\n return items.length > 0 ? items : null;\n }\n\n if (context.kind === 'field') {\n const dictionaries = Object.values(unmergedDictionaries)\n .flat()\n .filter((dictionary) => dictionary?.key === context.dictionaryKey);\n\n if (dictionaries.length === 0) return null;\n\n const primary = dictionaries[0]!;\n const fields = getFieldsAtPath(primary.content, context.fieldPath);\n const items: CompletionItem[] = fields.map((fieldName) => {\n const value = getFieldByPath(primary.content, [\n ...context.fieldPath,\n fieldName,\n ]);\n return {\n label: fieldName,\n kind: CompletionItemKind.Field,\n detail: formatFieldValue(value),\n };\n });\n log(\n `onCompletion — field \"${context.dictionaryKey}.${context.fieldPath.join('.')}\": ${items.length} field(s)`\n );\n return items.length > 0 ? items : null;\n }\n\n return null;\n }\n);\n\n// ---------------------------------------------------------------------------\n// Push diagnostics\n// ---------------------------------------------------------------------------\n\ndocuments.onDidOpen((event) => {\n sendDiagnosticsForDocument(\n event.document.uri,\n event.document.getText(),\n event.document.version\n );\n});\n\ndocuments.onDidChangeContent((event) => {\n scheduleDiagnostics(\n event.document.uri,\n event.document.getText(),\n event.document.version\n );\n});\n\ndocuments.onDidClose((event) => {\n const uri = event.document.uri;\n const timer = diagnosticTimers.get(uri);\n\n if (timer !== undefined) {\n clearTimeout(timer);\n diagnosticTimers.delete(uri);\n }\n connection.sendDiagnostics({ uri, diagnostics: [] });\n});\n\n// Invalidate the source-file list whenever the client reports a file change.\n// The client registers a watcher for *.content.* files; any creation or\n// deletion means the project layout may have changed.\nconnection.onDidChangeWatchedFiles(() => {\n invalidateConfigCaches();\n log(\n 'watched files changed — source file list and dictionary cache invalidated'\n );\n});\n\n// Make the text document manager listen on the connection\n// for open, change and close text document events\ndocuments.listen(connection);\n\n// Listen on the connection\nconnection.listen();\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../src/index.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { existsSync } from 'node:fs';\nimport { readFile } from 'node:fs/promises';\nimport { dirname, isAbsolute, join } from 'node:path';\nimport { pathToFileURL } from 'node:url';\nimport { getConfiguration } from '@intlayer/config/node';\nimport { buildComponentFilesList } from '@intlayer/engine/utils';\nimport type { IntlayerConfig } from '@intlayer/types/config';\nimport { getUnmergedDictionaries } from '@intlayer/unmerged-dictionaries-entry';\nimport {\n type CompletionItem,\n CompletionItemKind,\n type CompletionParams,\n createConnection,\n type Diagnostic,\n DiagnosticSeverity,\n type Hover,\n type HoverParams,\n type InitializeParams,\n type InitializeResult,\n Location,\n MarkupKind,\n Position,\n ProposedFeatures,\n Range,\n type ReferenceParams,\n TextDocumentSyncKind,\n TextDocuments,\n} from 'vscode-languageserver/node';\nimport { TextDocument } from 'vscode-languageserver-textdocument';\nimport {\n buildCallerWithKeyPattern,\n buildKeyUsagePatterns,\n getCallerNamesAlternation,\n} from './callers/patterns';\nimport {\n formatDictionaryHover,\n formatFieldHover,\n formatFieldValue,\n getFieldByPath,\n getFieldsAtPath,\n} from './dictionaryUtils';\nimport {\n escapeRegularExpression,\n findFieldRangesInFile,\n offsetToRange,\n} from './findFieldInFile';\nimport { findKeyAtOffset } from './findKeyAtOffset';\nimport {\n findContentFieldAtOffset,\n findKeyInContentFile,\n} from './findKeyInContentFile';\nimport { type OxcNode, parseText, walkAst } from './oxcUtils';\nimport {\n collectNamespaceReferences,\n findMessageUsageAtOffset,\n matchCallNamespace,\n} from './usageAnalyzer';\n\n// Create a connection for the server, using Node's IPC as a transport.\n// Also include all preview / proposed LSP features.\nconst connection = createConnection(ProposedFeatures.all);\n\n// Create a simple text document manager.\nconst documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);\n\n/**\n * Send a log line to the client's Output panel (View → Output → \"Intlayer LSP\").\n * Uses connection.console.log so it travels over the JSON-RPC channel and never\n * touches stdout (which is reserved for the LSP protocol).\n */\nconst log = (message: string): void =>\n connection.console.log(`[Intlayer LSP] ${message}`);\n\nlet workspaceRoot: string | null | undefined;\n\ntype WorkspaceConfig = Pick<\n IntlayerConfig,\n 'system' | 'build' | 'content' | 'compiler'\n>;\n\n/**\n * Resolve (and cache) the Intlayer configuration for the current workspace.\n * The LSP process cwd is not the project root, so we must resolve the config\n * from `workspaceRoot` to get the right `system`/`build`/`content` paths.\n */\nconst projectConfigs = new Map<string, WorkspaceConfig>();\n\ntype DictionariesCache = {\n dictionaries: ReturnType<typeof getUnmergedDictionaries>;\n cachedAt: number;\n};\nconst projectDictionaries = new Map<string, DictionariesCache>();\nconst DICTIONARY_CACHE_TTL_MS = 5_000;\n\ntype SourceFilesCache = {\n files: string[];\n cachedAt: number;\n};\nconst projectSourceFiles = new Map<string, SourceFilesCache>();\nconst SOURCE_FILES_CACHE_TTL_MS = 30_000;\n\nconst invalidateConfigCaches = () => {\n projectConfigs.clear();\n projectDictionaries.clear();\n\n projectSourceFiles.clear();\n};\n\nconst getProjectConfig = (absolutePath: string): WorkspaceConfig | null => {\n try {\n const baseDir = dirname(absolutePath);\n // Find the closest intlayer config relative to the file.\n const { system, build, content, compiler } = getConfiguration({ baseDir });\n const cacheKey = system.baseDir;\n\n if (!projectConfigs.has(cacheKey)) {\n projectConfigs.set(cacheKey, { system, build, content, compiler });\n log(\n `getProjectConfig — OK baseDir=${system.baseDir} unmergedDictionariesDir=${system.unmergedDictionariesDir}`\n );\n }\n return projectConfigs.get(cacheKey)!;\n } catch (error) {\n log(`getProjectConfig — FAILED: ${error}`);\n return null;\n }\n};\n\nconst getUnmergedDictionariesCached = (\n config: WorkspaceConfig\n): ReturnType<typeof getUnmergedDictionaries> => {\n const cacheKey = config.system.baseDir;\n const cached = projectDictionaries.get(cacheKey);\n const now = Date.now();\n\n if (cached && now - cached.cachedAt < DICTIONARY_CACHE_TTL_MS) {\n return cached.dictionaries;\n }\n\n const dictionaries = getUnmergedDictionaries(config);\n projectDictionaries.set(cacheKey, { dictionaries, cachedAt: now });\n return dictionaries;\n};\n\n// ---------------------------------------------------------------------------\n// Pattern helpers — derived lazily from the live config.\n// ---------------------------------------------------------------------------\n\nconst isContentFile = (uri: string, config: WorkspaceConfig): boolean => {\n const fileExtensions = config.content.fileExtensions ?? [];\n return fileExtensions.some((extension) => uri.endsWith(extension));\n};\n\n// ---------------------------------------------------------------------------\n// Async I/O helpers\n// ---------------------------------------------------------------------------\n\n/** Read a file as UTF-8, returning null on any error. */\nconst tryReadFile = async (filePath: string): Promise<string | null> => {\n try {\n return await readFile(filePath, 'utf-8');\n } catch {\n return null;\n }\n};\n\n/**\n * Run `callback` over every item with at most `limit` tasks in-flight at once.\n * JS is single-threaded so `results.push` inside callback is race-free.\n */\nconst runConcurrent = async <T>(\n items: T[],\n callback: (item: T) => Promise<void>,\n limit = 50\n): Promise<void> => {\n if (items.length === 0) return;\n let index = 0;\n const worker = async () => {\n while (index < items.length) {\n await callback(items[index++]!);\n }\n };\n await Promise.all(\n Array.from({ length: Math.min(limit, items.length) }, worker)\n );\n};\n\n/** Find the `key: \"<key>\"` line inside a content file so the cursor lands on it. */\nconst getKeyRange = async (\n absolutePath: string,\n key: string\n): Promise<Range> => {\n try {\n const content = await readFile(absolutePath, 'utf-8');\n const lines = content.split(/\\r?\\n/);\n const keyRegularExpression = new RegExp(\n `key\\\\s*:\\\\s*['\"\\`]${key.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}['\"\\`]`\n );\n\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const column = lines[lineIndex]?.search(keyRegularExpression);\n\n if (column !== undefined && column !== -1) {\n return Range.create(\n Position.create(lineIndex, column),\n Position.create(lineIndex, lines[lineIndex]!.length)\n );\n }\n }\n } catch {\n // fall through to the top of the file\n }\n\n return Range.create(Position.create(0, 0), Position.create(0, 0));\n};\n\n/**\n * Return every `.content` file location that declares the given key.\n * A key can be split across several content files (merged dictionaries), so we\n * return one `Location` per source file, pointing at the `key:` declaration.\n */\nconst getContentDeclarationLocations = async (\n key: string,\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config) return [];\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const dictionaries = Object.values(unmergedDictionaries)\n .flat()\n .filter((dictionary) => dictionary?.key === key && dictionary.filePath);\n\n const locations: Location[] = [];\n\n await runConcurrent(dictionaries, async (dictionary) => {\n const filePath = dictionary.filePath!;\n const absolutePath = isAbsolute(filePath)\n ? filePath\n : join(config.system.baseDir, filePath);\n\n if (!existsSync(absolutePath)) return;\n\n locations.push(\n Location.create(\n pathToFileURL(absolutePath).href,\n await getKeyRange(absolutePath, key)\n )\n );\n });\n\n return locations;\n};\n\n/**\n * Return the cached source-file list, re-scanning only when the cache has\n * expired or been explicitly invalidated (config change, watched-file event).\n */\nconst getSourceFiles = async (\n directory: string,\n config: WorkspaceConfig\n): Promise<string[]> => {\n const now = Date.now();\n const cached = projectSourceFiles.get(directory);\n\n if (cached && now - cached.cachedAt < SOURCE_FILES_CACHE_TTL_MS) {\n return cached.files;\n }\n const startTime = Date.now();\n const files = buildComponentFilesList(config as unknown as IntlayerConfig, [\n '**/.output/**',\n '**/.svelte-kit/**',\n '**/src-tauri/**',\n '**/.next/**',\n '**/.nuxt/**',\n '**/.expo/**',\n '**/.vercel/**',\n '**/.turbo/**',\n '**/.tanstack/**',\n '**/dist/**',\n '**/build/**',\n '**/node_modules/**',\n ]);\n projectSourceFiles.set(directory, { files, cachedAt: Date.now() });\n log(\n `source files scanned: ${files.length} file(s) in ${Date.now() - startTime}ms`\n );\n return files;\n};\n\n/**\n * Return every source-file location that calls useIntlayer / getIntlayer with\n * the given key.\n */\nconst getKeyUsageLocations = async (\n key: string,\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config) return [];\n\n const usagePatterns = buildKeyUsagePatterns(key);\n\n const filePaths = await getSourceFiles(config.system.baseDir, config);\n const locations: Location[] = [];\n const startTime = Date.now();\n\n await runConcurrent(filePaths, async (filePath) => {\n const text = await tryReadFile(filePath);\n // Quick string check before running the regexes — skips the vast majority of files\n\n if (!text?.includes(key)) return;\n\n const seenStarts = new Set<number>();\n\n for (const usagePattern of usagePatterns) {\n for (const match of text.matchAll(usagePattern)) {\n const matchStart = match.index!;\n\n if (seenStarts.has(matchStart)) continue;\n\n seenStarts.add(matchStart);\n const matchEnd = matchStart + match[0].length;\n locations.push(\n Location.create(\n pathToFileURL(filePath).href,\n offsetToRange(text, matchStart, matchEnd)\n )\n );\n }\n }\n });\n\n log(\n `getKeyUsageLocations(\"${key}\") — ${filePaths.length} file(s) searched in ${Date.now() - startTime}ms → ${locations.length} hit(s)`\n );\n return locations;\n};\n\n/**\n * Return every source-file location that accesses the field at `fieldPath` on\n * the result of `useIntlayer(dictionaryKey)` / `getIntlayer(dictionaryKey)`.\n *\n * `fieldPath` is an array of property names from the content root to the\n * target field (e.g. `['searchInput', 'text']` for a nested field).\n * Single-element paths (e.g. `['greet']`) behave like the previous\n * single-`fieldName` API.\n */\nconst getFieldUsageLocations = async (\n dictionaryKey: string,\n fieldPath: string[],\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config || fieldPath.length === 0) return [];\n\n const leafFieldName = fieldPath[fieldPath.length - 1]!;\n const escapedKey = escapeRegularExpression(dictionaryKey);\n const escapedLeaf = escapeRegularExpression(leafFieldName);\n const useCallRegularExpression = buildCallerWithKeyPattern(dictionaryKey);\n\n const filePaths = await getSourceFiles(config.system.baseDir, config);\n const locations: Location[] = [];\n const startTime = Date.now();\n\n await runConcurrent(filePaths, async (filePath) => {\n const text = await tryReadFile(filePath);\n\n if (!text) return;\n // Pre-filter: dictionaryKey and every segment of the path must appear in the file\n\n if (!text.includes(dictionaryKey)) return;\n\n if (fieldPath.some((segment) => !text.includes(segment))) return;\n\n if (!useCallRegularExpression.test(text)) return;\n\n for (const range of findFieldRangesInFile(\n text,\n escapedKey,\n leafFieldName,\n escapedLeaf,\n fieldPath\n )) {\n locations.push(Location.create(pathToFileURL(filePath).href, range));\n }\n });\n\n log(\n `getFieldUsageLocations(\"${dictionaryKey}.${fieldPath.join('.')}\") — ${filePaths.length} file(s) searched in ${Date.now() - startTime}ms → ${locations.length} hit(s)`\n );\n return locations;\n};\n\n/**\n * Find the line/column of the field's leaf name as a property key inside a\n * content file. Looks for `fieldName:` or `'field.name':` (flat keys with\n * dots, as used by lingui catalogs), not just any occurrence. Falls back to\n * the top of the file if the field cannot be located.\n */\nconst getFieldRangeInContentFile = async (\n absolutePath: string,\n fieldPath: string[]\n): Promise<Range> => {\n const content = await tryReadFile(absolutePath);\n const fieldName = fieldPath[fieldPath.length - 1];\n\n if (!content || !fieldName)\n return Range.create(Position.create(0, 0), Position.create(0, 0));\n\n // Match `fieldName:` at an object property key position (not a value),\n // allowing an optional quoted form for keys containing dots or dashes.\n const fieldRegularExpression = new RegExp(\n `(?<![.\\\\w])(['\"\\`]?)${escapeRegularExpression(fieldName)}\\\\1\\\\s*:`\n );\n const lines = content.split(/\\r?\\n/);\n\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex]!;\n const matchPosition = line.search(fieldRegularExpression);\n\n if (matchPosition === -1) continue;\n const column = line.indexOf(fieldName, matchPosition);\n\n if (column === -1) continue;\n return Range.create(\n Position.create(lineIndex, column),\n Position.create(lineIndex, column + fieldName.length)\n );\n }\n\n return Range.create(Position.create(0, 0), Position.create(0, 0));\n};\n\n/**\n * Return every content-file location where the field at `fieldPath` is\n * defined as a property of the dictionary with the given `dictionaryKey`.\n */\nconst getContentFieldLocations = async (\n dictionaryKey: string,\n fieldPath: string[],\n absolutePath: string\n): Promise<Location[]> => {\n const config = getProjectConfig(absolutePath);\n\n if (!config) return [];\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const dictionaries = Object.values(unmergedDictionaries)\n .flat()\n .filter(\n (dictionary) => dictionary?.key === dictionaryKey && dictionary.filePath\n );\n\n const locations: Location[] = [];\n\n await runConcurrent(dictionaries, async (dictionary) => {\n const filePath = dictionary.filePath!;\n const absolutePath = isAbsolute(filePath)\n ? filePath\n : join(config.system.baseDir, filePath);\n\n if (!existsSync(absolutePath)) return;\n\n locations.push(\n Location.create(\n pathToFileURL(absolutePath).href,\n await getFieldRangeInContentFile(absolutePath, fieldPath)\n )\n );\n });\n\n return locations;\n};\n\n// ---------------------------------------------------------------------------\n// Diagnostics — push-based (no capability needed)\n// ---------------------------------------------------------------------------\n\nconst DIAGNOSTIC_DEBOUNCE_MS = 300;\nconst diagnosticTimers = new Map<string, ReturnType<typeof setTimeout>>();\n\nconst computeDiagnostics = (\n text: string,\n config: WorkspaceConfig\n): Diagnostic[] => {\n const namespaceReferences = collectNamespaceReferences(text);\n\n if (namespaceReferences.length === 0) return [];\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const knownKeys = new Set(\n Object.values(unmergedDictionaries)\n .flat()\n .map((dictionary) => dictionary?.key)\n .filter((key): key is string => typeof key === 'string')\n );\n\n const diagnostics: Diagnostic[] = [];\n\n for (const reference of namespaceReferences) {\n if (knownKeys.has(reference.dictionaryKey)) continue;\n\n diagnostics.push({\n range: offsetToRange(\n text,\n reference.namespaceStart,\n reference.namespaceEnd\n ),\n severity: DiagnosticSeverity.Warning,\n source: 'intlayer',\n message: `Dictionary key \"${reference.dictionaryKey}\" is not declared in any content file`,\n });\n }\n\n return diagnostics;\n};\n\nconst sendDiagnosticsForDocument = (\n uri: string,\n text: string,\n version: number | null\n): void => {\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const config = getProjectConfig(absolutePath);\n\n const diagnostics =\n config && !isContentFile(uri, config)\n ? computeDiagnostics(text, config)\n : [];\n\n connection.sendDiagnostics({\n uri,\n version: version ?? undefined,\n diagnostics,\n });\n};\n\nconst scheduleDiagnostics = (\n uri: string,\n text: string,\n version: number | null\n): void => {\n const existing = diagnosticTimers.get(uri);\n\n if (existing !== undefined) clearTimeout(existing);\n\n diagnosticTimers.set(\n uri,\n setTimeout(() => {\n diagnosticTimers.delete(uri);\n sendDiagnosticsForDocument(uri, text, version);\n }, DIAGNOSTIC_DEBOUNCE_MS)\n );\n};\n\n// ---------------------------------------------------------------------------\n// Completion context detection\n// ---------------------------------------------------------------------------\n\ntype CompletionCtx =\n | { kind: 'key' }\n | { kind: 'field'; dictionaryKey: string; fieldPath: string[] }\n | null;\n\n/** Find the dictionary key for a simple variable assigned from any registered caller. */\nconst resolveVariableToDictionaryKey = (\n program: OxcNode,\n varName: string\n): string | null => {\n let result: string | null = null;\n\n walkAst(program, (node) => {\n if (result) return true;\n\n if (node['type'] !== 'VariableDeclarator') return;\n const id = node['id'] as OxcNode | undefined;\n\n if (id?.['type'] !== 'Identifier' || (id['name'] as string) !== varName)\n return;\n const init = node['init'] as OxcNode | undefined;\n\n if (!init) return;\n result = matchCallNamespace(init);\n return true;\n });\n return result;\n};\n\n/**\n * Detect what kind of completion is appropriate at the given offset.\n *\n * Handles:\n * - key completion: cursor inside first string arg of useIntlayer/getIntlayer\n * - field (member): cursor after `variable.` where variable came from useIntlayer\n * - field (destructure): cursor inside `const { | } = useIntlayer(\"key\")`\n */\nconst getCompletionContext = (\n textBefore: string,\n textAfter: string,\n program: OxcNode | null\n): CompletionCtx => {\n // Key completion\n\n if (\n new RegExp(\n `\\\\b(?:${getCallerNamesAlternation()})\\\\b(?:<[^<>]*>)?\\\\s*\\\\(\\\\s*['\"\\`][^'\"\\`\\\\n]*$`\n ).test(textBefore)\n ) {\n return { kind: 'key' };\n }\n\n // Field completion via member access: rootVar.path.to.\n const memberMatch = /\\b(\\w+)((?:\\.\\w+)*)\\.$/.exec(textBefore);\n\n if (memberMatch && program) {\n const rootVar = memberMatch[1]!;\n const chainStr = memberMatch[2]!;\n const dictKey = resolveVariableToDictionaryKey(program, rootVar);\n\n if (dictKey) {\n const fieldPath = chainStr ? chainStr.slice(1).split('.') : [];\n return { kind: 'field', dictionaryKey: dictKey, fieldPath };\n }\n }\n\n // Field completion inside destructuring: const { | } = useIntlayer(\"key\")\n\n if (/(?:const|let|var)\\s*\\{[^}]*$/.test(textBefore)) {\n const destructMatch = new RegExp(\n `^[^}]*\\\\}\\\\s*=\\\\s*(?:${getCallerNamesAlternation()})(?:<[^<>]*>)?\\\\s*\\\\(\\\\s*['\"\\`]([^'\"\\`\\\\n]+)['\"\\`]`\n ).exec(textAfter);\n\n if (destructMatch) {\n return { kind: 'field', dictionaryKey: destructMatch[1]!, fieldPath: [] };\n }\n }\n\n return null;\n};\n\nconnection.onInitialize((parameters: InitializeParams): InitializeResult => {\n workspaceRoot =\n parameters.rootPath ||\n (parameters.workspaceFolders?.[0]?.uri\n ? decodeURIComponent(new URL(parameters.workspaceFolders[0].uri).pathname)\n : undefined);\n invalidateConfigCaches();\n\n log(`initialized — workspaceRoot: ${workspaceRoot ?? '(none)'}`);\n\n if (parameters.workspaceFolders?.length) {\n log(\n `workspaceFolders: ${parameters.workspaceFolders.map((folder) => folder.name).join(', ')}`\n );\n }\n\n return {\n capabilities: {\n textDocumentSync: TextDocumentSyncKind.Incremental,\n definitionProvider: true,\n referencesProvider: true,\n hoverProvider: true,\n completionProvider: {\n triggerCharacters: ['\"', \"'\", '`', '.'],\n resolveProvider: false,\n },\n },\n };\n});\n\nconnection.onDefinition(async (parameters) => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const position = parameters.position;\n const document = documents.get(uri);\n\n if (!document) {\n log(`onDefinition — document not found in cache: ${uri}`);\n return null;\n }\n\n const text = document.getText();\n const offset = document.offsetAt(position);\n const shortenedUri = uri\n .replace(/^file:\\/\\//, '')\n .split('/')\n .slice(-2)\n .join('/');\n\n log(`onDefinition — ${shortenedUri} ${position.line}:${position.character}`);\n const config = getProjectConfig(absolutePath);\n\n if (config && isContentFile(uri, config)) {\n // Content-file → component navigation is handled exclusively by the VS Code\n // extension's intlayerContentDefinitionProvider (which returns DefinitionLink[]\n // with a proper originSelectionRange). Returning null here prevents the LSP\n // from emitting a duplicate set of Location[] results for the same request.\n log('onDefinition — content file: delegating to extension provider');\n return null;\n }\n\n // Cursor on the key string / function name → navigate to content declaration.\n const key = findKeyAtOffset(text, offset);\n\n if (key) {\n log(\n `onDefinition — useIntlayer key: \"${key}\" → searching content declarations`\n );\n const locations = await getContentDeclarationLocations(key, absolutePath);\n log(\n `onDefinition — found ${locations.length} content declaration(s) for key \"${key}\"`\n );\n return locations.length ? locations : null;\n }\n\n // Cursor on any message usage: destructured property, member access,\n // t('path.to.field') call, formatMessage({ id }), <Trans>, lingui t`…`, …\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (usage && usage.fieldPath.length > 0) {\n log(\n `onDefinition — usage field: \"${usage.dictionaryKey}.${usage.fieldPath.join('.')}\" (${usage.callerName}) → searching content field`\n );\n const locations = await getContentFieldLocations(\n usage.dictionaryKey,\n usage.fieldPath,\n absolutePath\n );\n log(\n `onDefinition — found ${locations.length} content file(s) with field \"${usage.fieldPath.join('.')}\"`\n );\n return locations.length ? locations : null;\n }\n\n if (usage) {\n // Dictionary-level usage (e.g. bare content variable) → content files.\n const locations = await getContentDeclarationLocations(\n usage.dictionaryKey,\n absolutePath\n );\n return locations.length ? locations : null;\n }\n\n log('onDefinition — no Intlayer key at cursor, no result');\n return null;\n});\n\n/**\n * \"Find All References\" handler (Shift+F12).\n *\n * From a content file, returns every source-file location that uses the key or\n * field at the cursor — the reverse direction of Go-to-Definition.\n *\n * From a source file, returns all content-file declarations for the key or\n * field at the cursor — complementing what Go-to-Definition already handles.\n */\nconnection.onReferences(async (parameters: ReferenceParams) => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const position = parameters.position;\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(position);\n const shortenedUri = uri\n .replace(/^file:\\/\\//, '')\n .split('/')\n .slice(-2)\n .join('/');\n\n log(`onReferences — ${shortenedUri} ${position.line}:${position.character}`);\n const config = getProjectConfig(absolutePath);\n\n if (config && isContentFile(uri, config)) {\n // Cursor on `key: \"…\"` → all useIntlayer(key) call sites\n const key = findKeyInContentFile(text, offset);\n\n if (key) {\n const locations = await getKeyUsageLocations(key, absolutePath);\n log(`onReferences — key \"${key}\" → ${locations.length} call site(s)`);\n return locations.length ? locations : null;\n }\n\n // Cursor on a content field → all field usage sites\n const extMatch = uri.match(/\\.[^.]+$/);\n const ext = extMatch ? extMatch[0] : '';\n const fieldInfo = findContentFieldAtOffset(text, offset, ext);\n\n if (fieldInfo) {\n const locations = await getFieldUsageLocations(\n fieldInfo.dictionaryKey,\n fieldInfo.fieldPath,\n absolutePath\n );\n log(\n `onReferences — field \"${fieldInfo.fieldPath.join('.')}\" → ${locations.length} usage(s)`\n );\n return locations.length ? locations : null;\n }\n\n return null;\n }\n\n // Source file: key string → content-file declarations\n const key = findKeyAtOffset(text, offset);\n\n if (key) {\n const locations = await getContentDeclarationLocations(key, absolutePath);\n log(`onReferences — key \"${key}\" → ${locations.length} declaration(s)`);\n return locations.length ? locations : null;\n }\n\n // Source file: field property, t('field') call, JSX message component, …\n // → content-file field definitions\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (usage && usage.fieldPath.length > 0) {\n const locations = await getContentFieldLocations(\n usage.dictionaryKey,\n usage.fieldPath,\n absolutePath\n );\n log(\n `onReferences — field \"${usage.fieldPath.join('.')}\" → ${locations.length} content definition(s)`\n );\n return locations.length ? locations : null;\n }\n\n return null;\n});\n\n// ---------------------------------------------------------------------------\n// Hover\n// ---------------------------------------------------------------------------\n\nconnection.onHover(async (parameters: HoverParams): Promise<Hover | null> => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(parameters.position);\n const config = getProjectConfig(absolutePath);\n\n if (!config) return null;\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n const getDicts = (key: string) =>\n Object.values(unmergedDictionaries)\n .flat()\n .filter((d) => d?.key === key);\n\n if (isContentFile(uri, config)) {\n // Cursor on `key: \"...\"` declaration\n const key = findKeyInContentFile(text, offset);\n\n if (key) {\n const dicts = getDicts(key);\n\n if (dicts.length > 0) {\n return {\n contents: {\n kind: MarkupKind.Markdown,\n value: formatDictionaryHover(dicts, key),\n },\n };\n }\n }\n\n // Cursor on a content field\n const ext = uri.match(/\\.[^.]+$/)?.[0] ?? '';\n const fieldInfo = findContentFieldAtOffset(text, offset, ext);\n\n if (fieldInfo) {\n const dicts = getDicts(fieldInfo.dictionaryKey);\n\n if (dicts.length > 0) {\n const hoverText = formatFieldHover(\n dicts,\n fieldInfo.dictionaryKey,\n fieldInfo.fieldPath\n );\n\n if (hoverText) {\n return {\n contents: { kind: MarkupKind.Markdown, value: hoverText },\n };\n }\n }\n }\n\n return null;\n }\n\n // Source file: cursor on key string inside useIntlayer(\"key\")\n const key = findKeyAtOffset(text, offset);\n\n if (key) {\n const dicts = getDicts(key);\n\n if (dicts.length > 0) {\n return {\n contents: {\n kind: MarkupKind.Markdown,\n value: formatDictionaryHover(dicts, key),\n },\n };\n }\n return null;\n }\n\n // Source file: cursor on any message usage (field property, t('field'),\n // formatMessage({ id }), <Trans>, lingui t`…`, …)\n const usage = findMessageUsageAtOffset(text, offset);\n\n if (usage) {\n const dicts = getDicts(usage.dictionaryKey);\n\n if (dicts.length > 0) {\n if (usage.fieldPath.length === 0) {\n return {\n contents: {\n kind: MarkupKind.Markdown,\n value: formatDictionaryHover(dicts, usage.dictionaryKey),\n },\n };\n }\n\n const hoverText = formatFieldHover(\n dicts,\n usage.dictionaryKey,\n usage.fieldPath\n );\n\n if (hoverText) {\n return {\n contents: { kind: MarkupKind.Markdown, value: hoverText },\n };\n }\n }\n }\n\n return null;\n});\n\n// ---------------------------------------------------------------------------\n// Completion\n// ---------------------------------------------------------------------------\n\nconnection.onCompletion(\n async (parameters: CompletionParams): Promise<CompletionItem[] | null> => {\n const uri = parameters.textDocument.uri;\n const absolutePath = decodeURIComponent(new URL(uri).pathname);\n const document = documents.get(uri);\n\n if (!document) return null;\n\n const text = document.getText();\n const offset = document.offsetAt(parameters.position);\n const config = getProjectConfig(absolutePath);\n\n if (!config) return null;\n\n const program = parseText(text);\n const context = getCompletionContext(\n text.slice(0, offset),\n text.slice(offset),\n program\n );\n\n if (!context) return null;\n\n const unmergedDictionaries = getUnmergedDictionariesCached(config);\n\n if (context.kind === 'key') {\n const seen = new Set<string>();\n const items: CompletionItem[] = [];\n for (const dictionary of Object.values(unmergedDictionaries).flat()) {\n if (!dictionary?.key || seen.has(dictionary.key)) continue;\n\n seen.add(dictionary.key);\n items.push({\n label: dictionary.key,\n kind: CompletionItemKind.Value,\n detail: dictionary.title ?? undefined,\n documentation: dictionary.description\n ? { kind: MarkupKind.Markdown, value: dictionary.description }\n : undefined,\n });\n }\n log(`onCompletion — key: ${items.length} dictionary keys`);\n return items.length > 0 ? items : null;\n }\n\n if (context.kind === 'field') {\n const dictionaries = Object.values(unmergedDictionaries)\n .flat()\n .filter((dictionary) => dictionary?.key === context.dictionaryKey);\n\n if (dictionaries.length === 0) return null;\n\n const primary = dictionaries[0]!;\n const fields = getFieldsAtPath(primary.content, context.fieldPath);\n const items: CompletionItem[] = fields.map((fieldName) => {\n const value = getFieldByPath(primary.content, [\n ...context.fieldPath,\n fieldName,\n ]);\n return {\n label: fieldName,\n kind: CompletionItemKind.Field,\n detail: formatFieldValue(value),\n };\n });\n log(\n `onCompletion — field \"${context.dictionaryKey}.${context.fieldPath.join('.')}\": ${items.length} field(s)`\n );\n return items.length > 0 ? items : null;\n }\n\n return null;\n }\n);\n\n// ---------------------------------------------------------------------------\n// Push diagnostics\n// ---------------------------------------------------------------------------\n\ndocuments.onDidOpen((event) => {\n sendDiagnosticsForDocument(\n event.document.uri,\n event.document.getText(),\n event.document.version\n );\n});\n\ndocuments.onDidChangeContent((event) => {\n scheduleDiagnostics(\n event.document.uri,\n event.document.getText(),\n event.document.version\n );\n});\n\ndocuments.onDidClose((event) => {\n const uri = event.document.uri;\n const timer = diagnosticTimers.get(uri);\n\n if (timer !== undefined) {\n clearTimeout(timer);\n diagnosticTimers.delete(uri);\n }\n connection.sendDiagnostics({ uri, diagnostics: [] });\n});\n\n// Invalidate the source-file list whenever the client reports a file change.\n// The client registers a watcher for *.content.* files; any creation or\n// deletion means the project layout may have changed.\nconnection.onDidChangeWatchedFiles(() => {\n invalidateConfigCaches();\n log(\n 'watched files changed — source file list and dictionary cache invalidated'\n );\n});\n\n// Make the text document manager listen on the connection\n// for open, change and close text document events\ndocuments.listen(connection);\n\n// Listen on the connection\nconnection.listen();\n"],"mappings":"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"}
|