@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 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.13";
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
- const posix = (path) => sep === "\\" ? path.replace(/\\/g, "/") : path;
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 ? await this.options.loadProjects(folder, wrap) : (await loadWorkspace({
162
+ (await (this.options.loadProjects ? this.options.loadProjects(folder, wrap) : loadWorkspace({
147
163
  cwd: folder,
148
164
  wrapHost: wrap
149
- }).catch((error) => {
150
- this.connection.window.showWarningMessage(`x18n: ${error instanceof Error ? error.message : String(error)}`);
151
- return { projects: [] };
152
- })).projects).forEach((item, i) => {
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
- return project.keys().flatMap((key) => {
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 && key.startsWith(`${scopeNs}:`)) insert = key.slice(scopeNs.length + 1);
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.13",
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.13",
33
- "@x18n/core": "0.0.13",
34
- "@x18n/node": "0.0.13",
35
- "@x18n/ui": "0.0.13",
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"