@intlayer/lsp 9.5.12 → 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.
- package/dist/esm/callers/editorCallers.mjs +39 -0
- package/dist/esm/callers/editorCallers.mjs.map +1 -0
- package/dist/esm/findFieldDeclaration.mjs +152 -0
- package/dist/esm/findFieldDeclaration.mjs.map +1 -0
- package/dist/esm/index.mjs +63 -110
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/resolveDictionaryTarget.mjs +101 -0
- package/dist/esm/resolveDictionaryTarget.mjs.map +1 -0
- package/dist/esm/usageAnalyzer.mjs +150 -14
- package/dist/esm/usageAnalyzer.mjs.map +1 -1
- package/dist/esm/utils.mjs +3 -1
- package/dist/types/callers/editorCallers.d.ts +15 -0
- package/dist/types/callers/editorCallers.d.ts.map +1 -0
- package/dist/types/findFieldDeclaration.d.ts +24 -0
- package/dist/types/findFieldDeclaration.d.ts.map +1 -0
- package/dist/types/resolveDictionaryTarget.d.ts +59 -0
- package/dist/types/resolveDictionaryTarget.d.ts.map +1 -0
- package/dist/types/usageAnalyzer.d.ts +6 -0
- package/dist/types/usageAnalyzer.d.ts.map +1 -1
- package/dist/types/utils.d.ts +3 -1
- package/package.json +6 -6
|
@@ -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"}
|
package/dist/esm/index.mjs
CHANGED
|
@@ -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
|
-
/**
|
|
131
|
-
const
|
|
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
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
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
|
|
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
|
|
151
|
-
|
|
152
|
-
const
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const
|
|
156
|
-
if (
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
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;
|