@x18n/lsp 0.0.14 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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.14";
37
+ var version = "0.1.0";
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 ? await this.options.loadProjects(folder, wrap) : (await loadWorkspace({
162
+ (await (this.options.loadProjects ? this.options.loadProjects(folder, wrap) : loadWorkspace({
149
163
  cwd: folder,
150
164
  wrapHost: wrap
151
- }).catch((error) => {
152
- this.connection.window.showWarningMessage(`x18n: ${error instanceof Error ? error.message : String(error)}`);
153
- return { projects: [] };
154
- })).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) => {
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.14",
4
+ "version": "0.1.0",
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.14",
33
- "@x18n/core": "0.0.14",
34
- "@x18n/node": "0.0.14",
35
- "@x18n/ui": "0.0.14",
32
+ "@x18n/builtins": "0.1.0",
33
+ "@x18n/core": "0.1.0",
34
+ "@x18n/node": "0.1.0",
35
+ "@x18n/ui": "0.1.0",
36
36
  "devframe": "^1.2.6",
37
37
  "vscode-languageserver": "^10.1.2",
38
38
  "vscode-languageserver-textdocument": "^1.0.15"