@intlayer/lsp 9.5.11 → 9.6.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.
@@ -0,0 +1,39 @@
1
+ import { COMPAT_CALLERS } from "@intlayer/config/callers";
2
+
3
+ //#region src/callers/editorCallers.ts
4
+ /**
5
+ * Every module a compat adapter is imported from (original library names and
6
+ * their `@intlayer/*` equivalents), derived from the shared registry so a new
7
+ * compat library is covered without touching this file.
8
+ */
9
+ const COMPAT_IMPORT_SOURCES = [...new Set(COMPAT_CALLERS.flatMap((descriptor) => descriptor.importSources))];
10
+ /**
11
+ * Callers recognised by the editor tooling only (go-to-definition, hover,
12
+ * references) — deliberately kept out of the shared registry in
13
+ * `@intlayer/config/callers`, because the babel/swc optimize and purge passes
14
+ * would otherwise start binding them.
15
+ *
16
+ * Their message ids carry no namespace, so the dictionary is the first id
17
+ * segment; `resolveDictionaryTarget` then falls back to the whole-file catalog
18
+ * (`index`) when that segment names no dictionary.
19
+ */
20
+ const EDITOR_CALLERS = [{
21
+ callerName: "t",
22
+ library: "i18next",
23
+ importSources: COMPAT_IMPORT_SOURCES,
24
+ requiresImport: true,
25
+ matchAsMethod: true,
26
+ namespaceSources: [{ from: "path-first-segment" }],
27
+ translationFunction: "self"
28
+ }, {
29
+ callerName: "$t",
30
+ library: "vue-i18n",
31
+ importSources: ["vue-i18n", "@intlayer/vue-i18n"],
32
+ matchAsMethod: true,
33
+ namespaceSources: [{ from: "path-first-segment" }],
34
+ translationFunction: "self"
35
+ }];
36
+
37
+ //#endregion
38
+ export { EDITOR_CALLERS };
39
+ //# sourceMappingURL=editorCallers.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editorCallers.mjs","names":[],"sources":["../../../src/callers/editorCallers.ts"],"sourcesContent":["import {\n type CallerDescriptor,\n COMPAT_CALLERS,\n} from '@intlayer/config/callers';\n\n/**\n * Every module a compat adapter is imported from (original library names and\n * their `@intlayer/*` equivalents), derived from the shared registry so a new\n * compat library is covered without touching this file.\n */\nconst COMPAT_IMPORT_SOURCES = [\n ...new Set(COMPAT_CALLERS.flatMap((descriptor) => descriptor.importSources)),\n];\n\n/**\n * Callers recognised by the editor tooling only (go-to-definition, hover,\n * references) — deliberately kept out of the shared registry in\n * `@intlayer/config/callers`, because the babel/swc optimize and purge passes\n * would otherwise start binding them.\n *\n * Their message ids carry no namespace, so the dictionary is the first id\n * segment; `resolveDictionaryTarget` then falls back to the whole-file catalog\n * (`index`) when that segment names no dictionary.\n */\nexport const EDITOR_CALLERS: CallerDescriptor[] = [\n {\n // `i18n.t('home.title')`, `i18next.t(…)`, `i18n.global.t(…)`, or a bare\n // `t` in a file importing any compat library. A `t` bound by\n // `useTranslation()` / `useTranslations()` resolves as a translator\n // binding first, and a library's own `t` caller wins over this one.\n callerName: 't',\n library: 'i18next',\n importSources: COMPAT_IMPORT_SOURCES,\n requiresImport: true,\n matchAsMethod: true,\n namespaceSources: [{ from: 'path-first-segment' }],\n translationFunction: 'self',\n },\n {\n // vue-i18n / @nuxtjs/i18n global `$t('home.title')` / `this.$t(…)` —\n // templates never import it, so no import is required.\n callerName: '$t',\n library: 'vue-i18n',\n importSources: ['vue-i18n', '@intlayer/vue-i18n'],\n matchAsMethod: true,\n namespaceSources: [{ from: 'path-first-segment' }],\n translationFunction: 'self',\n },\n];\n"],"mappings":";;;;;;;;AAUA,MAAM,wBAAwB,CAC5B,GAAG,IAAI,IAAI,eAAe,SAAS,eAAe,WAAW,aAAa,CAAC,CAC7E;;;;;;;;;;;AAYA,MAAa,iBAAqC,CAChD;CAKE,YAAY;CACZ,SAAS;CACT,eAAe;CACf,gBAAgB;CAChB,eAAe;CACf,kBAAkB,CAAC,EAAE,MAAM,qBAAqB,CAAC;CACjD,qBAAqB;AACvB,GACA;CAGE,YAAY;CACZ,SAAS;CACT,eAAe,CAAC,YAAY,oBAAoB;CAChD,eAAe;CACf,kBAAkB,CAAC,EAAE,MAAM,qBAAqB,CAAC;CACjD,qBAAqB;AACvB,CACF"}
@@ -0,0 +1,152 @@
1
+ import { getPropertyKeyName, nodeEnd, nodeStart, parseText } from "./oxcUtils.mjs";
2
+ import { escapeRegularExpression } from "./findFieldInFile.mjs";
3
+ import { extname } from "node:path";
4
+
5
+ //#region src/findFieldDeclaration.ts
6
+ const JSON_EXTENSIONS = /* @__PURE__ */ new Set([
7
+ ".json",
8
+ ".jsonc",
9
+ ".json5"
10
+ ]);
11
+ const SCRIPT_EXTENSIONS = /* @__PURE__ */ new Set([
12
+ ".ts",
13
+ ".tsx",
14
+ ".js",
15
+ ".jsx",
16
+ ".mjs",
17
+ ".cjs",
18
+ ".mts",
19
+ ".cts"
20
+ ]);
21
+ /** Strip `x as T`, `x satisfies T` and `(x)` wrappers. */
22
+ const unwrapExpression = (node) => {
23
+ let current = node;
24
+ while (current?.["type"] === "TSAsExpression" || current?.["type"] === "TSSatisfiesExpression" || current?.["type"] === "ParenthesizedExpression") current = current["expression"];
25
+ return current;
26
+ };
27
+ /**
28
+ * The object literal properties are read from: the node itself, or the first
29
+ * object argument of a wrapping call (`t({ … })`, `enu({ … })`).
30
+ */
31
+ const getObjectLiteral = (node) => {
32
+ const expression = unwrapExpression(node);
33
+ if (expression?.["type"] === "ObjectExpression") return expression;
34
+ if (expression?.["type"] === "CallExpression") {
35
+ const firstArgument = unwrapExpression(expression["arguments"]?.[0]);
36
+ if (firstArgument?.["type"] === "ObjectExpression") return firstArgument;
37
+ }
38
+ };
39
+ const findPropertyByName = (objectNode, propertyName) => (objectNode["properties"] ?? []).find((property) => (property["type"] === "Property" || property["type"] === "ObjectProperty") && getPropertyKeyName(property["key"]) === propertyName);
40
+ /**
41
+ * Deepest property reached by walking `segments` through nested object
42
+ * literals, where one property may consume several segments when its name is
43
+ * their dotted join (flat `'profile.title'` keys).
44
+ */
45
+ const findDeepestProperty = (objectNode, segments, consumedSegmentCount = 0) => {
46
+ let best = null;
47
+ const remaining = segments.length - consumedSegmentCount;
48
+ for (let length = 1; length <= remaining; length++) {
49
+ const propertyName = segments.slice(consumedSegmentCount, consumedSegmentCount + length).join(".");
50
+ const property = findPropertyByName(objectNode, propertyName);
51
+ if (!property) continue;
52
+ const matchedSegmentCount = consumedSegmentCount + length;
53
+ let match = {
54
+ keyNode: property["key"],
55
+ matchedSegmentCount
56
+ };
57
+ const childObject = getObjectLiteral(property["value"]);
58
+ if (matchedSegmentCount < segments.length && childObject) match = findDeepestProperty(childObject, segments, matchedSegmentCount) ?? match;
59
+ if (match.matchedSegmentCount === segments.length) return match;
60
+ if (!best || match.matchedSegmentCount > best.matchedSegmentCount) best = match;
61
+ }
62
+ return best;
63
+ };
64
+ /** The root object literal of a JSON document. */
65
+ const getJsonRootObject = (text) => {
66
+ const statement = (parseText(`(${text}\n)`)?.["body"])?.[0];
67
+ const rootObject = getObjectLiteral(statement?.["expression"]);
68
+ return rootObject ? {
69
+ rootObject,
70
+ offsetShift: 1
71
+ } : null;
72
+ };
73
+ /** The object literal a content declaration file exports. */
74
+ const getContentFileRootObject = (text) => {
75
+ const body = parseText(text)?.["body"] ?? [];
76
+ const exportDefault = body.find((statement) => statement["type"] === "ExportDefaultDeclaration");
77
+ let declaration = unwrapExpression(exportDefault?.["declaration"]);
78
+ if (declaration?.["type"] === "Identifier") {
79
+ const variableName = declaration["name"];
80
+ for (const statement of body) {
81
+ const variableDeclaration = statement["type"] === "ExportNamedDeclaration" ? statement["declaration"] : statement;
82
+ if (variableDeclaration?.["type"] !== "VariableDeclaration") continue;
83
+ const declarator = (variableDeclaration["declarations"] ?? []).find((candidate) => candidate["id"]?.["name"] === variableName);
84
+ if (declarator) {
85
+ declaration = unwrapExpression(declarator["init"]);
86
+ break;
87
+ }
88
+ }
89
+ }
90
+ return getObjectLiteral(declaration) ?? null;
91
+ };
92
+ /**
93
+ * Regex fallback for formats with no AST (YAML, Markdown frontmatter): the
94
+ * leaf field name written as a property key.
95
+ */
96
+ const findLeafPropertyByText = (text, fieldPath) => {
97
+ const leafName = fieldPath[fieldPath.length - 1];
98
+ if (!leafName) return null;
99
+ const match = new RegExp(`(?<![.\\w])(['"\`]?)${escapeRegularExpression(leafName)}\\1\\s*:`, "m").exec(text);
100
+ if (!match) return null;
101
+ const start = match.index + match[1].length;
102
+ return {
103
+ start,
104
+ end: start + leafName.length
105
+ };
106
+ };
107
+ const toSpan = (match, offsetShift) => ({
108
+ start: nodeStart(match.keyNode) - offsetShift,
109
+ end: nodeEnd(match.keyNode) - offsetShift
110
+ });
111
+ /**
112
+ * Locate where a dictionary field is declared in its source file.
113
+ *
114
+ * - JSON catalogs may hold the dictionary at their root or under a property
115
+ * named after it (one file splitting into several dictionaries); both are
116
+ * tried, with nested and flat dotted keys.
117
+ * - Content declaration files (`.content.ts`) are walked from the exported
118
+ * object's `content` property.
119
+ * - Other formats fall back to a text search on the leaf name.
120
+ *
121
+ * With an empty `fieldPath`, the dictionary itself is located: its namespace
122
+ * property in a JSON catalog, or the `key` property of a content file.
123
+ *
124
+ * @returns The span of the property key, or `null` when not found.
125
+ */
126
+ const findFieldDeclaration = (text, filePath, dictionaryKey, fieldPath) => {
127
+ const extension = extname(filePath).toLowerCase();
128
+ const segments = fieldPath.flatMap((propertyName) => propertyName.split("."));
129
+ if (JSON_EXTENSIONS.has(extension)) {
130
+ const json = getJsonRootObject(text);
131
+ if (!json) return findLeafPropertyByText(text, fieldPath);
132
+ const underNamespace = findDeepestProperty(json.rootObject, [dictionaryKey, ...segments]);
133
+ const atRoot = segments.length > 0 ? findDeepestProperty(json.rootObject, segments) : null;
134
+ const isComplete = (match, segmentCount) => match?.matchedSegmentCount === segmentCount;
135
+ if (isComplete(underNamespace, segments.length + 1)) return toSpan(underNamespace, json.offsetShift);
136
+ if (isComplete(atRoot, segments.length)) return toSpan(atRoot, json.offsetShift);
137
+ const partial = underNamespace ?? atRoot;
138
+ return partial ? toSpan(partial, json.offsetShift) : null;
139
+ }
140
+ if (SCRIPT_EXTENSIONS.has(extension)) {
141
+ const rootObject = getContentFileRootObject(text);
142
+ if (rootObject) {
143
+ const match = findDeepestProperty(rootObject, segments.length > 0 ? ["content", ...segments] : ["key"]);
144
+ if (match) return toSpan(match, 0);
145
+ }
146
+ }
147
+ return findLeafPropertyByText(text, fieldPath);
148
+ };
149
+
150
+ //#endregion
151
+ export { findFieldDeclaration };
152
+ //# sourceMappingURL=findFieldDeclaration.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"findFieldDeclaration.mjs","names":[],"sources":["../../src/findFieldDeclaration.ts"],"sourcesContent":["import { extname } from 'node:path';\nimport { escapeRegularExpression } from './findFieldInFile';\nimport {\n getPropertyKeyName,\n nodeEnd,\n nodeStart,\n type OxcNode,\n parseText,\n} from './oxcUtils';\n\n/** Offsets of a declaration inside a file's text. */\nexport type OffsetSpan = { start: number; end: number };\n\nconst JSON_EXTENSIONS = new Set(['.json', '.jsonc', '.json5']);\nconst SCRIPT_EXTENSIONS = new Set([\n '.ts',\n '.tsx',\n '.js',\n '.jsx',\n '.mjs',\n '.cjs',\n '.mts',\n '.cts',\n]);\n\n/** A property matched while walking an object literal. */\ntype PropertyMatch = {\n keyNode: OxcNode;\n /** Number of path segments consumed up to this property. */\n matchedSegmentCount: number;\n};\n\n/** Strip `x as T`, `x satisfies T` and `(x)` wrappers. */\nconst unwrapExpression = (node: OxcNode | undefined): OxcNode | undefined => {\n let current = node;\n\n while (\n current?.['type'] === 'TSAsExpression' ||\n current?.['type'] === 'TSSatisfiesExpression' ||\n current?.['type'] === 'ParenthesizedExpression'\n ) {\n current = current['expression'] as OxcNode | undefined;\n }\n\n return current;\n};\n\n/**\n * The object literal properties are read from: the node itself, or the first\n * object argument of a wrapping call (`t({ … })`, `enu({ … })`).\n */\nconst getObjectLiteral = (node: OxcNode | undefined): OxcNode | undefined => {\n const expression = unwrapExpression(node);\n\n if (expression?.['type'] === 'ObjectExpression') return expression;\n\n if (expression?.['type'] === 'CallExpression') {\n const firstArgument = unwrapExpression(\n (expression['arguments'] as OxcNode[] | undefined)?.[0]\n );\n\n if (firstArgument?.['type'] === 'ObjectExpression') return firstArgument;\n }\n\n return undefined;\n};\n\nconst findPropertyByName = (\n objectNode: OxcNode,\n propertyName: string\n): OxcNode | undefined =>\n ((objectNode['properties'] as OxcNode[] | undefined) ?? []).find(\n (property) =>\n (property['type'] === 'Property' ||\n property['type'] === 'ObjectProperty') &&\n getPropertyKeyName(property['key'] as OxcNode | undefined) ===\n propertyName\n );\n\n/**\n * Deepest property reached by walking `segments` through nested object\n * literals, where one property may consume several segments when its name is\n * their dotted join (flat `'profile.title'` keys).\n */\nconst findDeepestProperty = (\n objectNode: OxcNode,\n segments: string[],\n consumedSegmentCount = 0\n): PropertyMatch | null => {\n let best: PropertyMatch | null = null;\n const remaining = segments.length - consumedSegmentCount;\n\n for (let length = 1; length <= remaining; length++) {\n const propertyName = segments\n .slice(consumedSegmentCount, consumedSegmentCount + length)\n .join('.');\n const property = findPropertyByName(objectNode, propertyName);\n\n if (!property) continue;\n\n const matchedSegmentCount = consumedSegmentCount + length;\n let match: PropertyMatch = {\n keyNode: property['key'] as OxcNode,\n matchedSegmentCount,\n };\n\n const childObject = getObjectLiteral(property['value'] as OxcNode);\n\n if (matchedSegmentCount < segments.length && childObject) {\n match =\n findDeepestProperty(childObject, segments, matchedSegmentCount) ??\n match;\n }\n\n if (match.matchedSegmentCount === segments.length) return match;\n\n if (!best || match.matchedSegmentCount > best.matchedSegmentCount) {\n best = match;\n }\n }\n\n return best;\n};\n\n/** The root object literal of a JSON document. */\nconst getJsonRootObject = (\n text: string\n): { rootObject: OxcNode; offsetShift: number } | null => {\n // Parsed as a parenthesised expression so a bare `{…}` is not read as a\n // block statement; every offset is then shifted by the added `(`.\n const program = parseText(`(${text}\\n)`);\n const statement = (program?.['body'] as OxcNode[] | undefined)?.[0];\n const rootObject = getObjectLiteral(\n statement?.['expression'] as OxcNode | undefined\n );\n\n return rootObject ? { rootObject, offsetShift: 1 } : null;\n};\n\n/** The object literal a content declaration file exports. */\nconst getContentFileRootObject = (text: string): OxcNode | null => {\n const program = parseText(text);\n const body = (program?.['body'] as OxcNode[] | undefined) ?? [];\n\n const exportDefault = body.find(\n (statement) => statement['type'] === 'ExportDefaultDeclaration'\n );\n let declaration = unwrapExpression(\n exportDefault?.['declaration'] as OxcNode | undefined\n );\n\n // `const content = { … } satisfies Dictionary; export default content;`\n if (declaration?.['type'] === 'Identifier') {\n const variableName = declaration['name'] as string;\n\n for (const statement of body) {\n const variableDeclaration =\n statement['type'] === 'ExportNamedDeclaration'\n ? (statement['declaration'] as OxcNode | undefined)\n : statement;\n\n if (variableDeclaration?.['type'] !== 'VariableDeclaration') continue;\n\n const declarator = (\n (variableDeclaration['declarations'] as OxcNode[]) ?? []\n ).find(\n (candidate) =>\n (candidate['id'] as OxcNode | undefined)?.['name'] === variableName\n );\n\n if (declarator) {\n declaration = unwrapExpression(\n declarator['init'] as OxcNode | undefined\n );\n break;\n }\n }\n }\n\n return getObjectLiteral(declaration) ?? null;\n};\n\n/**\n * Regex fallback for formats with no AST (YAML, Markdown frontmatter): the\n * leaf field name written as a property key.\n */\nconst findLeafPropertyByText = (\n text: string,\n fieldPath: string[]\n): OffsetSpan | null => {\n const leafName = fieldPath[fieldPath.length - 1];\n\n if (!leafName) return null;\n\n const match = new RegExp(\n `(?<![.\\\\w])(['\"\\`]?)${escapeRegularExpression(leafName)}\\\\1\\\\s*:`,\n 'm'\n ).exec(text);\n\n if (!match) return null;\n\n const start = match.index + match[1]!.length;\n\n return { start, end: start + leafName.length };\n};\n\nconst toSpan = (match: PropertyMatch, offsetShift: number): OffsetSpan => ({\n start: nodeStart(match.keyNode) - offsetShift,\n end: nodeEnd(match.keyNode) - offsetShift,\n});\n\n/**\n * Locate where a dictionary field is declared in its source file.\n *\n * - JSON catalogs may hold the dictionary at their root or under a property\n * named after it (one file splitting into several dictionaries); both are\n * tried, with nested and flat dotted keys.\n * - Content declaration files (`.content.ts`) are walked from the exported\n * object's `content` property.\n * - Other formats fall back to a text search on the leaf name.\n *\n * With an empty `fieldPath`, the dictionary itself is located: its namespace\n * property in a JSON catalog, or the `key` property of a content file.\n *\n * @returns The span of the property key, or `null` when not found.\n */\nexport const findFieldDeclaration = (\n text: string,\n filePath: string,\n dictionaryKey: string,\n fieldPath: string[]\n): OffsetSpan | null => {\n const extension = extname(filePath).toLowerCase();\n const segments = fieldPath.flatMap((propertyName) => propertyName.split('.'));\n\n if (JSON_EXTENSIONS.has(extension)) {\n const json = getJsonRootObject(text);\n\n if (!json) return findLeafPropertyByText(text, fieldPath);\n\n const underNamespace = findDeepestProperty(json.rootObject, [\n dictionaryKey,\n ...segments,\n ]);\n const atRoot =\n segments.length > 0\n ? findDeepestProperty(json.rootObject, segments)\n : null;\n\n const isComplete = (\n match: PropertyMatch | null,\n segmentCount: number\n ): match is PropertyMatch => match?.matchedSegmentCount === segmentCount;\n\n if (isComplete(underNamespace, segments.length + 1)) {\n return toSpan(underNamespace, json.offsetShift);\n }\n\n if (isComplete(atRoot, segments.length)) {\n return toSpan(atRoot, json.offsetShift);\n }\n\n const partial = underNamespace ?? atRoot;\n\n return partial ? toSpan(partial, json.offsetShift) : null;\n }\n\n if (SCRIPT_EXTENSIONS.has(extension)) {\n const rootObject = getContentFileRootObject(text);\n\n if (rootObject) {\n const match = findDeepestProperty(\n rootObject,\n segments.length > 0 ? ['content', ...segments] : ['key']\n );\n\n if (match) return toSpan(match, 0);\n }\n }\n\n return findLeafPropertyByText(text, fieldPath);\n};\n"],"mappings":";;;;;AAaA,MAAM,kCAAkB,IAAI,IAAI;CAAC;CAAS;CAAU;AAAQ,CAAC;AAC7D,MAAM,oCAAoB,IAAI,IAAI;CAChC;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;AAUD,MAAM,oBAAoB,SAAmD;CAC3E,IAAI,UAAU;CAEd,OACE,UAAU,YAAY,oBACtB,UAAU,YAAY,2BACtB,UAAU,YAAY,2BAEtB,UAAU,QAAQ;CAGpB,OAAO;AACT;;;;;AAMA,MAAM,oBAAoB,SAAmD;CAC3E,MAAM,aAAa,iBAAiB,IAAI;CAExC,IAAI,aAAa,YAAY,oBAAoB,OAAO;CAExD,IAAI,aAAa,YAAY,kBAAkB;EAC7C,MAAM,gBAAgB,iBACnB,WAAW,YAAY,GAA6B,EACvD;EAEA,IAAI,gBAAgB,YAAY,oBAAoB,OAAO;CAC7D;AAGF;AAEA,MAAM,sBACJ,YACA,kBAEE,WAAW,iBAA2C,CAAC,EAAC,CAAE,MACzD,cACE,SAAS,YAAY,cACpB,SAAS,YAAY,qBACvB,mBAAmB,SAAS,MAA6B,MACvD,YACN;;;;;;AAOF,MAAM,uBACJ,YACA,UACA,uBAAuB,MACE;CACzB,IAAI,OAA6B;CACjC,MAAM,YAAY,SAAS,SAAS;CAEpC,KAAK,IAAI,SAAS,GAAG,UAAU,WAAW,UAAU;EAClD,MAAM,eAAe,SAClB,MAAM,sBAAsB,uBAAuB,MAAM,CAAC,CAC1D,KAAK,GAAG;EACX,MAAM,WAAW,mBAAmB,YAAY,YAAY;EAE5D,IAAI,CAAC,UAAU;EAEf,MAAM,sBAAsB,uBAAuB;EACnD,IAAI,QAAuB;GACzB,SAAS,SAAS;GAClB;EACF;EAEA,MAAM,cAAc,iBAAiB,SAAS,QAAmB;EAEjE,IAAI,sBAAsB,SAAS,UAAU,aAC3C,QACE,oBAAoB,aAAa,UAAU,mBAAmB,KAC9D;EAGJ,IAAI,MAAM,wBAAwB,SAAS,QAAQ,OAAO;EAE1D,IAAI,CAAC,QAAQ,MAAM,sBAAsB,KAAK,qBAC5C,OAAO;CAEX;CAEA,OAAO;AACT;;AAGA,MAAM,qBACJ,SACwD;CAIxD,MAAM,aADU,UAAU,IAAI,KAAK,IACV,CAAC,GAAG,QAAO,GAA6B;CACjE,MAAM,aAAa,iBACjB,YAAY,aACd;CAEA,OAAO,aAAa;EAAE;EAAY,aAAa;CAAE,IAAI;AACvD;;AAGA,MAAM,4BAA4B,SAAiC;CAEjE,MAAM,OADU,UAAU,IACN,CAAC,GAAG,WAAqC,CAAC;CAE9D,MAAM,gBAAgB,KAAK,MACxB,cAAc,UAAU,YAAY,0BACvC;CACA,IAAI,cAAc,iBAChB,gBAAgB,cAClB;CAGA,IAAI,cAAc,YAAY,cAAc;EAC1C,MAAM,eAAe,YAAY;EAEjC,KAAK,MAAM,aAAa,MAAM;GAC5B,MAAM,sBACJ,UAAU,YAAY,2BACjB,UAAU,iBACX;GAEN,IAAI,sBAAsB,YAAY,uBAAuB;GAE7D,MAAM,cACH,oBAAoB,mBAAiC,CAAC,EAAC,CACxD,MACC,cACE,UAAU,KAAK,GAA2B,YAAY,YAC3D;GAEA,IAAI,YAAY;IACd,cAAc,iBACZ,WAAW,OACb;IACA;GACF;EACF;CACF;CAEA,OAAO,iBAAiB,WAAW,KAAK;AAC1C;;;;;AAMA,MAAM,0BACJ,MACA,cACsB;CACtB,MAAM,WAAW,UAAU,UAAU,SAAS;CAE9C,IAAI,CAAC,UAAU,OAAO;CAEtB,MAAM,QAAQ,IAAI,OAChB,uBAAuB,wBAAwB,QAAQ,EAAE,WACzD,GACF,CAAC,CAAC,KAAK,IAAI;CAEX,IAAI,CAAC,OAAO,OAAO;CAEnB,MAAM,QAAQ,MAAM,QAAQ,MAAM,EAAE,CAAE;CAEtC,OAAO;EAAE;EAAO,KAAK,QAAQ,SAAS;CAAO;AAC/C;AAEA,MAAM,UAAU,OAAsB,iBAAqC;CACzE,OAAO,UAAU,MAAM,OAAO,IAAI;CAClC,KAAK,QAAQ,MAAM,OAAO,IAAI;AAChC;;;;;;;;;;;;;;;;AAiBA,MAAa,wBACX,MACA,UACA,eACA,cACsB;CACtB,MAAM,YAAY,QAAQ,QAAQ,CAAC,CAAC,YAAY;CAChD,MAAM,WAAW,UAAU,SAAS,iBAAiB,aAAa,MAAM,GAAG,CAAC;CAE5E,IAAI,gBAAgB,IAAI,SAAS,GAAG;EAClC,MAAM,OAAO,kBAAkB,IAAI;EAEnC,IAAI,CAAC,MAAM,OAAO,uBAAuB,MAAM,SAAS;EAExD,MAAM,iBAAiB,oBAAoB,KAAK,YAAY,CAC1D,eACA,GAAG,QACL,CAAC;EACD,MAAM,SACJ,SAAS,SAAS,IACd,oBAAoB,KAAK,YAAY,QAAQ,IAC7C;EAEN,MAAM,cACJ,OACA,iBAC2B,OAAO,wBAAwB;EAE5D,IAAI,WAAW,gBAAgB,SAAS,SAAS,CAAC,GAChD,OAAO,OAAO,gBAAgB,KAAK,WAAW;EAGhD,IAAI,WAAW,QAAQ,SAAS,MAAM,GACpC,OAAO,OAAO,QAAQ,KAAK,WAAW;EAGxC,MAAM,UAAU,kBAAkB;EAElC,OAAO,UAAU,OAAO,SAAS,KAAK,WAAW,IAAI;CACvD;CAEA,IAAI,kBAAkB,IAAI,SAAS,GAAG;EACpC,MAAM,aAAa,yBAAyB,IAAI;EAEhD,IAAI,YAAY;GACd,MAAM,QAAQ,oBACZ,YACA,SAAS,SAAS,IAAI,CAAC,WAAW,GAAG,QAAQ,IAAI,CAAC,KAAK,CACzD;GAEA,IAAI,OAAO,OAAO,OAAO,OAAO,CAAC;EACnC;CACF;CAEA,OAAO,uBAAuB,MAAM,SAAS;AAC/C"}
@@ -1,20 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import { parseText, walkAst } from "./oxcUtils.mjs";
3
3
  import { DICTIONARIES_NOT_BUILT_NOTIFICATION } from "./protocol.mjs";
4
- import { formatDictionaryHover, formatFieldHover, formatFieldValue, getFieldByPath, getFieldsAtPath } from "./dictionaryUtils.mjs";
5
4
  import { collectNamespaceReferences, findMessageUsageAtOffset, matchCallNamespace } from "./usageAnalyzer.mjs";
6
5
  import { escapeRegularExpression, findFieldRangesInFile, offsetToRange } from "./findFieldInFile.mjs";
6
+ import { findFieldDeclaration } from "./findFieldDeclaration.mjs";
7
+ import { formatDictionaryHover, formatFieldHover, formatFieldValue, getFieldByPath, getFieldsAtPath } from "./dictionaryUtils.mjs";
8
+ import { resolveDictionaryTarget } from "./resolveDictionaryTarget.mjs";
7
9
  import { buildCallerWithKeyPattern, buildKeyUsagePatterns, getCallerNamesAlternation } from "./callers/patterns.mjs";
8
10
  import { findKeyAtOffset } from "./findKeyAtOffset.mjs";
9
11
  import { findContentFieldAtOffset, findKeyInContentFile } from "./findKeyInContentFile.mjs";
10
- import { existsSync } from "node:fs";
11
- import { readFile } from "node:fs/promises";
12
12
  import { dirname, isAbsolute, join } from "node:path";
13
+ import { CompletionItemKind, DiagnosticSeverity, Location, MarkupKind, Position, ProposedFeatures, Range, TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node";
14
+ import { readFile } from "node:fs/promises";
13
15
  import { pathToFileURL } from "node:url";
14
16
  import { getConfiguration, searchConfigurationFile } from "@intlayer/config/node";
17
+ import { clearAllCache } from "@intlayer/config/utils";
15
18
  import { getUnmergedDictionaries } from "@intlayer/dictionaries-entry/unmerged";
16
19
  import { buildComponentFilesList } from "@intlayer/engine/utils";
17
- import { CompletionItemKind, DiagnosticSeverity, Location, MarkupKind, Position, ProposedFeatures, Range, TextDocumentSyncKind, TextDocuments, createConnection } from "vscode-languageserver/node";
18
20
  import { TextDocument } from "vscode-languageserver-textdocument";
19
21
 
20
22
  //#region src/index.ts
@@ -49,6 +51,7 @@ const projectRoots = /* @__PURE__ */ new Map();
49
51
  */
50
52
  const buildRequestedProjects = /* @__PURE__ */ new Set();
51
53
  const invalidateConfigCaches = () => {
54
+ clearAllCache();
52
55
  projectConfigs.clear();
53
56
  projectDictionaries.clear();
54
57
  projectRoots.clear();
@@ -75,14 +78,15 @@ const getProjectRoot = (fileDirectory) => {
75
78
  const getProjectConfig = (absolutePath) => {
76
79
  try {
77
80
  const baseDir = getProjectRoot(dirname(absolutePath));
78
- const { system, build, content, compiler } = getConfiguration({ baseDir });
81
+ const { system, build, content, compiler, internationalization } = getConfiguration({ baseDir });
79
82
  const cacheKey = system.baseDir;
80
83
  if (!projectConfigs.has(cacheKey)) {
81
84
  projectConfigs.set(cacheKey, {
82
85
  system,
83
86
  build,
84
87
  content,
85
- compiler
88
+ compiler,
89
+ internationalization
86
90
  });
87
91
  log(`getProjectConfig — OK baseDir=${system.baseDir} unmergedDictionariesDir=${system.unmergedDictionariesDir}`);
88
92
  }
@@ -127,36 +131,33 @@ const runConcurrent = async (items, callback, limit = 50) => {
127
131
  };
128
132
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
129
133
  };
130
- /** Find the `key: "<key>"` line inside a content file so the cursor lands on it. */
131
- const getKeyRange = async (absolutePath, key) => {
132
- try {
133
- const lines = (await readFile(absolutePath, "utf-8")).split(/\r?\n/);
134
- const keyRegularExpression = new RegExp(`key\\s*:\\s*['"\`]${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}['"\`]`);
135
- for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
136
- const column = lines[lineIndex]?.search(keyRegularExpression);
137
- if (column !== void 0 && column !== -1) return Range.create(Position.create(lineIndex, column), Position.create(lineIndex, lines[lineIndex].length));
138
- }
139
- } catch {}
140
- return Range.create(Position.create(0, 0), Position.create(0, 0));
141
- };
134
+ /** Built dictionaries registered under `dictionaryKey`. */
135
+ const getDictionariesForKey = (config, dictionaryKey) => getUnmergedDictionariesCached(config)[dictionaryKey] ?? [];
142
136
  /**
143
- * Return every `.content` file location that declares the given key.
144
- * A key can be split across several content files (merged dictionaries), so we
145
- * return one `Location` per source file, pointing at the `key:` declaration.
137
+ * Match a usage (or a bare dictionary key) against the built dictionaries,
138
+ * following the compat adapters' fallbacks (`index` whole-file catalog, flat
139
+ * dotted keys) — see `resolveDictionaryTarget`.
146
140
  */
147
- const getContentDeclarationLocations = async (key, absolutePath) => {
141
+ const resolveTarget = (target, config) => resolveDictionaryTarget(target, (dictionaryKey) => getDictionariesForKey(config, dictionaryKey));
142
+ /**
143
+ * Where a resolved target is declared: one location per source file (content
144
+ * file, or one JSON catalog per locale), the default locale first so VS Code
145
+ * previews it.
146
+ */
147
+ const getDeclarationLocations = async (target, absolutePath) => {
148
148
  const config = getProjectConfig(absolutePath);
149
149
  if (!config) return [];
150
- const unmergedDictionaries = getUnmergedDictionariesCached(config);
151
- const dictionaries = Object.values(unmergedDictionaries).flat().filter((dictionary) => dictionary?.key === key && dictionary.filePath);
152
- const locations = [];
153
- await runConcurrent(dictionaries, async (dictionary) => {
154
- const filePath = dictionary.filePath;
155
- const absolutePath = isAbsolute(filePath) ? filePath : join(config.system.baseDir, filePath);
156
- if (!existsSync(absolutePath)) return;
157
- locations.push(Location.create(pathToFileURL(absolutePath).href, await getKeyRange(absolutePath, key)));
158
- });
159
- return locations;
150
+ const resolved = await resolveTarget(target, config);
151
+ if (!resolved) return [];
152
+ const defaultLocale = config.internationalization?.defaultLocale;
153
+ const sourcePaths = [...new Set([...resolved.dictionaries].sort((first, second) => Number(second.locale === defaultLocale) - Number(first.locale === defaultLocale)).map((dictionary) => dictionary.filePath).filter((filePath) => Boolean(filePath)).map((filePath) => isAbsolute(filePath) ? filePath : join(config.system.baseDir, filePath)))];
154
+ return (await Promise.all(sourcePaths.map(async (sourcePath) => {
155
+ const text = await tryReadFile(sourcePath);
156
+ if (text === null) return null;
157
+ const span = findFieldDeclaration(text, sourcePath, resolved.dictionaryKey, resolved.fieldPath);
158
+ const range = span ? offsetToRange(text, span.start, span.end) : Range.create(Position.create(0, 0), Position.create(0, 0));
159
+ return Location.create(pathToFileURL(sourcePath).href, range);
160
+ }))).filter((location) => location !== null);
160
161
  };
161
162
  /**
162
163
  * Return the cached source-file list, re-scanning only when the cache has
@@ -244,46 +245,6 @@ const getFieldUsageLocations = async (dictionaryKey, fieldPath, absolutePath) =>
244
245
  log(`getFieldUsageLocations("${dictionaryKey}.${fieldPath.join(".")}") — ${filePaths.length} file(s) searched in ${Date.now() - startTime}ms → ${locations.length} hit(s)`);
245
246
  return locations;
246
247
  };
247
- /**
248
- * Find the line/column of the field's leaf name as a property key inside a
249
- * content file. Looks for `fieldName:` or `'field.name':` (flat keys with
250
- * dots, as used by lingui catalogs), not just any occurrence. Falls back to
251
- * the top of the file if the field cannot be located.
252
- */
253
- const getFieldRangeInContentFile = async (absolutePath, fieldPath) => {
254
- const content = await tryReadFile(absolutePath);
255
- const fieldName = fieldPath[fieldPath.length - 1];
256
- if (!content || !fieldName) return Range.create(Position.create(0, 0), Position.create(0, 0));
257
- const fieldRegularExpression = new RegExp(`(?<![.\\w])(['"\`]?)${escapeRegularExpression(fieldName)}\\1\\s*:`);
258
- const lines = content.split(/\r?\n/);
259
- for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
260
- const line = lines[lineIndex];
261
- const matchPosition = line.search(fieldRegularExpression);
262
- if (matchPosition === -1) continue;
263
- const column = line.indexOf(fieldName, matchPosition);
264
- if (column === -1) continue;
265
- return Range.create(Position.create(lineIndex, column), Position.create(lineIndex, column + fieldName.length));
266
- }
267
- return Range.create(Position.create(0, 0), Position.create(0, 0));
268
- };
269
- /**
270
- * Return every content-file location where the field at `fieldPath` is
271
- * defined as a property of the dictionary with the given `dictionaryKey`.
272
- */
273
- const getContentFieldLocations = async (dictionaryKey, fieldPath, absolutePath) => {
274
- const config = getProjectConfig(absolutePath);
275
- if (!config) return [];
276
- const unmergedDictionaries = getUnmergedDictionariesCached(config);
277
- const dictionaries = Object.values(unmergedDictionaries).flat().filter((dictionary) => dictionary?.key === dictionaryKey && dictionary.filePath);
278
- const locations = [];
279
- await runConcurrent(dictionaries, async (dictionary) => {
280
- const filePath = dictionary.filePath;
281
- const absolutePath = isAbsolute(filePath) ? filePath : join(config.system.baseDir, filePath);
282
- if (!existsSync(absolutePath)) return;
283
- locations.push(Location.create(pathToFileURL(absolutePath).href, await getFieldRangeInContentFile(absolutePath, fieldPath)));
284
- });
285
- return locations;
286
- };
287
248
  const DIAGNOSTIC_DEBOUNCE_MS = 300;
288
249
  const diagnosticTimers = /* @__PURE__ */ new Map();
289
250
  /**
@@ -425,19 +386,18 @@ connection.onDefinition(async (parameters) => {
425
386
  const key = findKeyAtOffset(text, offset);
426
387
  if (key) {
427
388
  log(`onDefinition — useIntlayer key: "${key}" → searching content declarations`);
428
- const locations = await getContentDeclarationLocations(key, absolutePath);
389
+ const locations = await getDeclarationLocations({
390
+ dictionaryKey: key,
391
+ fieldPath: []
392
+ }, absolutePath);
429
393
  log(`onDefinition — found ${locations.length} content declaration(s) for key "${key}"`);
430
394
  return locations.length ? locations : null;
431
395
  }
432
396
  const usage = findMessageUsageAtOffset(text, offset);
433
- if (usage && usage.fieldPath.length > 0) {
434
- log(`onDefinition — usage field: "${usage.dictionaryKey}.${usage.fieldPath.join(".")}" (${usage.callerName}) → searching content field`);
435
- const locations = await getContentFieldLocations(usage.dictionaryKey, usage.fieldPath, absolutePath);
436
- log(`onDefinition — found ${locations.length} content file(s) with field "${usage.fieldPath.join(".")}"`);
437
- return locations.length ? locations : null;
438
- }
439
397
  if (usage) {
440
- const locations = await getContentDeclarationLocations(usage.dictionaryKey, absolutePath);
398
+ log(`onDefinition — usage: "${[usage.dictionaryKey, ...usage.fieldPath].join(".")}" (${usage.callerName}) → searching declaration`);
399
+ const locations = await getDeclarationLocations(usage, absolutePath);
400
+ log(`onDefinition — found ${locations.length} declaration(s)`);
441
401
  return locations.length ? locations : null;
442
402
  }
443
403
  log("onDefinition — no Intlayer key at cursor, no result");
@@ -482,13 +442,16 @@ connection.onReferences(async (parameters) => {
482
442
  }
483
443
  const key = findKeyAtOffset(text, offset);
484
444
  if (key) {
485
- const locations = await getContentDeclarationLocations(key, absolutePath);
445
+ const locations = await getDeclarationLocations({
446
+ dictionaryKey: key,
447
+ fieldPath: []
448
+ }, absolutePath);
486
449
  log(`onReferences — key "${key}" → ${locations.length} declaration(s)`);
487
450
  return locations.length ? locations : null;
488
451
  }
489
452
  const usage = findMessageUsageAtOffset(text, offset);
490
453
  if (usage && usage.fieldPath.length > 0) {
491
- const locations = await getContentFieldLocations(usage.dictionaryKey, usage.fieldPath, absolutePath);
454
+ const locations = await getDeclarationLocations(usage, absolutePath);
492
455
  log(`onReferences — field "${usage.fieldPath.join(".")}" → ${locations.length} content definition(s)`);
493
456
  return locations.length ? locations : null;
494
457
  }
@@ -503,12 +466,10 @@ connection.onHover(async (parameters) => {
503
466
  const offset = document.offsetAt(parameters.position);
504
467
  const config = getProjectConfig(absolutePath);
505
468
  if (!config) return null;
506
- const unmergedDictionaries = getUnmergedDictionariesCached(config);
507
- const getDicts = (key) => Object.values(unmergedDictionaries).flat().filter((d) => d?.key === key);
508
469
  if (isContentFile(uri, config)) {
509
470
  const key = findKeyInContentFile(text, offset);
510
471
  if (key) {
511
- const dicts = getDicts(key);
472
+ const dicts = getDictionariesForKey(config, key);
512
473
  if (dicts.length > 0) return { contents: {
513
474
  kind: MarkupKind.Markdown,
514
475
  value: formatDictionaryHover(dicts, key)
@@ -517,7 +478,7 @@ connection.onHover(async (parameters) => {
517
478
  const ext = uri.match(/\.[^.]+$/)?.[0] ?? "";
518
479
  const fieldInfo = findContentFieldAtOffset(text, offset, ext);
519
480
  if (fieldInfo) {
520
- const dicts = getDicts(fieldInfo.dictionaryKey);
481
+ const dicts = getDictionariesForKey(config, fieldInfo.dictionaryKey);
521
482
  if (dicts.length > 0) {
522
483
  const hoverText = formatFieldHover(dicts, fieldInfo.dictionaryKey, fieldInfo.fieldPath);
523
484
  if (hoverText) return { contents: {
@@ -529,30 +490,22 @@ connection.onHover(async (parameters) => {
529
490
  return null;
530
491
  }
531
492
  const key = findKeyAtOffset(text, offset);
532
- if (key) {
533
- const dicts = getDicts(key);
534
- if (dicts.length > 0) return { contents: {
535
- kind: MarkupKind.Markdown,
536
- value: formatDictionaryHover(dicts, key)
537
- } };
538
- return null;
539
- }
540
- const usage = findMessageUsageAtOffset(text, offset);
541
- if (usage) {
542
- const dicts = getDicts(usage.dictionaryKey);
543
- if (dicts.length > 0) {
544
- if (usage.fieldPath.length === 0) return { contents: {
545
- kind: MarkupKind.Markdown,
546
- value: formatDictionaryHover(dicts, usage.dictionaryKey)
547
- } };
548
- const hoverText = formatFieldHover(dicts, usage.dictionaryKey, usage.fieldPath);
549
- if (hoverText) return { contents: {
550
- kind: MarkupKind.Markdown,
551
- value: hoverText
552
- } };
553
- }
554
- }
555
- return null;
493
+ const target = key ? {
494
+ dictionaryKey: key,
495
+ fieldPath: []
496
+ } : findMessageUsageAtOffset(text, offset);
497
+ if (!target) return null;
498
+ const resolved = await resolveTarget(target, config);
499
+ if (!resolved) return null;
500
+ if (resolved.fieldPath.length === 0) return { contents: {
501
+ kind: MarkupKind.Markdown,
502
+ value: formatDictionaryHover(resolved.dictionaries, resolved.dictionaryKey)
503
+ } };
504
+ const hoverText = formatFieldHover(resolved.dictionaries, resolved.dictionaryKey, resolved.fieldPath);
505
+ return hoverText ? { contents: {
506
+ kind: MarkupKind.Markdown,
507
+ value: hoverText
508
+ } } : null;
556
509
  });
557
510
  connection.onCompletion(async (parameters) => {
558
511
  const uri = parameters.textDocument.uri;