@x18n/lsp 0.0.14 → 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 +119 -9
- 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, joinKey, renderPrompt, splitKey } 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,
|
|
@@ -69,6 +81,8 @@ var Server = class {
|
|
|
69
81
|
options;
|
|
70
82
|
documents = new TextDocuments(TextDocument);
|
|
71
83
|
projects = [];
|
|
84
|
+
/** Workspace folders whose projects could not be loaded. */
|
|
85
|
+
failures = [];
|
|
72
86
|
published = /* @__PURE__ */ new Map();
|
|
73
87
|
timers = /* @__PURE__ */ new Map();
|
|
74
88
|
settings = {
|
|
@@ -145,22 +159,29 @@ var Server = class {
|
|
|
145
159
|
overlays.push(overlay);
|
|
146
160
|
return overlay;
|
|
147
161
|
};
|
|
148
|
-
(this.options.loadProjects ?
|
|
162
|
+
(await (this.options.loadProjects ? this.options.loadProjects(folder, wrap) : loadWorkspace({
|
|
149
163
|
cwd: folder,
|
|
150
164
|
wrapHost: wrap
|
|
151
|
-
}).catch((error) => {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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) => {
|
|
155
174
|
const overlay = overlays[i];
|
|
156
175
|
if (overlay && !this.projects.some((p) => p.root === item.root)) this.projects.push({
|
|
157
176
|
name: item.name,
|
|
158
177
|
root: item.root,
|
|
159
178
|
project: item.project,
|
|
160
|
-
overlay
|
|
179
|
+
overlay,
|
|
180
|
+
configFile: item.configFile
|
|
161
181
|
});
|
|
162
182
|
});
|
|
163
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()}`);
|
|
164
185
|
for (const entry of this.projects) {
|
|
165
186
|
entry.project.onChange((change) => {
|
|
166
187
|
this.connection.sendNotification(methods.changed, {
|
|
@@ -416,6 +437,11 @@ var Server = class {
|
|
|
416
437
|
const flag = this.settings.showFlags ? project.localeInfo(locale).flag : void 0;
|
|
417
438
|
lines.push(`| ${flag ?? ""} ${locale} | ${value === void 0 ? "_missing_" : value === "" ? "_empty_" : value.replace(/\|/g, "\\|").replace(/\n/g, " ")} |${changed}`);
|
|
418
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(" · ")}`);
|
|
419
445
|
}
|
|
420
446
|
const usages = target.type === "subtree" ? project.usages({ prefix: target.key }) : project.usages({ key: target.key });
|
|
421
447
|
const args = encodeURIComponent(JSON.stringify([{
|
|
@@ -639,6 +665,7 @@ var Server = class {
|
|
|
639
665
|
const arg = params.arguments?.[0];
|
|
640
666
|
if (typeof arg !== "object" || arg === null) return null;
|
|
641
667
|
const input = arg;
|
|
668
|
+
if (params.command === commands.review || params.command === commands.resolveReview || params.command === commands.applySuggestion) return this.reviewCommand(params.command, input);
|
|
642
669
|
if (params.command === commands.extract && typeof input.uri === "string") return this.extractFile(input.uri);
|
|
643
670
|
if (params.command === commands.migrateI18nAlly && typeof input.uri === "string") return migrateI18nAlly(fileURLToPath(input.uri));
|
|
644
671
|
const entry = this.projects.find((p) => p.name === input.project) ?? this.projects[0];
|
|
@@ -666,6 +693,50 @@ var Server = class {
|
|
|
666
693
|
await this.connection.workspace.applyEdit(edit);
|
|
667
694
|
return edit;
|
|
668
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
|
+
}
|
|
669
740
|
async extractFile(uri) {
|
|
670
741
|
const found = this.locate(uri);
|
|
671
742
|
if (!found) return null;
|
|
@@ -683,6 +754,15 @@ var Server = class {
|
|
|
683
754
|
entryByName(name) {
|
|
684
755
|
return name === void 0 ? this.projects[0] : this.projects.find((p) => p.name === name);
|
|
685
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
|
+
}
|
|
686
766
|
projectInfos() {
|
|
687
767
|
return this.projects.map((entry) => ({
|
|
688
768
|
name: entry.name,
|
|
@@ -814,6 +894,24 @@ var Server = class {
|
|
|
814
894
|
missing
|
|
815
895
|
};
|
|
816
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
|
+
}
|
|
817
915
|
async decorations(params) {
|
|
818
916
|
await this.ready;
|
|
819
917
|
const found = this.locate(params.uri);
|
|
@@ -885,6 +983,10 @@ function setupServer(connection, options = {}) {
|
|
|
885
983
|
await server.ready;
|
|
886
984
|
return server.usageReport();
|
|
887
985
|
});
|
|
986
|
+
connection.onRequest(methods.search, async (params) => {
|
|
987
|
+
await server.ready;
|
|
988
|
+
return server.search(params);
|
|
989
|
+
});
|
|
888
990
|
connection.onRequest(methods.decorations, (params) => server.decorations(params));
|
|
889
991
|
connection.onNotification(methods.setDisplayLocale, (params) => {
|
|
890
992
|
server.settings.displayLocale = params.locale;
|
|
@@ -896,6 +998,14 @@ function setupServer(connection, options = {}) {
|
|
|
896
998
|
if (!entry) throw new ResponseError(1, `No project "${params?.project ?? ""}" in this server.`);
|
|
897
999
|
return openUi(entry);
|
|
898
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
|
+
});
|
|
899
1009
|
connection.onRequest(methods.translatePrompt, async (params) => {
|
|
900
1010
|
await server.ready;
|
|
901
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"
|