@x18n/lsp 0.0.13 → 0.0.15
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/index.d.mts +63 -0
- package/dist/src.mjs +131 -12
- package/package.json +5 -5
package/dist/index.d.mts
CHANGED
|
@@ -95,6 +95,22 @@ export interface UsageReportResult {
|
|
|
95
95
|
readonly locations: readonly Location[];
|
|
96
96
|
}[];
|
|
97
97
|
}
|
|
98
|
+
export interface SearchParams {
|
|
99
|
+
/** Text of a value in any locale, ignoring case. */
|
|
100
|
+
readonly query: string;
|
|
101
|
+
/** Only values containing the query; by default a fuzzy match also counts. */
|
|
102
|
+
readonly exact?: boolean | undefined;
|
|
103
|
+
}
|
|
104
|
+
export interface SearchMatch {
|
|
105
|
+
readonly project: string;
|
|
106
|
+
readonly key: string;
|
|
107
|
+
/** The best-matching locale, source first on ties. */
|
|
108
|
+
readonly locale: string;
|
|
109
|
+
readonly value: string;
|
|
110
|
+
/** The entry holding the matched value. */
|
|
111
|
+
readonly definition: Location | undefined;
|
|
112
|
+
readonly usages: readonly Location[];
|
|
113
|
+
}
|
|
98
114
|
export interface Decoration {
|
|
99
115
|
readonly range: Range;
|
|
100
116
|
/** The whole call holding the key, when the scanner knows it. */
|
|
@@ -122,6 +138,42 @@ export interface TranslatePromptParams {
|
|
|
122
138
|
readonly locale: string;
|
|
123
139
|
readonly keys?: readonly string[] | undefined;
|
|
124
140
|
}
|
|
141
|
+
export interface ReviewsParams {
|
|
142
|
+
readonly project?: string | undefined;
|
|
143
|
+
readonly key?: string | undefined;
|
|
144
|
+
readonly locale?: string | undefined;
|
|
145
|
+
/** Only unresolved comments, and only threads that have some. */
|
|
146
|
+
readonly open?: boolean | undefined;
|
|
147
|
+
}
|
|
148
|
+
export interface ReviewCommentInfo {
|
|
149
|
+
readonly id: string;
|
|
150
|
+
readonly type: "comment" | "approve" | "request-change" | "suggestion";
|
|
151
|
+
readonly author?: {
|
|
152
|
+
readonly name?: string | undefined;
|
|
153
|
+
readonly email?: string | undefined;
|
|
154
|
+
} | undefined;
|
|
155
|
+
/** ISO 8601. */
|
|
156
|
+
readonly time: string;
|
|
157
|
+
readonly text?: string | undefined;
|
|
158
|
+
readonly suggestion?: string | undefined;
|
|
159
|
+
/** The value an approval approved. */
|
|
160
|
+
readonly value?: string | undefined;
|
|
161
|
+
readonly resolved?: boolean | undefined;
|
|
162
|
+
/** An approval of a value the message no longer has. */
|
|
163
|
+
readonly stale: boolean;
|
|
164
|
+
}
|
|
165
|
+
export interface ReviewThreadInfo {
|
|
166
|
+
readonly project: string;
|
|
167
|
+
readonly key: string;
|
|
168
|
+
readonly locale: string;
|
|
169
|
+
readonly state: "changes-requested" | "approved" | "stale-approval" | "suggested" | "commented" | undefined;
|
|
170
|
+
readonly comments: readonly ReviewCommentInfo[];
|
|
171
|
+
/** The key in the locale's resource; undefined while the locale lacks the message. */
|
|
172
|
+
readonly location: Location | undefined;
|
|
173
|
+
}
|
|
174
|
+
export interface ReviewsResult {
|
|
175
|
+
readonly threads: readonly ReviewThreadInfo[];
|
|
176
|
+
}
|
|
125
177
|
export interface ChangedParams {
|
|
126
178
|
readonly project: string;
|
|
127
179
|
readonly revision: number;
|
|
@@ -134,10 +186,13 @@ export interface Methods {
|
|
|
134
186
|
readonly progress: "x18n/progress";
|
|
135
187
|
readonly fileKeys: "x18n/fileKeys";
|
|
136
188
|
readonly usageReport: "x18n/usageReport";
|
|
189
|
+
readonly search: "x18n/search";
|
|
137
190
|
readonly decorations: "x18n/decorations";
|
|
138
191
|
readonly setDisplayLocale: "x18n/setDisplayLocale";
|
|
139
192
|
readonly openUi: "x18n/openUi";
|
|
140
193
|
readonly translatePrompt: "x18n/translatePrompt";
|
|
194
|
+
readonly reviews: "x18n/reviews";
|
|
195
|
+
readonly explain: "x18n/explain";
|
|
141
196
|
readonly changed: "x18n/changed";
|
|
142
197
|
}
|
|
143
198
|
export declare const methods: Methods;
|
|
@@ -149,6 +204,12 @@ export interface Commands {
|
|
|
149
204
|
readonly extract: "x18n.extract";
|
|
150
205
|
/** `{ uri: <folder> }` -> `{ files: { path, content }[], notes, removable }`; nothing is written. */
|
|
151
206
|
readonly migrateI18nAlly: "x18n.migrateI18nAlly";
|
|
207
|
+
/** `{ project?, key, locale, review, text?, suggestion? }`: adds a review comment, written to disk at once. */
|
|
208
|
+
readonly review: "x18n.review";
|
|
209
|
+
/** `{ project?, id }` */
|
|
210
|
+
readonly resolveReview: "x18n.resolveReview";
|
|
211
|
+
/** `{ project?, id }`: sets the suggested value and resolves the suggestion. */
|
|
212
|
+
readonly applySuggestion: "x18n.applySuggestion";
|
|
152
213
|
}
|
|
153
214
|
export declare const commands: Commands;
|
|
154
215
|
//#endregion
|
|
@@ -159,6 +220,8 @@ interface ServerProject {
|
|
|
159
220
|
readonly root: string;
|
|
160
221
|
readonly project: Project;
|
|
161
222
|
readonly overlay: OverlayHost;
|
|
223
|
+
/** Absolute path of the config file, when there is one. */
|
|
224
|
+
readonly configFile?: string | undefined;
|
|
162
225
|
}
|
|
163
226
|
interface ServerOptions {
|
|
164
227
|
/** Loads the projects of a workspace folder. Default: `@x18n/node` with an overlay host. */
|
package/dist/src.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { relative, resolve, sep } from "node:path";
|
|
2
2
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
3
|
import { extractItems, findCandidates } from "@x18n/builtins/extractors";
|
|
4
|
-
import { createLineIndex, createOverlayHost, isX18nError, renderPrompt } from "@x18n/core";
|
|
4
|
+
import { createLineIndex, createOverlayHost, isX18nError, joinKey, renderExplanation, renderPrompt, splitKey, toErrorInfo } from "@x18n/core";
|
|
5
5
|
import { loadWorkspace, migrateI18nAlly } from "@x18n/node";
|
|
6
6
|
import { CodeActionKind, CompletionItemKind, DiagnosticSeverity, DidChangeWatchedFilesNotification, FileChangeType, ResponseError, SymbolKind, TextDocumentSyncKind, TextDocuments } from "vscode-languageserver";
|
|
7
7
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
@@ -13,10 +13,13 @@ const methods = {
|
|
|
13
13
|
progress: "x18n/progress",
|
|
14
14
|
fileKeys: "x18n/fileKeys",
|
|
15
15
|
usageReport: "x18n/usageReport",
|
|
16
|
+
search: "x18n/search",
|
|
16
17
|
decorations: "x18n/decorations",
|
|
17
18
|
setDisplayLocale: "x18n/setDisplayLocale",
|
|
18
19
|
openUi: "x18n/openUi",
|
|
19
20
|
translatePrompt: "x18n/translatePrompt",
|
|
21
|
+
reviews: "x18n/reviews",
|
|
22
|
+
explain: "x18n/explain",
|
|
20
23
|
changed: "x18n/changed"
|
|
21
24
|
};
|
|
22
25
|
const commands = {
|
|
@@ -24,13 +27,22 @@ const commands = {
|
|
|
24
27
|
delete: "x18n.delete",
|
|
25
28
|
renamePrefix: "x18n.renamePrefix",
|
|
26
29
|
extract: "x18n.extract",
|
|
27
|
-
migrateI18nAlly: "x18n.migrateI18nAlly"
|
|
30
|
+
migrateI18nAlly: "x18n.migrateI18nAlly",
|
|
31
|
+
review: "x18n.review",
|
|
32
|
+
resolveReview: "x18n.resolveReview",
|
|
33
|
+
applySuggestion: "x18n.applySuggestion"
|
|
28
34
|
};
|
|
29
35
|
//#endregion
|
|
30
36
|
//#region package.json
|
|
31
|
-
var version = "0.0.
|
|
37
|
+
var version = "0.0.15";
|
|
32
38
|
//#endregion
|
|
33
39
|
//#region src/server.ts
|
|
40
|
+
const reviewTypes = [
|
|
41
|
+
"comment",
|
|
42
|
+
"approve",
|
|
43
|
+
"request-change",
|
|
44
|
+
"suggestion"
|
|
45
|
+
];
|
|
34
46
|
const severities = {
|
|
35
47
|
error: DiagnosticSeverity.Error,
|
|
36
48
|
warning: DiagnosticSeverity.Warning,
|
|
@@ -61,12 +73,16 @@ function changedAt(change) {
|
|
|
61
73
|
change.author
|
|
62
74
|
].filter(Boolean).join(" · ");
|
|
63
75
|
}
|
|
64
|
-
|
|
76
|
+
function posix(path) {
|
|
77
|
+
return sep === "\\" ? path.replace(/\\/g, "/") : path;
|
|
78
|
+
}
|
|
65
79
|
var Server = class {
|
|
66
80
|
connection;
|
|
67
81
|
options;
|
|
68
82
|
documents = new TextDocuments(TextDocument);
|
|
69
83
|
projects = [];
|
|
84
|
+
/** Workspace folders whose projects could not be loaded. */
|
|
85
|
+
failures = [];
|
|
70
86
|
published = /* @__PURE__ */ new Map();
|
|
71
87
|
timers = /* @__PURE__ */ new Map();
|
|
72
88
|
settings = {
|
|
@@ -143,22 +159,29 @@ var Server = class {
|
|
|
143
159
|
overlays.push(overlay);
|
|
144
160
|
return overlay;
|
|
145
161
|
};
|
|
146
|
-
(this.options.loadProjects ?
|
|
162
|
+
(await (this.options.loadProjects ? this.options.loadProjects(folder, wrap) : loadWorkspace({
|
|
147
163
|
cwd: folder,
|
|
148
164
|
wrapHost: wrap
|
|
149
|
-
}).catch((error) => {
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
165
|
+
}).then((workspace) => workspace.projects)).catch((error) => {
|
|
166
|
+
const info = toErrorInfo(error);
|
|
167
|
+
this.failures.push({
|
|
168
|
+
folder,
|
|
169
|
+
error: info
|
|
170
|
+
});
|
|
171
|
+
this.connection.window.showWarningMessage(`x18n: ${info.why}`);
|
|
172
|
+
return [];
|
|
173
|
+
})).forEach((item, i) => {
|
|
153
174
|
const overlay = overlays[i];
|
|
154
175
|
if (overlay && !this.projects.some((p) => p.root === item.root)) this.projects.push({
|
|
155
176
|
name: item.name,
|
|
156
177
|
root: item.root,
|
|
157
178
|
project: item.project,
|
|
158
|
-
overlay
|
|
179
|
+
overlay,
|
|
180
|
+
configFile: item.configFile
|
|
159
181
|
});
|
|
160
182
|
});
|
|
161
183
|
}
|
|
184
|
+
if (this.projects.every((entry) => entry.project.resources.length === 0)) this.connection.console.info(`No locale files found in the workspace.\n\n${this.explain()}`);
|
|
162
185
|
for (const entry of this.projects) {
|
|
163
186
|
entry.project.onChange((change) => {
|
|
164
187
|
this.connection.sendNotification(methods.changed, {
|
|
@@ -414,6 +437,11 @@ var Server = class {
|
|
|
414
437
|
const flag = this.settings.showFlags ? project.localeInfo(locale).flag : void 0;
|
|
415
438
|
lines.push(`| ${flag ?? ""} ${locale} | ${value === void 0 ? "_missing_" : value === "" ? "_empty_" : value.replace(/\|/g, "\\|").replace(/\n/g, " ")} |${changed}`);
|
|
416
439
|
});
|
|
440
|
+
const threads = project.reviews({
|
|
441
|
+
key: target.key,
|
|
442
|
+
open: true
|
|
443
|
+
});
|
|
444
|
+
if (threads.length > 0) lines.push("", `**Reviews** ${threads.map((t) => `${t.locale}: ${t.state.replace("-", " ")}`).join(" · ")}`);
|
|
417
445
|
}
|
|
418
446
|
const usages = target.type === "subtree" ? project.usages({ prefix: target.key }) : project.usages({ key: target.key });
|
|
419
447
|
const args = encodeURIComponent(JSON.stringify([{
|
|
@@ -461,9 +489,16 @@ var Server = class {
|
|
|
461
489
|
const scopeNs = usage?.scopeNamespace ?? project.config.namespace.default;
|
|
462
490
|
const prefix = usage?.scopePrefix ? `${usage.scopePrefix}.` : "";
|
|
463
491
|
const range = this.range(at.entry, at.path, slot);
|
|
464
|
-
|
|
492
|
+
const { separator, fallback } = project.config.namespace;
|
|
493
|
+
const keys = project.keys();
|
|
494
|
+
const known = new Set(keys);
|
|
495
|
+
return keys.flatMap((key) => {
|
|
465
496
|
let insert = key;
|
|
466
|
-
if (project.namespaced
|
|
497
|
+
if (project.namespaced) {
|
|
498
|
+
const { namespace, path } = splitKey(key);
|
|
499
|
+
const bare = path.join(".");
|
|
500
|
+
insert = namespace === scopeNs || namespace !== void 0 && fallback.includes(namespace) && !known.has(joinKey(scopeNs, path)) || namespace === void 0 ? bare : `${namespace}${separator}${bare}`;
|
|
501
|
+
}
|
|
467
502
|
if (prefix) {
|
|
468
503
|
if (!insert.startsWith(prefix)) return [];
|
|
469
504
|
insert = insert.slice(prefix.length);
|
|
@@ -630,6 +665,7 @@ var Server = class {
|
|
|
630
665
|
const arg = params.arguments?.[0];
|
|
631
666
|
if (typeof arg !== "object" || arg === null) return null;
|
|
632
667
|
const input = arg;
|
|
668
|
+
if (params.command === commands.review || params.command === commands.resolveReview || params.command === commands.applySuggestion) return this.reviewCommand(params.command, input);
|
|
633
669
|
if (params.command === commands.extract && typeof input.uri === "string") return this.extractFile(input.uri);
|
|
634
670
|
if (params.command === commands.migrateI18nAlly && typeof input.uri === "string") return migrateI18nAlly(fileURLToPath(input.uri));
|
|
635
671
|
const entry = this.projects.find((p) => p.name === input.project) ?? this.projects[0];
|
|
@@ -657,6 +693,50 @@ var Server = class {
|
|
|
657
693
|
await this.connection.workspace.applyEdit(edit);
|
|
658
694
|
return edit;
|
|
659
695
|
}
|
|
696
|
+
/** Review commands write at once: a comment is not an edit to review in the editor. */
|
|
697
|
+
async reviewCommand(command, input) {
|
|
698
|
+
const entry = this.entryByName(input.project);
|
|
699
|
+
if (!entry) return null;
|
|
700
|
+
const { project } = entry;
|
|
701
|
+
const review = reviewTypes.find((type) => type === input.review);
|
|
702
|
+
let op;
|
|
703
|
+
if (command === commands.review && typeof input.key === "string" && typeof input.locale === "string" && review) op = {
|
|
704
|
+
type: "review",
|
|
705
|
+
key: project.parseKey(input.key),
|
|
706
|
+
locale: input.locale,
|
|
707
|
+
review,
|
|
708
|
+
text: typeof input.text === "string" ? input.text : void 0,
|
|
709
|
+
suggestion: typeof input.suggestion === "string" ? input.suggestion : void 0,
|
|
710
|
+
author: await project.reviewAuthor()
|
|
711
|
+
};
|
|
712
|
+
else if (typeof input.id === "string") op = command === commands.resolveReview ? {
|
|
713
|
+
type: "resolve-review",
|
|
714
|
+
id: input.id
|
|
715
|
+
} : {
|
|
716
|
+
type: "apply-suggestion",
|
|
717
|
+
id: input.id
|
|
718
|
+
};
|
|
719
|
+
if (!op) return null;
|
|
720
|
+
try {
|
|
721
|
+
return { revision: (await project.apply(this.plan(project, [op]))).revision };
|
|
722
|
+
} catch (error) {
|
|
723
|
+
throw isX18nError(error) ? new ResponseError(1, `${error.message}. ${error.fix}`) : error;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
reviews(params) {
|
|
727
|
+
return { threads: (params.project === void 0 ? this.projects : this.projects.filter((p) => p.name === params.project)).flatMap((entry) => entry.project.reviews({
|
|
728
|
+
key: params.key === void 0 ? void 0 : entry.project.parseKey(params.key),
|
|
729
|
+
locale: params.locale,
|
|
730
|
+
open: params.open
|
|
731
|
+
}).map((thread) => {
|
|
732
|
+
const node = entry.project.locations(thread.key).find((n) => n.locale === thread.locale && n.leaf);
|
|
733
|
+
return {
|
|
734
|
+
...thread,
|
|
735
|
+
project: entry.name,
|
|
736
|
+
location: node && this.entryLocation(entry, node)
|
|
737
|
+
};
|
|
738
|
+
})) };
|
|
739
|
+
}
|
|
660
740
|
async extractFile(uri) {
|
|
661
741
|
const found = this.locate(uri);
|
|
662
742
|
if (!found) return null;
|
|
@@ -674,6 +754,15 @@ var Server = class {
|
|
|
674
754
|
entryByName(name) {
|
|
675
755
|
return name === void 0 ? this.projects[0] : this.projects.find((p) => p.name === name);
|
|
676
756
|
}
|
|
757
|
+
/** `x18n doctor` for every folder: load errors, then how each project was understood. */
|
|
758
|
+
explain() {
|
|
759
|
+
if (this.folders.length === 0) return "No workspace folder is open.";
|
|
760
|
+
return [...this.failures.map(({ folder, error }) => [
|
|
761
|
+
`# ${folder}`,
|
|
762
|
+
`error: ${error.name} ${error.why}`,
|
|
763
|
+
...error.fix ? [`fix: ${error.fix}`] : []
|
|
764
|
+
].join("\n")), ...this.projects.map((entry) => renderExplanation(entry.project.explain(), entry.configFile))].join("\n\n");
|
|
765
|
+
}
|
|
677
766
|
projectInfos() {
|
|
678
767
|
return this.projects.map((entry) => ({
|
|
679
768
|
name: entry.name,
|
|
@@ -805,6 +894,24 @@ var Server = class {
|
|
|
805
894
|
missing
|
|
806
895
|
};
|
|
807
896
|
}
|
|
897
|
+
/** Matches of every project, best first. */
|
|
898
|
+
search(params) {
|
|
899
|
+
return this.projects.flatMap((entry) => entry.project.search(params.query, { exact: params.exact }).map((match) => ({
|
|
900
|
+
entry,
|
|
901
|
+
match
|
|
902
|
+
}))).sort((a, b) => b.match.score - a.match.score).slice(0, 100).map(({ entry, match }) => {
|
|
903
|
+
const { project } = entry;
|
|
904
|
+
const node = project.locations(match.key).find((node) => node.locale === match.locale);
|
|
905
|
+
return {
|
|
906
|
+
project: entry.name,
|
|
907
|
+
key: match.key,
|
|
908
|
+
locale: match.locale,
|
|
909
|
+
value: match.value,
|
|
910
|
+
definition: node && this.entryLocation(entry, node),
|
|
911
|
+
usages: project.usages({ key: match.key }).map((m) => this.location(entry, m.usage.path, m.usage.range))
|
|
912
|
+
};
|
|
913
|
+
});
|
|
914
|
+
}
|
|
808
915
|
async decorations(params) {
|
|
809
916
|
await this.ready;
|
|
810
917
|
const found = this.locate(params.uri);
|
|
@@ -876,6 +983,10 @@ function setupServer(connection, options = {}) {
|
|
|
876
983
|
await server.ready;
|
|
877
984
|
return server.usageReport();
|
|
878
985
|
});
|
|
986
|
+
connection.onRequest(methods.search, async (params) => {
|
|
987
|
+
await server.ready;
|
|
988
|
+
return server.search(params);
|
|
989
|
+
});
|
|
879
990
|
connection.onRequest(methods.decorations, (params) => server.decorations(params));
|
|
880
991
|
connection.onNotification(methods.setDisplayLocale, (params) => {
|
|
881
992
|
server.settings.displayLocale = params.locale;
|
|
@@ -887,6 +998,14 @@ function setupServer(connection, options = {}) {
|
|
|
887
998
|
if (!entry) throw new ResponseError(1, `No project "${params?.project ?? ""}" in this server.`);
|
|
888
999
|
return openUi(entry);
|
|
889
1000
|
});
|
|
1001
|
+
connection.onRequest(methods.reviews, async (params) => {
|
|
1002
|
+
await server.ready;
|
|
1003
|
+
return server.reviews(params ?? {});
|
|
1004
|
+
});
|
|
1005
|
+
connection.onRequest(methods.explain, async () => {
|
|
1006
|
+
await server.ready;
|
|
1007
|
+
return server.explain();
|
|
1008
|
+
});
|
|
890
1009
|
connection.onRequest(methods.translatePrompt, async (params) => {
|
|
891
1010
|
await server.ready;
|
|
892
1011
|
const entry = server.entryByName(params.project);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x18n/lsp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.15",
|
|
5
5
|
"description": "x18n language server for any LSP editor",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
@@ -29,10 +29,10 @@
|
|
|
29
29
|
"node": ">=22"
|
|
30
30
|
},
|
|
31
31
|
"dependencies": {
|
|
32
|
-
"@x18n/builtins": "0.0.
|
|
33
|
-
"@x18n/core": "0.0.
|
|
34
|
-
"@x18n/node": "0.0.
|
|
35
|
-
"@x18n/ui": "0.0.
|
|
32
|
+
"@x18n/builtins": "0.0.15",
|
|
33
|
+
"@x18n/core": "0.0.15",
|
|
34
|
+
"@x18n/node": "0.0.15",
|
|
35
|
+
"@x18n/ui": "0.0.15",
|
|
36
36
|
"devframe": "^1.2.6",
|
|
37
37
|
"vscode-languageserver": "^10.1.2",
|
|
38
38
|
"vscode-languageserver-textdocument": "^1.0.15"
|