@intlayer/lsp 8.11.4 → 9.0.0-canary.12
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/_virtual/_rolldown/runtime.mjs +36 -0
- package/dist/esm/callers/index.mjs +12 -0
- package/dist/esm/callers/index.mjs.map +1 -0
- package/dist/esm/callers/patterns.mjs +37 -0
- package/dist/esm/callers/patterns.mjs.map +1 -0
- package/dist/esm/dictionaryUtils.mjs +84 -0
- package/dist/esm/dictionaryUtils.mjs.map +1 -0
- package/dist/esm/findFieldInFile.mjs +22 -125
- package/dist/esm/findFieldInFile.mjs.map +1 -1
- package/dist/esm/findKeyAtOffset.mjs +20 -24
- package/dist/esm/findKeyAtOffset.mjs.map +1 -1
- package/dist/esm/findKeyInContentFile.mjs +259 -40
- package/dist/esm/findKeyInContentFile.mjs.map +1 -1
- package/dist/esm/findTCallAtOffset.mjs +33 -0
- package/dist/esm/findTCallAtOffset.mjs.map +1 -0
- package/dist/esm/findUsageFieldAtOffset.mjs +12 -51
- package/dist/esm/findUsageFieldAtOffset.mjs.map +1 -1
- package/dist/esm/index.mjs +338 -170
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/oxcUtils.mjs +99 -29
- package/dist/esm/oxcUtils.mjs.map +1 -1
- package/dist/esm/usageAnalyzer.mjs +692 -0
- package/dist/esm/usageAnalyzer.mjs.map +1 -0
- package/dist/esm/utils.mjs +12 -1
- package/dist/types/callers/index.d.ts +8 -0
- package/dist/types/callers/index.d.ts.map +1 -0
- package/dist/types/callers/patterns.d.ts +30 -0
- package/dist/types/callers/patterns.d.ts.map +1 -0
- package/dist/types/dictionaryUtils.d.ts +12 -0
- package/dist/types/dictionaryUtils.d.ts.map +1 -0
- package/dist/types/findFieldInFile.d.ts +12 -14
- package/dist/types/findFieldInFile.d.ts.map +1 -1
- package/dist/types/findKeyAtOffset.d.ts +13 -10
- package/dist/types/findKeyAtOffset.d.ts.map +1 -1
- package/dist/types/findKeyInContentFile.d.ts +15 -13
- package/dist/types/findKeyInContentFile.d.ts.map +1 -1
- package/dist/types/findTCallAtOffset.d.ts +24 -0
- package/dist/types/findTCallAtOffset.d.ts.map +1 -0
- package/dist/types/findUsageFieldAtOffset.d.ts +2 -1
- package/dist/types/findUsageFieldAtOffset.d.ts.map +1 -1
- package/dist/types/oxcUtils.d.ts +42 -13
- package/dist/types/oxcUtils.d.ts.map +1 -1
- package/dist/types/usageAnalyzer.d.ts +129 -0
- package/dist/types/usageAnalyzer.d.ts.map +1 -0
- package/dist/types/utils.d.ts +15 -1
- package/package.json +9 -22
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
//#region \0rolldown/runtime.js
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __exportAll = (all, no_symbols) => {
|
|
7
|
+
let target = {};
|
|
8
|
+
for (var name in all) {
|
|
9
|
+
__defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
if (!no_symbols) {
|
|
15
|
+
__defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
16
|
+
}
|
|
17
|
+
return target;
|
|
18
|
+
};
|
|
19
|
+
var __copyProps = (to, from, except, desc) => {
|
|
20
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
21
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
22
|
+
key = keys[i];
|
|
23
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
24
|
+
__defProp(to, key, {
|
|
25
|
+
get: ((k) => from[k]).bind(null, key),
|
|
26
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return to;
|
|
32
|
+
};
|
|
33
|
+
var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget && __copyProps(secondTarget, mod, "default"));
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { __exportAll, __reExport };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { __exportAll, __reExport } from "../_virtual/_rolldown/runtime.mjs";
|
|
2
|
+
|
|
3
|
+
export * from "@intlayer/config/callers"
|
|
4
|
+
|
|
5
|
+
//#region src/callers/index.ts
|
|
6
|
+
var callers_exports = /* @__PURE__ */ __exportAll({});
|
|
7
|
+
import * as import__intlayer_config_callers from "@intlayer/config/callers";
|
|
8
|
+
__reExport(callers_exports, import__intlayer_config_callers);
|
|
9
|
+
|
|
10
|
+
//#endregion
|
|
11
|
+
export { callers_exports };
|
|
12
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../../../src/callers/index.ts"],"sourcesContent":["/**\n * Caller registry — re-exported from the shared source of truth in\n * `@intlayer/config/callers`.\n *\n * The descriptors are shared with `@intlayer/babel` (usage analysis + optimize\n * rewrite), `@intlayer/swc` (via `toSwcExtraCallers`) and every compat\n * package's bundler plugin, so a compat library declared once in\n * `@intlayer/config/src/callers/compat/<lib>.ts` is supported everywhere.\n *\n * Editor-specific matching helpers built on top of the registry live in\n * `./patterns.ts`.\n */\nexport * from '@intlayer/config/callers';\n"],"mappings":""}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { callers_exports } from "./index.mjs";
|
|
2
|
+
import { escapeRegularExpression } from "../findFieldInFile.mjs";
|
|
3
|
+
|
|
4
|
+
//#region src/callers/patterns.ts
|
|
5
|
+
/**
|
|
6
|
+
* Alternation of every caller name whose dictionary key appears as a
|
|
7
|
+
* positional string argument — for building text-search patterns.
|
|
8
|
+
*/
|
|
9
|
+
const getCallerNamesAlternation = () => (0, callers_exports.getPositionalKeyCallerNames)().join("|");
|
|
10
|
+
/**
|
|
11
|
+
* Regexes matching call sites that reference the dictionary `key` in project
|
|
12
|
+
* source files. Used by "find all references" (content file → call sites).
|
|
13
|
+
*
|
|
14
|
+
* Two shapes are covered:
|
|
15
|
+
* 1. Positional key argument, with optional TypeScript generic:
|
|
16
|
+
* `useIntlayer("key")`, `useTranslations<T>("key")`, `getFixedT("en", "key")`
|
|
17
|
+
* 2. Options/descriptor object property:
|
|
18
|
+
* `getTranslations({ namespace: 'key' })`, `useI18n({ namespace: 'key' })`,
|
|
19
|
+
* `formatMessage({ id: 'key.field' })`
|
|
20
|
+
*
|
|
21
|
+
* @param key - The dictionary key to search for.
|
|
22
|
+
*/
|
|
23
|
+
const buildKeyUsagePatterns = (key) => {
|
|
24
|
+
const escapedKey = escapeRegularExpression(key);
|
|
25
|
+
return [new RegExp(`\\b(?:${getCallerNamesAlternation()})\\b\\s*(?:<[^<>()]*>)?\\s*\\(\\s*(['"\`])${escapedKey}\\1`, "g"), new RegExp(`\\{\\s*(?:id|namespace)\\s*:\\s*(['"\`])${escapedKey}(?:\\.[^'"\`\\n]*)?\\1`, "g")];
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Regex testing whether a file contains a caller invoked with the given
|
|
29
|
+
* dictionary key — a cheap pre-filter before running field-level analysis.
|
|
30
|
+
*
|
|
31
|
+
* @param key - The dictionary key.
|
|
32
|
+
*/
|
|
33
|
+
const buildCallerWithKeyPattern = (key) => new RegExp(`\\b(?:${getCallerNamesAlternation()})\\b(?:<[^<>()]*>)?\\s*\\(\\s*['"\`]${escapeRegularExpression(key)}['"\`]`);
|
|
34
|
+
|
|
35
|
+
//#endregion
|
|
36
|
+
export { buildCallerWithKeyPattern, buildKeyUsagePatterns, getCallerNamesAlternation };
|
|
37
|
+
//# sourceMappingURL=patterns.mjs.map
|
|
@@ -0,0 +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,oFACkB,CAAC,KAAK,IAAI;;;;;;;;;;;;;;AAezC,MAAa,yBAAyB,QAA0B;CAC9D,MAAM,aAAa,wBAAwB,IAAI;AAE/C,QAAO,CACL,IAAI,OACF,SAAS,2BAA2B,CAAC,4CAA4C,WAAW,MAC5F,IACD,EACD,IAAI,OACF,2CAA2C,WAAW,yBACtD,IACD,CACF;;;;;;;;AASH,MAAa,6BAA6B,QACxC,IAAI,OACF,SAAS,2BAA2B,CAAC,sCAAsC,wBAAwB,IAAI,CAAC,QACzG"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
//#region src/dictionaryUtils.ts
|
|
2
|
+
const LOCALE_PATTERN = /^[a-z]{2}(-[A-Z]{2})?$/;
|
|
3
|
+
const META_KEYS = new Set([
|
|
4
|
+
"nodeType",
|
|
5
|
+
"translation",
|
|
6
|
+
"enumeration",
|
|
7
|
+
"condition",
|
|
8
|
+
"plural",
|
|
9
|
+
"insertion",
|
|
10
|
+
"nested",
|
|
11
|
+
"markdown",
|
|
12
|
+
"html",
|
|
13
|
+
"file",
|
|
14
|
+
"composite",
|
|
15
|
+
"dictionaryKey"
|
|
16
|
+
]);
|
|
17
|
+
const isTypedNode = (val) => typeof val === "object" && val !== null && typeof val["nodeType"] === "string";
|
|
18
|
+
const getTypedNodeSummary = (node) => {
|
|
19
|
+
const nodeType = node["nodeType"];
|
|
20
|
+
const inner = node[nodeType];
|
|
21
|
+
if (inner && typeof inner === "object") {
|
|
22
|
+
const map = inner;
|
|
23
|
+
const localeEntries = Object.entries(map).filter(([key]) => LOCALE_PATTERN.test(key));
|
|
24
|
+
const entries = localeEntries.length > 0 ? localeEntries : Object.entries(map);
|
|
25
|
+
if (entries.length > 0) return entries.slice(0, 5).map(([k, v]) => `**${k}**: ${typeof v === "string" ? v : JSON.stringify(v)}`).join("\n\n");
|
|
26
|
+
}
|
|
27
|
+
return `*(${nodeType})*`;
|
|
28
|
+
};
|
|
29
|
+
const formatFieldValue = (value) => {
|
|
30
|
+
if (value === null || value === void 0) return `\`${String(value)}\``;
|
|
31
|
+
if (typeof value === "string") return `"${value}"`;
|
|
32
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
33
|
+
if (typeof value === "object") {
|
|
34
|
+
const obj = value;
|
|
35
|
+
if (isTypedNode(obj)) return getTypedNodeSummary(obj);
|
|
36
|
+
const fields = Object.keys(obj).filter((key) => !META_KEYS.has(key) && !LOCALE_PATTERN.test(key));
|
|
37
|
+
return fields.length > 0 ? `{ ${fields.slice(0, 8).join(", ")} }` : "{}";
|
|
38
|
+
}
|
|
39
|
+
return JSON.stringify(value);
|
|
40
|
+
};
|
|
41
|
+
const getFieldByPath = (content, path) => {
|
|
42
|
+
let current = content;
|
|
43
|
+
for (const key of path) {
|
|
44
|
+
if (current === null || typeof current !== "object") return void 0;
|
|
45
|
+
current = current[key];
|
|
46
|
+
}
|
|
47
|
+
return current;
|
|
48
|
+
};
|
|
49
|
+
const isNavigableField = (key) => !META_KEYS.has(key) && !LOCALE_PATTERN.test(key);
|
|
50
|
+
const getTopLevelFields = (content) => {
|
|
51
|
+
if (!content || typeof content !== "object") return [];
|
|
52
|
+
return Object.keys(content).filter(isNavigableField);
|
|
53
|
+
};
|
|
54
|
+
const getFieldsAtPath = (content, path) => {
|
|
55
|
+
const value = path.length === 0 ? content : getFieldByPath(content, path);
|
|
56
|
+
if (!value || typeof value !== "object") return [];
|
|
57
|
+
if (isTypedNode(value)) return [];
|
|
58
|
+
return Object.keys(value).filter(isNavigableField);
|
|
59
|
+
};
|
|
60
|
+
const formatDictionaryHover = (dictionaries, key) => {
|
|
61
|
+
const primary = dictionaries[0];
|
|
62
|
+
const lines = [`**Intlayer dictionary** \`${key}\``];
|
|
63
|
+
if (primary.title) lines.push(`*${primary.title}*`);
|
|
64
|
+
if (primary.description) lines.push(primary.description);
|
|
65
|
+
const fields = getTopLevelFields(primary.content);
|
|
66
|
+
if (fields.length > 0) {
|
|
67
|
+
lines.push("");
|
|
68
|
+
lines.push(`**Fields:** ${fields.map((field) => `\`${field}\``).join(", ")}`);
|
|
69
|
+
}
|
|
70
|
+
if (dictionaries.length > 1) lines.push(`*(${dictionaries.length} source files)*`);
|
|
71
|
+
return lines.join("\n\n");
|
|
72
|
+
};
|
|
73
|
+
const formatFieldHover = (dictionaries, dictionaryKey, fieldPath) => {
|
|
74
|
+
for (const dict of dictionaries) {
|
|
75
|
+
const value = getFieldByPath(dict.content, fieldPath);
|
|
76
|
+
if (value === void 0) continue;
|
|
77
|
+
return [`**\`${dictionaryKey}.${fieldPath.join(".")}\`**`, formatFieldValue(value)].join("\n\n");
|
|
78
|
+
}
|
|
79
|
+
return null;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
//#endregion
|
|
83
|
+
export { formatDictionaryHover, formatFieldHover, formatFieldValue, getFieldByPath, getFieldsAtPath, getTopLevelFields };
|
|
84
|
+
//# sourceMappingURL=dictionaryUtils.mjs.map
|
|
@@ -0,0 +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":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { Position, Range } from "vscode-languageserver/node
|
|
1
|
+
import { collectMessageUsages } from "./usageAnalyzer.mjs";
|
|
2
|
+
import { Position, Range } from "vscode-languageserver/node";
|
|
3
3
|
|
|
4
4
|
//#region src/findFieldInFile.ts
|
|
5
5
|
const escapeRegularExpression = (content) => content.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
@@ -12,100 +12,20 @@ const offsetToRange = (text, start, end) => {
|
|
|
12
12
|
const endCharacter = end - (beforeEnd.lastIndexOf("\n") + 1);
|
|
13
13
|
return Range.create(Position.create(startLine, startCharacter), Position.create(endLine, endCharacter));
|
|
14
14
|
};
|
|
15
|
-
const isComputedAccess = (node) => !!node["computed"];
|
|
16
|
-
/**
|
|
17
|
-
* Walk up a chain of MemberExpressions to find the root Identifier name.
|
|
18
|
-
* Returns null for computed access (`a[b]`) or non-Identifier roots.
|
|
19
|
-
*/
|
|
20
|
-
const getChainRoot = (node) => {
|
|
21
|
-
let current = node;
|
|
22
|
-
while (current["type"] === "MemberExpression") {
|
|
23
|
-
if (isComputedAccess(current)) return null;
|
|
24
|
-
current = current["object"];
|
|
25
|
-
}
|
|
26
|
-
return current["type"] === "Identifier" ? current["name"] : null;
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Collect the property names in a chain of MemberExpressions, excluding the
|
|
30
|
-
* root Identifier. Returns null for any computed access.
|
|
31
|
-
* e.g. `content.searchInput.text` → `['searchInput', 'text']`
|
|
32
|
-
*/
|
|
33
|
-
const getChainProperties = (node) => {
|
|
34
|
-
const chain = [];
|
|
35
|
-
let current = node;
|
|
36
|
-
while (current["type"] === "MemberExpression") {
|
|
37
|
-
if (isComputedAccess(current)) return null;
|
|
38
|
-
const name = current["property"]["name"];
|
|
39
|
-
if (!name) return null;
|
|
40
|
-
chain.unshift(name);
|
|
41
|
-
current = current["object"];
|
|
42
|
-
}
|
|
43
|
-
return chain;
|
|
44
|
-
};
|
|
45
|
-
/**
|
|
46
|
-
* Recursively search for a field at `path[depth]` inside an ObjectPattern
|
|
47
|
-
* (destructure). Returns the Range of the matched key node, or null.
|
|
48
|
-
*
|
|
49
|
-
* e.g. `{ searchInput: { text } }` with path `['searchInput', 'text']` depth 0
|
|
50
|
-
* → returns the range of the `text` identifier inside the nested pattern.
|
|
51
|
-
*/
|
|
52
|
-
const findInObjectPattern = (pattern, path, depth, text) => {
|
|
53
|
-
if (pattern["type"] !== "ObjectPattern") return null;
|
|
54
|
-
if (depth >= path.length) return null;
|
|
55
|
-
const targetName = path[depth];
|
|
56
|
-
for (const prop of pattern["properties"] ?? []) {
|
|
57
|
-
if (prop["type"] !== "Property" && prop["type"] !== "ObjectProperty") continue;
|
|
58
|
-
const keyNode = prop["key"];
|
|
59
|
-
if (!keyNode) continue;
|
|
60
|
-
if ((keyNode["name"] ?? keyNode["value"]) !== targetName) continue;
|
|
61
|
-
if (depth === path.length - 1) return offsetToRange(text, keyNode["start"], keyNode["end"]);
|
|
62
|
-
const valueNode = prop["value"];
|
|
63
|
-
if (!valueNode) return null;
|
|
64
|
-
return findInObjectPattern(valueNode, path, depth + 1, text);
|
|
65
|
-
}
|
|
66
|
-
return null;
|
|
67
|
-
};
|
|
68
|
-
/**
|
|
69
|
-
* Recursively collect variables that were destructured out of an ObjectPattern,
|
|
70
|
-
* recording the "remaining intlayer path" each variable represents.
|
|
71
|
-
*
|
|
72
|
-
* e.g. given `{ searchInput }` with basePath `[]`, the variable `searchInput`
|
|
73
|
-
* maps to remaining path `['searchInput']` (meaning it holds the searchInput
|
|
74
|
-
* branch of the dictionary).
|
|
75
|
-
*
|
|
76
|
-
* e.g. given `{ searchInput: { text: alias } }` with basePath `[]`, the
|
|
77
|
-
* variable `alias` maps to remaining path `['searchInput', 'text']`.
|
|
78
|
-
*/
|
|
79
|
-
const collectDestructuredVariables = (pattern, basePath, map) => {
|
|
80
|
-
if (pattern["type"] !== "ObjectPattern") return;
|
|
81
|
-
for (const prop of pattern["properties"] ?? []) {
|
|
82
|
-
if (prop["type"] !== "Property" && prop["type"] !== "ObjectProperty") continue;
|
|
83
|
-
const keyNode = prop["key"];
|
|
84
|
-
const valueNode = prop["value"];
|
|
85
|
-
if (!keyNode || !valueNode) continue;
|
|
86
|
-
const keyName = keyNode["name"] ?? keyNode["value"];
|
|
87
|
-
if (!keyName) continue;
|
|
88
|
-
const thisPath = [...basePath, keyName];
|
|
89
|
-
if (valueNode["type"] === "Identifier") map.set(valueNode["name"], thisPath);
|
|
90
|
-
else if (valueNode["type"] === "ObjectPattern") collectDestructuredVariables(valueNode, thisPath, map);
|
|
91
|
-
}
|
|
92
|
-
};
|
|
93
15
|
/**
|
|
94
16
|
* Within a single file's text, find all positions where `fieldPath` (or legacy
|
|
95
|
-
* `fieldName`) is
|
|
96
|
-
*
|
|
17
|
+
* `fieldName`) is consumed from the dictionary `escapedKey` — through any
|
|
18
|
+
* registered caller.
|
|
97
19
|
*
|
|
98
|
-
* Covers
|
|
99
|
-
* 1. Direct destructure:
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
* const content = useIntlayer("key")
|
|
108
|
-
* content.parent.child
|
|
20
|
+
* Covers base-intlayer patterns:
|
|
21
|
+
* 1. Direct destructure: const { field } = useIntlayer("key")
|
|
22
|
+
* 2. Nested destructure: const { parent: { child } } = useIntlayer("key")
|
|
23
|
+
* 3. Indirect member access: const { parent } = useIntlayer("key"); parent.child
|
|
24
|
+
* 4. Full chain access: const content = useIntlayer("key"); content.parent.child
|
|
25
|
+
*
|
|
26
|
+
* …and every compat form resolved by `collectMessageUsages`: translation
|
|
27
|
+
* calls (`t('parent.child')`), self calls (`formatMessage({ id })`), JSX
|
|
28
|
+
* message components and lingui tagged templates.
|
|
109
29
|
*
|
|
110
30
|
* `fieldPath` (optional 5th parameter) is the preferred input; when omitted
|
|
111
31
|
* the function derives the single-element path from `escapedField`, preserving
|
|
@@ -115,39 +35,16 @@ const findFieldRangesInFile = (text, escapedKey, _fieldName, escapedField, field
|
|
|
115
35
|
const dictionaryKey = escapedKey.replace(/\\([.*+?^${}()|[\]\\])/g, "$1");
|
|
116
36
|
const unescapedLeaf = escapedField.replace(/\\([.*+?^${}()|[\]\\])/g, "$1");
|
|
117
37
|
const path = fieldPath && fieldPath.length > 0 ? fieldPath : [unescapedLeaf];
|
|
118
|
-
const program = parseText(text);
|
|
119
|
-
if (!program) return [];
|
|
120
38
|
const ranges = [];
|
|
121
|
-
const
|
|
122
|
-
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
if (!
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (idNode["type"] === "ObjectPattern") {
|
|
131
|
-
const range = findInObjectPattern(idNode, path, 0, text);
|
|
132
|
-
if (range) ranges.push(range);
|
|
133
|
-
collectDestructuredVariables(idNode, [], variableToRemainingPath);
|
|
134
|
-
}
|
|
135
|
-
});
|
|
136
|
-
walkAst(program, (node) => {
|
|
137
|
-
if (node["type"] !== "MemberExpression") return;
|
|
138
|
-
if (isComputedAccess(node)) return;
|
|
139
|
-
const root = getChainRoot(node);
|
|
140
|
-
if (!root) return;
|
|
141
|
-
const remainingPath = variableToRemainingPath.get(root);
|
|
142
|
-
if (remainingPath === void 0) return;
|
|
143
|
-
const chain = getChainProperties(node);
|
|
144
|
-
if (!chain) return;
|
|
145
|
-
const fullPath = [...remainingPath, ...chain];
|
|
146
|
-
if (fullPath.length !== path.length) return;
|
|
147
|
-
if (!fullPath.every((segment, i) => segment === path[i])) return;
|
|
148
|
-
const leafPropertyNode = node["property"];
|
|
149
|
-
ranges.push(offsetToRange(text, leafPropertyNode["start"], leafPropertyNode["end"]));
|
|
150
|
-
});
|
|
39
|
+
for (const usage of collectMessageUsages(text)) {
|
|
40
|
+
if (usage.dictionaryKey !== dictionaryKey) continue;
|
|
41
|
+
if (usage.isBareReference) continue;
|
|
42
|
+
if (usage.fieldPath.length !== path.length) continue;
|
|
43
|
+
if (!usage.fieldPath.every((segment, index) => segment === path[index])) continue;
|
|
44
|
+
const lastFieldSpan = usage.fieldSpans?.[usage.fieldSpans.length - 1];
|
|
45
|
+
if (usage.kind === "member" && lastFieldSpan) ranges.push(offsetToRange(text, lastFieldSpan.start, lastFieldSpan.end));
|
|
46
|
+
else ranges.push(offsetToRange(text, usage.start, usage.end));
|
|
47
|
+
}
|
|
151
48
|
return ranges;
|
|
152
49
|
};
|
|
153
50
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findFieldInFile.mjs","names":[],"sources":["../../src/findFieldInFile.ts"],"sourcesContent":["import { Position, Range } from 'vscode-languageserver/node.js';\nimport {\n getFirstStringArg,\n isIntlayerCall,\n type OxcNode,\n parseText,\n walkAst,\n} from './oxcUtils';\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// Helpers for nested path matching\n// ---------------------------------------------------------------------------\n\nconst isComputedAccess = (node: OxcNode): boolean =>\n !!(node['computed'] as boolean);\n\n/**\n * Walk up a chain of MemberExpressions to find the root Identifier name.\n * Returns null for computed access (`a[b]`) or non-Identifier roots.\n */\nconst getChainRoot = (node: OxcNode): string | null => {\n let current = node;\n while (current['type'] === 'MemberExpression') {\n if (isComputedAccess(current)) return null;\n current = current['object'] as OxcNode;\n }\n return current['type'] === 'Identifier' ? (current['name'] as string) : null;\n};\n\n/**\n * Collect the property names in a chain of MemberExpressions, excluding the\n * root Identifier. Returns null for any computed access.\n * e.g. `content.searchInput.text` → `['searchInput', 'text']`\n */\nconst getChainProperties = (node: OxcNode): string[] | null => {\n const chain: string[] = [];\n let current = node;\n while (current['type'] === 'MemberExpression') {\n if (isComputedAccess(current)) return null;\n const prop = current['property'] as OxcNode;\n const name = prop['name'] as string | undefined;\n if (!name) return null;\n chain.unshift(name);\n current = current['object'] as OxcNode;\n }\n return chain;\n};\n\n/**\n * Recursively search for a field at `path[depth]` inside an ObjectPattern\n * (destructure). Returns the Range of the matched key node, or null.\n *\n * e.g. `{ searchInput: { text } }` with path `['searchInput', 'text']` depth 0\n * → returns the range of the `text` identifier inside the nested pattern.\n */\nconst findInObjectPattern = (\n pattern: OxcNode,\n path: string[],\n depth: number,\n text: string\n): Range | null => {\n if (pattern['type'] !== 'ObjectPattern') return null;\n if (depth >= path.length) return null;\n\n const targetName = path[depth]!;\n\n for (const prop of (pattern['properties'] as OxcNode[]) ?? []) {\n if (prop['type'] !== 'Property' && prop['type'] !== 'ObjectProperty')\n continue;\n const keyNode = prop['key'] as OxcNode | undefined;\n if (!keyNode) continue;\n\n const keyName =\n (keyNode['name'] as string | undefined) ??\n (keyNode['value'] as string | undefined);\n if (keyName !== targetName) continue;\n\n if (depth === path.length - 1) {\n return offsetToRange(\n text,\n keyNode['start'] as number,\n keyNode['end'] as number\n );\n }\n\n const valueNode = prop['value'] as OxcNode | undefined;\n if (!valueNode) return null;\n return findInObjectPattern(valueNode, path, depth + 1, text);\n }\n\n return null;\n};\n\n/**\n * Recursively collect variables that were destructured out of an ObjectPattern,\n * recording the \"remaining intlayer path\" each variable represents.\n *\n * e.g. given `{ searchInput }` with basePath `[]`, the variable `searchInput`\n * maps to remaining path `['searchInput']` (meaning it holds the searchInput\n * branch of the dictionary).\n *\n * e.g. given `{ searchInput: { text: alias } }` with basePath `[]`, the\n * variable `alias` maps to remaining path `['searchInput', 'text']`.\n */\nconst collectDestructuredVariables = (\n pattern: OxcNode,\n basePath: string[],\n map: Map<string, string[]>\n): void => {\n if (pattern['type'] !== 'ObjectPattern') return;\n\n for (const prop of (pattern['properties'] as OxcNode[]) ?? []) {\n if (prop['type'] !== 'Property' && prop['type'] !== 'ObjectProperty')\n continue;\n const keyNode = prop['key'] as OxcNode | undefined;\n const valueNode = prop['value'] as OxcNode | undefined;\n if (!keyNode || !valueNode) continue;\n\n const keyName =\n (keyNode['name'] as string | undefined) ??\n (keyNode['value'] as string | undefined);\n if (!keyName) continue;\n\n const thisPath = [...basePath, keyName];\n\n if (valueNode['type'] === 'Identifier') {\n // `{ a }` or `{ a: alias }` → variable name is valueNode.name\n map.set(valueNode['name'] as string, thisPath);\n } else if (valueNode['type'] === 'ObjectPattern') {\n // `{ a: { b } }` → 'a' is consumed as a pattern level; recurse\n collectDestructuredVariables(valueNode, thisPath, map);\n }\n // AssignmentPattern defaults (`{ a = 'x' }`) are ignored for simplicity\n }\n};\n\n// ---------------------------------------------------------------------------\n// Main export\n// ---------------------------------------------------------------------------\n\n/**\n * Within a single file's text, find all positions where `fieldPath` (or legacy\n * `fieldName`) is used as a destructured property or member access on a\n * `useIntlayer(key)` / `getIntlayer(key)` result.\n *\n * Covers four patterns:\n * 1. Direct destructure:\n * const { field } = useIntlayer(\"key\")\n * 2. Nested destructure:\n * const { parent: { child } } = useIntlayer(\"key\")\n * 3. Indirect variable + member access:\n * const { parent } = useIntlayer(\"key\")\n * parent.child\n * 4. Full chain member access:\n * const content = useIntlayer(\"key\")\n * content.parent.child\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 program = parseText(text);\n if (!program) return [];\n\n const ranges: Range[] = [];\n\n // variable name → \"remaining intlayer path\" represented by this variable\n // '' (root) entry would map to [] for `const v = useIntlayer('key')`\n const variableToRemainingPath = new Map<string, string[]>();\n\n // First pass: collect useIntlayer / getIntlayer variable assignments and\n // destructures, and emit ranges for direct/nested ObjectPattern matches.\n walkAst(program, (node) => {\n if (node['type'] !== 'VariableDeclarator') return;\n\n const initializer = node['init'] as OxcNode | undefined;\n if (!initializer || !isIntlayerCall(initializer)) return;\n\n const dk = getFirstStringArg(initializer);\n if (dk !== dictionaryKey) return;\n\n const idNode = node['id'] as OxcNode | undefined;\n if (!idNode) return;\n\n if (idNode['type'] === 'Identifier') {\n // `const content = useIntlayer('key')` → root variable, remaining path []\n variableToRemainingPath.set(idNode['name'] as string, []);\n }\n\n if (idNode['type'] === 'ObjectPattern') {\n // Check if the target path is matched directly in the destructure pattern\n const range = findInObjectPattern(idNode, path, 0, text);\n if (range) ranges.push(range);\n\n // Collect destructured variables for the member-access pass\n collectDestructuredVariables(idNode, [], variableToRemainingPath);\n }\n });\n\n // Second pass: find member-access chains that, combined with the variable's\n // remaining path, equal the target path.\n walkAst(program, (node) => {\n if (node['type'] !== 'MemberExpression') return;\n if (isComputedAccess(node)) return;\n\n const root = getChainRoot(node);\n if (!root) return;\n\n const remainingPath = variableToRemainingPath.get(root);\n if (remainingPath === undefined) return;\n\n const chain = getChainProperties(node);\n if (!chain) return;\n\n const fullPath = [...remainingPath, ...chain];\n if (fullPath.length !== path.length) return;\n if (!fullPath.every((segment, i) => segment === path[i])) return;\n\n // Highlight the last (leaf) property in the chain\n const leafPropertyNode = node['property'] as OxcNode;\n ranges.push(\n offsetToRange(\n text,\n leafPropertyNode['start'] as number,\n leafPropertyNode['end'] as number\n )\n );\n });\n\n return ranges;\n};\n"],"mappings":"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"}
|
|
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,OAAO;AAEhD,MAAa,iBACX,MACA,OACA,QACU;CACV,MAAM,cAAc,KAAK,MAAM,GAAG,MAAM;CACxC,MAAM,aAAa,YAAY,MAAM,MAAM,IAAI,EAAE,EAAE;CACnD,MAAM,iBAAiB,SAAS,YAAY,YAAY,KAAK,GAAG;CAChE,MAAM,YAAY,KAAK,MAAM,GAAG,IAAI;CACpC,MAAM,WAAW,UAAU,MAAM,MAAM,IAAI,EAAE,EAAE;CAC/C,MAAM,eAAe,OAAO,UAAU,YAAY,KAAK,GAAG;AAE1D,QAAO,MAAM,OACX,SAAS,OAAO,WAAW,eAAe,EAC1C,SAAS,OAAO,SAAS,aAAa,CACvC;;;;;;;;;;;;;;;;;;;;;AAsBH,MAAa,yBACX,MACA,YACA,YACA,cACA,cACY;CACZ,MAAM,gBAAgB,WAAW,QAAQ,2BAA2B,KAAK;CACzE,MAAM,gBAAgB,aAAa,QAAQ,2BAA2B,KAAK;CAG3E,MAAM,OAAO,aAAa,UAAU,SAAS,IAAI,YAAY,CAAC,cAAc;CAE5E,MAAM,SAAkB,EAAE;AAE1B,MAAK,MAAM,SAAS,qBAAqB,KAAK,EAAE;AAC9C,MAAI,MAAM,kBAAkB,cAAe;AAI3C,MAAI,MAAM,gBAAiB;AAE3B,MAAI,MAAM,UAAU,WAAW,KAAK,OAAQ;AAE5C,MAAI,CAAC,MAAM,UAAU,OAAO,SAAS,UAAU,YAAY,KAAK,OAAO,CACrE;EAKF,MAAM,gBAAgB,MAAM,aAAa,MAAM,WAAW,SAAS;AAEnE,MAAI,MAAM,SAAS,YAAY,cAC7B,QAAO,KAAK,cAAc,MAAM,cAAc,OAAO,cAAc,IAAI,CAAC;MAExE,QAAO,KAAK,cAAc,MAAM,MAAM,OAAO,MAAM,IAAI,CAAC;;AAI5D,QAAO"}
|
|
@@ -1,33 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { collectNamespaceReferences } from "./usageAnalyzer.mjs";
|
|
2
2
|
|
|
3
3
|
//#region src/findKeyAtOffset.ts
|
|
4
4
|
/**
|
|
5
|
-
* Scan `text` for a
|
|
6
|
-
* `offset`.
|
|
7
|
-
*
|
|
5
|
+
* Scan `text` for a caller whose namespace (dictionary key) is addressed at
|
|
6
|
+
* `offset`. Returns the namespace string when the cursor is on the caller
|
|
7
|
+
* name or on the namespace argument itself, `null` otherwise.
|
|
8
8
|
*
|
|
9
|
-
* Handles
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* )
|
|
15
|
-
*
|
|
9
|
+
* Handles every registered caller with a positional or options-object
|
|
10
|
+
* namespace:
|
|
11
|
+
* useIntlayer("my-key") ← cursor on "my-key" or the callee
|
|
12
|
+
* useTranslations("namespace") ← idem
|
|
13
|
+
* getFixedT("en", "namespace") ← cursor on "namespace", not "en"
|
|
14
|
+
* useI18n({ namespace: "ns" }) ← cursor on "ns" or the callee
|
|
15
|
+
* getTranslations({ namespace: "ns" }) ← idem
|
|
16
|
+
*
|
|
17
|
+
* Self callers (`formatMessage`, lingui `t`/`_`) are message *usages*, not
|
|
18
|
+
* namespace declarations — they are resolved by `findMessageUsageAtOffset`.
|
|
16
19
|
*/
|
|
17
20
|
const findKeyAtOffset = (text, offset) => {
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
const start = node["start"];
|
|
25
|
-
const end = node["end"];
|
|
26
|
-
if (offset < start || offset > end) return true;
|
|
27
|
-
result = getFirstStringArg(node);
|
|
28
|
-
return true;
|
|
29
|
-
});
|
|
30
|
-
return result;
|
|
21
|
+
for (const reference of collectNamespaceReferences(text)) {
|
|
22
|
+
const isOnCalleeName = offset >= reference.calleeStart && offset <= reference.calleeEnd;
|
|
23
|
+
const isOnNamespaceArgument = offset >= reference.namespaceStart && offset <= reference.namespaceEnd;
|
|
24
|
+
if (isOnCalleeName || isOnNamespaceArgument) return reference.dictionaryKey;
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
31
27
|
};
|
|
32
28
|
|
|
33
29
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"findKeyAtOffset.mjs","names":[],"sources":["../../src/findKeyAtOffset.ts"],"sourcesContent":["import {
|
|
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;AAClB,MAAK,MAAM,aAAa,2BAA2B,KAAK,EAAE;EACxD,MAAM,iBACJ,UAAU,UAAU,eAAe,UAAU,UAAU;EACzD,MAAM,wBACJ,UAAU,UAAU,kBAAkB,UAAU,UAAU;AAE5D,MAAI,kBAAkB,sBACpB,QAAO,UAAU;;AAIrB,QAAO"}
|