@x18n/lsp 0.0.2 → 0.0.5
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/cli.d.mts +1 -0
- package/dist/cli.mjs +8 -0
- package/dist/index.d.mts +170 -0
- package/dist/index.mjs +1 -0
- package/dist/src.mjs +894 -0
- package/dist/ui.mjs +51 -0
- package/package.json +10 -10
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {}
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { t as setupServer } from "./src.mjs";
|
|
2
|
+
import { ProposedFeatures, createConnection } from "vscode-languageserver/node";
|
|
3
|
+
//#region src/cli.ts
|
|
4
|
+
const connection = createConnection(ProposedFeatures.all);
|
|
5
|
+
setupServer(connection);
|
|
6
|
+
connection.listen();
|
|
7
|
+
//#endregion
|
|
8
|
+
export {};
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { OverlayHost, Project } from "@x18n/core";
|
|
2
|
+
import { NodeHost } from "@x18n/node";
|
|
3
|
+
import { Connection } from "vscode-languageserver";
|
|
4
|
+
//#region src/protocol.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* The `x18n/*` custom protocol. Shapes are versioned with the JSON contract (`contractVersion` in `@x18n/core`).
|
|
7
|
+
* Positions and ranges use LSP conventions (zero-based lines, UTF-16 characters).
|
|
8
|
+
*/
|
|
9
|
+
export interface Position {
|
|
10
|
+
readonly line: number;
|
|
11
|
+
readonly character: number;
|
|
12
|
+
}
|
|
13
|
+
export interface Range {
|
|
14
|
+
readonly start: Position;
|
|
15
|
+
readonly end: Position;
|
|
16
|
+
}
|
|
17
|
+
export interface Location {
|
|
18
|
+
readonly uri: string;
|
|
19
|
+
readonly range: Range;
|
|
20
|
+
}
|
|
21
|
+
export interface DocumentParams {
|
|
22
|
+
readonly uri: string;
|
|
23
|
+
}
|
|
24
|
+
export interface PositionParams extends DocumentParams {
|
|
25
|
+
readonly position: Position;
|
|
26
|
+
}
|
|
27
|
+
export interface ProjectInfo {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly root: string;
|
|
30
|
+
readonly sourceLocale: string;
|
|
31
|
+
readonly locales: readonly {
|
|
32
|
+
readonly code: string;
|
|
33
|
+
readonly displayName: string | undefined;
|
|
34
|
+
readonly flag: string | undefined;
|
|
35
|
+
}[];
|
|
36
|
+
readonly displayLocale: string;
|
|
37
|
+
readonly keys: number;
|
|
38
|
+
}
|
|
39
|
+
export interface KeyAtResult {
|
|
40
|
+
readonly project: string;
|
|
41
|
+
readonly key: string;
|
|
42
|
+
readonly type: "key" | "subtree";
|
|
43
|
+
readonly range: Range;
|
|
44
|
+
}
|
|
45
|
+
export interface TreeParams {
|
|
46
|
+
readonly project?: string | undefined;
|
|
47
|
+
/** Parent key; omitted for the roots. */
|
|
48
|
+
readonly parent?: string | undefined;
|
|
49
|
+
}
|
|
50
|
+
export interface TreeNode {
|
|
51
|
+
readonly key: string;
|
|
52
|
+
readonly label: string;
|
|
53
|
+
readonly leaf: boolean;
|
|
54
|
+
readonly translated: number;
|
|
55
|
+
readonly total: number;
|
|
56
|
+
/** Display-locale value for leaves. */
|
|
57
|
+
readonly value: string | undefined;
|
|
58
|
+
}
|
|
59
|
+
export interface LocaleProgress {
|
|
60
|
+
readonly locale: string;
|
|
61
|
+
readonly translated: number;
|
|
62
|
+
readonly total: number;
|
|
63
|
+
readonly missing: readonly string[];
|
|
64
|
+
readonly empty: readonly string[];
|
|
65
|
+
}
|
|
66
|
+
export interface ProgressResult {
|
|
67
|
+
readonly projects: readonly {
|
|
68
|
+
readonly name: string;
|
|
69
|
+
readonly locales: readonly LocaleProgress[];
|
|
70
|
+
}[];
|
|
71
|
+
}
|
|
72
|
+
export interface FileKey {
|
|
73
|
+
readonly key: string;
|
|
74
|
+
readonly range: Range;
|
|
75
|
+
readonly value: string | undefined;
|
|
76
|
+
readonly missing: boolean;
|
|
77
|
+
readonly dynamic: boolean;
|
|
78
|
+
}
|
|
79
|
+
export interface FileKeysResult {
|
|
80
|
+
readonly keys: readonly FileKey[];
|
|
81
|
+
readonly hardStrings: readonly {
|
|
82
|
+
readonly text: string;
|
|
83
|
+
readonly range: Range;
|
|
84
|
+
}[];
|
|
85
|
+
}
|
|
86
|
+
export interface UsageReportResult {
|
|
87
|
+
readonly unused: readonly {
|
|
88
|
+
readonly project: string;
|
|
89
|
+
readonly key: string;
|
|
90
|
+
readonly location: Location | undefined;
|
|
91
|
+
}[];
|
|
92
|
+
readonly missing: readonly {
|
|
93
|
+
readonly project: string;
|
|
94
|
+
readonly key: string;
|
|
95
|
+
readonly locations: readonly Location[];
|
|
96
|
+
}[];
|
|
97
|
+
}
|
|
98
|
+
export interface Decoration {
|
|
99
|
+
readonly range: Range;
|
|
100
|
+
readonly key: string;
|
|
101
|
+
readonly value: string | undefined;
|
|
102
|
+
readonly state: "ok" | "missing" | "dynamic";
|
|
103
|
+
}
|
|
104
|
+
export interface DecorationsResult {
|
|
105
|
+
readonly locale: string;
|
|
106
|
+
readonly decorations: readonly Decoration[];
|
|
107
|
+
}
|
|
108
|
+
export interface SetDisplayLocaleParams {
|
|
109
|
+
readonly locale: string | undefined;
|
|
110
|
+
}
|
|
111
|
+
export interface OpenUiParams {
|
|
112
|
+
readonly project?: string | undefined;
|
|
113
|
+
}
|
|
114
|
+
export interface OpenUiResult {
|
|
115
|
+
readonly url: string;
|
|
116
|
+
readonly token: string;
|
|
117
|
+
}
|
|
118
|
+
export interface TranslatePromptParams {
|
|
119
|
+
readonly project?: string | undefined;
|
|
120
|
+
readonly locale: string;
|
|
121
|
+
readonly keys?: readonly string[] | undefined;
|
|
122
|
+
}
|
|
123
|
+
export interface ChangedParams {
|
|
124
|
+
readonly project: string;
|
|
125
|
+
readonly revision: number;
|
|
126
|
+
readonly keys: readonly string[];
|
|
127
|
+
}
|
|
128
|
+
export interface Methods {
|
|
129
|
+
readonly projects: "x18n/projects";
|
|
130
|
+
readonly keyAt: "x18n/keyAt";
|
|
131
|
+
readonly tree: "x18n/tree";
|
|
132
|
+
readonly progress: "x18n/progress";
|
|
133
|
+
readonly fileKeys: "x18n/fileKeys";
|
|
134
|
+
readonly usageReport: "x18n/usageReport";
|
|
135
|
+
readonly decorations: "x18n/decorations";
|
|
136
|
+
readonly setDisplayLocale: "x18n/setDisplayLocale";
|
|
137
|
+
readonly openUi: "x18n/openUi";
|
|
138
|
+
readonly translatePrompt: "x18n/translatePrompt";
|
|
139
|
+
readonly changed: "x18n/changed";
|
|
140
|
+
}
|
|
141
|
+
export declare const methods: Methods;
|
|
142
|
+
/** Commands for `workspace/executeCommand`. */
|
|
143
|
+
export interface Commands {
|
|
144
|
+
readonly set: "x18n.set";
|
|
145
|
+
readonly delete: "x18n.delete";
|
|
146
|
+
readonly renamePrefix: "x18n.renamePrefix";
|
|
147
|
+
readonly extract: "x18n.extract";
|
|
148
|
+
/** `{ uri: <folder> }` -> `{ files: { path, content }[], notes, removable }`; nothing is written. */
|
|
149
|
+
readonly migrateI18nAlly: "x18n.migrateI18nAlly";
|
|
150
|
+
}
|
|
151
|
+
export declare const commands: Commands;
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/server.d.ts
|
|
154
|
+
interface ServerProject {
|
|
155
|
+
readonly name: string;
|
|
156
|
+
/** Absolute root. */
|
|
157
|
+
readonly root: string;
|
|
158
|
+
readonly project: Project;
|
|
159
|
+
readonly overlay: OverlayHost;
|
|
160
|
+
}
|
|
161
|
+
interface ServerOptions {
|
|
162
|
+
/** Loads the projects of a workspace folder. Default: `@x18n/node` with an overlay host. */
|
|
163
|
+
readonly loadProjects?: ((folder: string, wrapHost: (host: NodeHost) => OverlayHost) => Promise<readonly Omit<ServerProject, "overlay">[]>) | undefined;
|
|
164
|
+
/** Starts the UI server for a project (`x18n/openUi`). */
|
|
165
|
+
readonly openUi?: ((project: ServerProject) => Promise<OpenUiResult>) | undefined;
|
|
166
|
+
}
|
|
167
|
+
/** Registers the x18n handlers on a connection. The caller owns transport and `listen()`. */
|
|
168
|
+
export declare function setupServer(connection: Connection, options?: ServerOptions): void;
|
|
169
|
+
//#endregion
|
|
170
|
+
export type { ServerOptions, ServerProject };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { n as commands, r as methods, t as setupServer } from "./src.mjs";
|
package/dist/src.mjs
ADDED
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
import { relative, resolve, sep } from "node:path";
|
|
2
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3
|
+
import { extractItems, findCandidates } from "@x18n/builtins/extractors";
|
|
4
|
+
import { createLineIndex, createOverlayHost, isX18nError, renderPrompt } from "@x18n/core";
|
|
5
|
+
import { loadWorkspace, migrateI18nAlly } from "@x18n/node";
|
|
6
|
+
import { CodeActionKind, CompletionItemKind, DiagnosticSeverity, DidChangeWatchedFilesNotification, FileChangeType, ResponseError, SymbolKind, TextDocumentSyncKind, TextDocuments } from "vscode-languageserver";
|
|
7
|
+
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
8
|
+
//#region src/protocol.ts
|
|
9
|
+
const methods = {
|
|
10
|
+
projects: "x18n/projects",
|
|
11
|
+
keyAt: "x18n/keyAt",
|
|
12
|
+
tree: "x18n/tree",
|
|
13
|
+
progress: "x18n/progress",
|
|
14
|
+
fileKeys: "x18n/fileKeys",
|
|
15
|
+
usageReport: "x18n/usageReport",
|
|
16
|
+
decorations: "x18n/decorations",
|
|
17
|
+
setDisplayLocale: "x18n/setDisplayLocale",
|
|
18
|
+
openUi: "x18n/openUi",
|
|
19
|
+
translatePrompt: "x18n/translatePrompt",
|
|
20
|
+
changed: "x18n/changed"
|
|
21
|
+
};
|
|
22
|
+
const commands = {
|
|
23
|
+
set: "x18n.set",
|
|
24
|
+
delete: "x18n.delete",
|
|
25
|
+
renamePrefix: "x18n.renamePrefix",
|
|
26
|
+
extract: "x18n.extract",
|
|
27
|
+
migrateI18nAlly: "x18n.migrateI18nAlly"
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
//#region package.json
|
|
31
|
+
var version = "0.0.5";
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/server.ts
|
|
34
|
+
const severities = {
|
|
35
|
+
error: DiagnosticSeverity.Error,
|
|
36
|
+
warning: DiagnosticSeverity.Warning,
|
|
37
|
+
info: DiagnosticSeverity.Information,
|
|
38
|
+
hint: DiagnosticSeverity.Hint
|
|
39
|
+
};
|
|
40
|
+
function truncate(text, max) {
|
|
41
|
+
return text.length > max ? `${text.slice(0, max - 1)}…` : text;
|
|
42
|
+
}
|
|
43
|
+
function display(value) {
|
|
44
|
+
return value === void 0 ? void 0 : typeof value === "string" ? value : JSON.stringify(value);
|
|
45
|
+
}
|
|
46
|
+
const timeUnits = [
|
|
47
|
+
["year", 31536e6],
|
|
48
|
+
["month", 2592e6],
|
|
49
|
+
["week", 6048e5],
|
|
50
|
+
["day", 864e5],
|
|
51
|
+
["hour", 36e5],
|
|
52
|
+
["minute", 6e4]
|
|
53
|
+
];
|
|
54
|
+
function changedAt(change) {
|
|
55
|
+
if (!Number.isFinite(change.updatedAt)) return "changed locally";
|
|
56
|
+
const diff = change.updatedAt - Date.now();
|
|
57
|
+
const [unit, size] = timeUnits.find(([, size]) => Math.abs(diff) >= size) ?? ["minute", 6e4];
|
|
58
|
+
return [
|
|
59
|
+
new Intl.RelativeTimeFormat("en", { numeric: "auto" }).format(Math.round(diff / size), unit),
|
|
60
|
+
change.revision?.slice(0, 7),
|
|
61
|
+
change.author
|
|
62
|
+
].filter(Boolean).join(" · ");
|
|
63
|
+
}
|
|
64
|
+
const posix = (path) => sep === "\\" ? path.replace(/\\/g, "/") : path;
|
|
65
|
+
var Server = class {
|
|
66
|
+
connection;
|
|
67
|
+
options;
|
|
68
|
+
documents = new TextDocuments(TextDocument);
|
|
69
|
+
projects = [];
|
|
70
|
+
published = /* @__PURE__ */ new Map();
|
|
71
|
+
timers = /* @__PURE__ */ new Map();
|
|
72
|
+
settings = {
|
|
73
|
+
annotationMaxLength: 40,
|
|
74
|
+
displayLocale: void 0,
|
|
75
|
+
codeLens: false
|
|
76
|
+
};
|
|
77
|
+
ready = Promise.resolve();
|
|
78
|
+
folders = [];
|
|
79
|
+
watchRegistration = false;
|
|
80
|
+
constructor(connection, options) {
|
|
81
|
+
this.connection = connection;
|
|
82
|
+
this.options = options;
|
|
83
|
+
}
|
|
84
|
+
/** ---- Paths and positions ---- */
|
|
85
|
+
locate(uri) {
|
|
86
|
+
if (!uri.startsWith("file:")) return void 0;
|
|
87
|
+
const file = fileURLToPath(uri);
|
|
88
|
+
let best;
|
|
89
|
+
for (const entry of this.projects) {
|
|
90
|
+
const rel = relative(entry.root, file);
|
|
91
|
+
if (!rel.startsWith("..") && !resolve(entry.root, rel).startsWith("..") && (!best || entry.root.length > best.root.length)) best = entry;
|
|
92
|
+
}
|
|
93
|
+
return best ? {
|
|
94
|
+
entry: best,
|
|
95
|
+
path: posix(relative(best.root, file))
|
|
96
|
+
} : void 0;
|
|
97
|
+
}
|
|
98
|
+
uriOf(entry, path) {
|
|
99
|
+
return pathToFileURL(resolve(entry.root, path)).href;
|
|
100
|
+
}
|
|
101
|
+
text(entry, path) {
|
|
102
|
+
return this.documents.get(this.uriOf(entry, path))?.getText() ?? entry.project.text(path) ?? "";
|
|
103
|
+
}
|
|
104
|
+
range(entry, path, range) {
|
|
105
|
+
const index = createLineIndex(this.text(entry, path));
|
|
106
|
+
return {
|
|
107
|
+
start: index.position(range.start),
|
|
108
|
+
end: index.position(range.end)
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
offset(uri, position, entry, path) {
|
|
112
|
+
const document = this.documents.get(uri);
|
|
113
|
+
return document ? document.offsetAt(position) : createLineIndex(this.text(entry, path)).offset(position);
|
|
114
|
+
}
|
|
115
|
+
location(entry, path, range) {
|
|
116
|
+
return {
|
|
117
|
+
uri: this.uriOf(entry, path),
|
|
118
|
+
range: this.range(entry, path, range)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
at(params) {
|
|
122
|
+
const found = this.locate(params.textDocument.uri);
|
|
123
|
+
if (!found) return void 0;
|
|
124
|
+
return {
|
|
125
|
+
...found,
|
|
126
|
+
offset: this.offset(params.textDocument.uri, params.position, found.entry, found.path)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
displayLocale(entry) {
|
|
130
|
+
const wanted = this.settings.displayLocale;
|
|
131
|
+
return wanted && entry.project.locales.includes(wanted) ? wanted : entry.project.sourceLocale;
|
|
132
|
+
}
|
|
133
|
+
resourcePath(entry, id) {
|
|
134
|
+
return entry.project.resources.find((r) => r.id === id)?.path ?? id;
|
|
135
|
+
}
|
|
136
|
+
/** ---- Lifecycle ---- */
|
|
137
|
+
async load() {
|
|
138
|
+
for (const folder of this.folders) {
|
|
139
|
+
const overlays = [];
|
|
140
|
+
const wrap = (host) => {
|
|
141
|
+
const overlay = createOverlayHost(host);
|
|
142
|
+
overlays.push(overlay);
|
|
143
|
+
return overlay;
|
|
144
|
+
};
|
|
145
|
+
(this.options.loadProjects ? await this.options.loadProjects(folder, wrap) : (await loadWorkspace({
|
|
146
|
+
cwd: folder,
|
|
147
|
+
wrapHost: wrap
|
|
148
|
+
}).catch((error) => {
|
|
149
|
+
this.connection.window.showWarningMessage(`x18n: ${error instanceof Error ? error.message : String(error)}`);
|
|
150
|
+
return { projects: [] };
|
|
151
|
+
})).projects).forEach((item, i) => {
|
|
152
|
+
const overlay = overlays[i];
|
|
153
|
+
if (overlay && !this.projects.some((p) => p.root === item.root)) this.projects.push({
|
|
154
|
+
name: item.name,
|
|
155
|
+
root: item.root,
|
|
156
|
+
project: item.project,
|
|
157
|
+
overlay
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
for (const entry of this.projects) {
|
|
162
|
+
entry.project.onChange((change) => {
|
|
163
|
+
this.connection.sendNotification(methods.changed, {
|
|
164
|
+
project: entry.name,
|
|
165
|
+
revision: change.revision,
|
|
166
|
+
keys: change.keys
|
|
167
|
+
});
|
|
168
|
+
this.publishDiagnostics();
|
|
169
|
+
});
|
|
170
|
+
entry.project.loadHistory({ watch: true });
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async initialize(params) {
|
|
174
|
+
const folders = params.workspaceFolders?.map((folder) => fileURLToPath(folder.uri)) ?? (params.rootUri ? [fileURLToPath(params.rootUri)] : []);
|
|
175
|
+
this.folders = folders;
|
|
176
|
+
this.watchRegistration = params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration === true;
|
|
177
|
+
const settings = params.initializationOptions;
|
|
178
|
+
if (typeof settings?.displayLocale === "string") this.settings.displayLocale = settings.displayLocale;
|
|
179
|
+
if (typeof settings?.annotationMaxLength === "number") this.settings.annotationMaxLength = settings.annotationMaxLength;
|
|
180
|
+
if (typeof settings?.codeLens === "boolean") this.settings.codeLens = settings.codeLens;
|
|
181
|
+
return {
|
|
182
|
+
serverInfo: {
|
|
183
|
+
name: "x18n",
|
|
184
|
+
version
|
|
185
|
+
},
|
|
186
|
+
capabilities: {
|
|
187
|
+
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
188
|
+
definitionProvider: true,
|
|
189
|
+
implementationProvider: true,
|
|
190
|
+
referencesProvider: true,
|
|
191
|
+
renameProvider: { prepareProvider: true },
|
|
192
|
+
hoverProvider: true,
|
|
193
|
+
inlayHintProvider: true,
|
|
194
|
+
completionProvider: { triggerCharacters: [
|
|
195
|
+
".",
|
|
196
|
+
"'",
|
|
197
|
+
"\"",
|
|
198
|
+
"`",
|
|
199
|
+
":"
|
|
200
|
+
] },
|
|
201
|
+
codeActionProvider: { codeActionKinds: [CodeActionKind.QuickFix, CodeActionKind.RefactorExtract] },
|
|
202
|
+
documentSymbolProvider: true,
|
|
203
|
+
workspaceSymbolProvider: true,
|
|
204
|
+
codeLensProvider: { resolveProvider: false },
|
|
205
|
+
executeCommandProvider: { commands: Object.values(commands) }
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
async initialized() {
|
|
210
|
+
const progress = await this.connection.window.createWorkDoneProgress().catch(() => void 0);
|
|
211
|
+
progress?.begin("x18n", 0, "Indexing keys and usages", false);
|
|
212
|
+
this.ready = this.load().finally(() => progress?.done());
|
|
213
|
+
await this.ready;
|
|
214
|
+
this.publishDiagnostics();
|
|
215
|
+
if (this.watchRegistration) this.connection.client.register(DidChangeWatchedFilesNotification.type, { watchers: [{ globPattern: "**/*" }] }).catch(() => void 0);
|
|
216
|
+
}
|
|
217
|
+
/** ---- Document sync ---- */
|
|
218
|
+
schedule(uri, run) {
|
|
219
|
+
clearTimeout(this.timers.get(uri));
|
|
220
|
+
this.timers.set(uri, setTimeout(() => {
|
|
221
|
+
this.timers.delete(uri);
|
|
222
|
+
run();
|
|
223
|
+
}, 100));
|
|
224
|
+
}
|
|
225
|
+
async changed(uri, text) {
|
|
226
|
+
await this.ready;
|
|
227
|
+
const found = this.locate(uri);
|
|
228
|
+
if (!found) return;
|
|
229
|
+
if (text === void 0) found.entry.overlay.clearOverlay(found.path);
|
|
230
|
+
else found.entry.overlay.setOverlay(found.path, text);
|
|
231
|
+
await found.entry.project.update([{
|
|
232
|
+
type: "change",
|
|
233
|
+
path: found.path
|
|
234
|
+
}]);
|
|
235
|
+
}
|
|
236
|
+
async watched(changes) {
|
|
237
|
+
await this.ready;
|
|
238
|
+
const byProject = /* @__PURE__ */ new Map();
|
|
239
|
+
for (const change of changes) {
|
|
240
|
+
const found = this.locate(change.uri);
|
|
241
|
+
if (!found || found.path.startsWith("node_modules/") || found.path.includes("/node_modules/") || this.documents.get(change.uri)) continue;
|
|
242
|
+
const list = byProject.get(found.entry) ?? [];
|
|
243
|
+
byProject.set(found.entry, list);
|
|
244
|
+
list.push({
|
|
245
|
+
type: change.type === FileChangeType.Created ? "create" : change.type === FileChangeType.Deleted ? "delete" : "change",
|
|
246
|
+
path: found.path
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
for (const [entry, events] of byProject) await entry.project.update(events);
|
|
250
|
+
}
|
|
251
|
+
publishDiagnostics() {
|
|
252
|
+
for (const entry of this.projects) {
|
|
253
|
+
const byFile = /* @__PURE__ */ new Map();
|
|
254
|
+
for (const d of entry.project.analyze().diagnostics) {
|
|
255
|
+
if (!d.location) continue;
|
|
256
|
+
const uri = this.uriOf(entry, d.location.path);
|
|
257
|
+
const list = byFile.get(uri) ?? [];
|
|
258
|
+
byFile.set(uri, list);
|
|
259
|
+
list.push({
|
|
260
|
+
range: this.range(entry, d.location.path, d.location.range),
|
|
261
|
+
severity: severities[d.severity],
|
|
262
|
+
code: d.code,
|
|
263
|
+
source: "x18n",
|
|
264
|
+
message: d.message,
|
|
265
|
+
data: {
|
|
266
|
+
key: d.key,
|
|
267
|
+
locale: d.locale
|
|
268
|
+
}
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
const previous = this.published.get(entry.root) ?? /* @__PURE__ */ new Set();
|
|
272
|
+
for (const uri of previous) if (!byFile.has(uri)) this.connection.sendDiagnostics({
|
|
273
|
+
uri,
|
|
274
|
+
diagnostics: []
|
|
275
|
+
});
|
|
276
|
+
for (const [uri, diagnostics] of byFile) this.connection.sendDiagnostics({
|
|
277
|
+
uri,
|
|
278
|
+
diagnostics
|
|
279
|
+
});
|
|
280
|
+
this.published.set(entry.root, new Set(byFile.keys()));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
/** ---- Symbols ---- */
|
|
284
|
+
entryLocation(entry, node) {
|
|
285
|
+
return this.location(entry, node.path, node.range.key);
|
|
286
|
+
}
|
|
287
|
+
async definition(params) {
|
|
288
|
+
await this.ready;
|
|
289
|
+
const at = this.at(params);
|
|
290
|
+
const target = at && at.entry.project.keyAt(at.path, at.offset);
|
|
291
|
+
if (!at || !target) return null;
|
|
292
|
+
const nodes = at.entry.project.locations(target.key);
|
|
293
|
+
const source = nodes.find((node) => node.locale === at.entry.project.sourceLocale) ?? nodes[0];
|
|
294
|
+
return source ? [this.entryLocation(at.entry, source)] : null;
|
|
295
|
+
}
|
|
296
|
+
async implementation(params) {
|
|
297
|
+
await this.ready;
|
|
298
|
+
const at = this.at(params);
|
|
299
|
+
const target = at && at.entry.project.keyAt(at.path, at.offset);
|
|
300
|
+
if (!at || !target) return null;
|
|
301
|
+
return at.entry.project.locations(target.key).map((node) => this.entryLocation(at.entry, node));
|
|
302
|
+
}
|
|
303
|
+
async references(params) {
|
|
304
|
+
await this.ready;
|
|
305
|
+
const at = this.at(params);
|
|
306
|
+
const target = at && at.entry.project.keyAt(at.path, at.offset);
|
|
307
|
+
if (!at || !target) return null;
|
|
308
|
+
const { project } = at.entry;
|
|
309
|
+
return (target.type === "subtree" ? project.usages({ prefix: target.key }) : project.usages({ key: target.key })).map((m) => this.location(at.entry, m.usage.path, m.usage.range));
|
|
310
|
+
}
|
|
311
|
+
async prepareRename(params) {
|
|
312
|
+
await this.ready;
|
|
313
|
+
const at = this.at(params);
|
|
314
|
+
const target = at && at.entry.project.keyAt(at.path, at.offset);
|
|
315
|
+
if (!at || !target) return null;
|
|
316
|
+
return {
|
|
317
|
+
range: this.range(at.entry, at.path, target.range),
|
|
318
|
+
placeholder: target.key
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
async rename(params) {
|
|
322
|
+
await this.ready;
|
|
323
|
+
const at = this.at(params);
|
|
324
|
+
const target = at && at.entry.project.keyAt(at.path, at.offset);
|
|
325
|
+
if (!at || !target) return null;
|
|
326
|
+
const { project } = at.entry;
|
|
327
|
+
const to = project.parseKey(params.newName.trim());
|
|
328
|
+
const op = target.type === "subtree" ? {
|
|
329
|
+
type: "rename-prefix",
|
|
330
|
+
from: target.key,
|
|
331
|
+
to
|
|
332
|
+
} : {
|
|
333
|
+
type: "rename",
|
|
334
|
+
from: target.key,
|
|
335
|
+
to
|
|
336
|
+
};
|
|
337
|
+
const plan = this.plan(project, [op]);
|
|
338
|
+
if (plan.warnings.length) this.connection.window.showWarningMessage(`x18n: ${plan.warnings.join(" ")}`);
|
|
339
|
+
return this.workspaceEdit(at.entry, plan);
|
|
340
|
+
}
|
|
341
|
+
plan(project, ops) {
|
|
342
|
+
try {
|
|
343
|
+
return project.plan(ops);
|
|
344
|
+
} catch (error) {
|
|
345
|
+
throw new ResponseError(1, isX18nError(error) ? `${error.message}. ${error.hint}` : String(error));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
workspaceEdit(entry, plan) {
|
|
349
|
+
const documentChanges = [];
|
|
350
|
+
const toEdit = (change) => {
|
|
351
|
+
const uri = this.uriOf(entry, change.path);
|
|
352
|
+
if (change.type === "create") {
|
|
353
|
+
documentChanges.push({
|
|
354
|
+
kind: "create",
|
|
355
|
+
uri,
|
|
356
|
+
options: { ignoreIfExists: true }
|
|
357
|
+
});
|
|
358
|
+
documentChanges.push({
|
|
359
|
+
textDocument: {
|
|
360
|
+
uri,
|
|
361
|
+
version: null
|
|
362
|
+
},
|
|
363
|
+
edits: [{
|
|
364
|
+
range: {
|
|
365
|
+
start: {
|
|
366
|
+
line: 0,
|
|
367
|
+
character: 0
|
|
368
|
+
},
|
|
369
|
+
end: {
|
|
370
|
+
line: 0,
|
|
371
|
+
character: 0
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
newText: change.content
|
|
375
|
+
}]
|
|
376
|
+
});
|
|
377
|
+
} else if (change.type === "delete") documentChanges.push({
|
|
378
|
+
kind: "delete",
|
|
379
|
+
uri
|
|
380
|
+
});
|
|
381
|
+
else documentChanges.push({
|
|
382
|
+
textDocument: {
|
|
383
|
+
uri,
|
|
384
|
+
version: this.documents.get(uri)?.version ?? null
|
|
385
|
+
},
|
|
386
|
+
edits: change.edits.map((edit) => ({
|
|
387
|
+
range: this.range(entry, change.path, edit.range),
|
|
388
|
+
newText: edit.text
|
|
389
|
+
}))
|
|
390
|
+
});
|
|
391
|
+
};
|
|
392
|
+
plan.changes.forEach(toEdit);
|
|
393
|
+
return { documentChanges };
|
|
394
|
+
}
|
|
395
|
+
/** ---- Editor features ---- */
|
|
396
|
+
async hover(params) {
|
|
397
|
+
await this.ready;
|
|
398
|
+
const at = this.at(params);
|
|
399
|
+
const target = at && at.entry.project.keyAt(at.path, at.offset);
|
|
400
|
+
if (!at || !target) return null;
|
|
401
|
+
const { project } = at.entry;
|
|
402
|
+
const lines = [`**x18n** \`${target.key}\``, ""];
|
|
403
|
+
if (target.type === "key") {
|
|
404
|
+
const changes = project.locales.map((locale) => project.history(target.key, locale));
|
|
405
|
+
const dated = changes.some(Boolean);
|
|
406
|
+
const outdated = new Set(project.analyze().coverage.filter((c) => c.outdated.includes(target.key)).map((c) => c.locale));
|
|
407
|
+
lines.push(dated ? "| | | |" : "| | |", dated ? "| --- | --- | --- |" : "| --- | --- |");
|
|
408
|
+
project.locales.forEach((locale, i) => {
|
|
409
|
+
const value = display(project.lookup(target.key, locale)?.value);
|
|
410
|
+
const change = changes[i];
|
|
411
|
+
const changed = dated ? ` ${change ? changedAt(change).replace(/\|/g, "\\|") : ""}${outdated.has(locale) ? " · **outdated**" : ""} |` : "";
|
|
412
|
+
lines.push(`| ${project.localeInfo(locale).flag ?? ""} ${locale} | ${value === void 0 ? "_missing_" : value === "" ? "_empty_" : value.replace(/\|/g, "\\|").replace(/\n/g, " ")} |${changed}`);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
const usages = target.type === "subtree" ? project.usages({ prefix: target.key }) : project.usages({ key: target.key });
|
|
416
|
+
const args = encodeURIComponent(JSON.stringify([{
|
|
417
|
+
project: at.entry.name,
|
|
418
|
+
key: target.key
|
|
419
|
+
}]));
|
|
420
|
+
lines.push("", `${usages.length} usage${usages.length === 1 ? "" : "s"} · [Open in UI](command:x18n.openKey?${args}) · [Copy translate prompt](command:x18n.copyTranslatePrompt?${args})`);
|
|
421
|
+
return {
|
|
422
|
+
contents: {
|
|
423
|
+
kind: "markdown",
|
|
424
|
+
value: lines.join("\n")
|
|
425
|
+
},
|
|
426
|
+
range: this.range(at.entry, at.path, target.range)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
async inlayHints(params) {
|
|
430
|
+
await this.ready;
|
|
431
|
+
const found = this.locate(params.textDocument.uri);
|
|
432
|
+
if (!found) return [];
|
|
433
|
+
const { entry, path } = found;
|
|
434
|
+
const locale = this.displayLocale(entry);
|
|
435
|
+
const index = createLineIndex(this.text(entry, path));
|
|
436
|
+
const start = index.offset(params.range.start);
|
|
437
|
+
const end = index.offset(params.range.end);
|
|
438
|
+
const hints = [];
|
|
439
|
+
for (const { usage } of entry.project.usages({ path })) {
|
|
440
|
+
if (usage.key === void 0 || usage.dynamic !== false || usage.range.start < start || usage.range.end > end) continue;
|
|
441
|
+
const value = display(entry.project.lookup(usage.key, locale)?.value);
|
|
442
|
+
hints.push({
|
|
443
|
+
position: index.position(usage.range.end + 1),
|
|
444
|
+
label: value === void 0 ? " ⚠ missing" : ` ${truncate(value, this.settings.annotationMaxLength)}`,
|
|
445
|
+
paddingLeft: true
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
return hints;
|
|
449
|
+
}
|
|
450
|
+
async completion(params) {
|
|
451
|
+
await this.ready;
|
|
452
|
+
const at = this.at(params);
|
|
453
|
+
if (!at) return [];
|
|
454
|
+
const { project } = at.entry;
|
|
455
|
+
const slot = project.keySlots(at.path).find((range) => at.offset >= range.start && at.offset <= range.end);
|
|
456
|
+
if (!slot) return [];
|
|
457
|
+
const usage = project.usages({ path: at.path }).map((m) => m.usage).find((u) => u.range.start === slot.start);
|
|
458
|
+
const scopeNs = usage?.scopeNamespace ?? project.config.namespace.default;
|
|
459
|
+
const prefix = usage?.scopePrefix ? `${usage.scopePrefix}.` : "";
|
|
460
|
+
const range = this.range(at.entry, at.path, slot);
|
|
461
|
+
return project.keys().flatMap((key) => {
|
|
462
|
+
let insert = key;
|
|
463
|
+
if (project.namespaced && key.startsWith(`${scopeNs}:`)) insert = key.slice(scopeNs.length + 1);
|
|
464
|
+
if (prefix) {
|
|
465
|
+
if (!insert.startsWith(prefix)) return [];
|
|
466
|
+
insert = insert.slice(prefix.length);
|
|
467
|
+
}
|
|
468
|
+
const source = display(project.lookup(key, project.sourceLocale)?.value);
|
|
469
|
+
return [{
|
|
470
|
+
label: insert,
|
|
471
|
+
kind: CompletionItemKind.Text,
|
|
472
|
+
...source === void 0 ? {} : { detail: source },
|
|
473
|
+
textEdit: {
|
|
474
|
+
range,
|
|
475
|
+
newText: insert
|
|
476
|
+
},
|
|
477
|
+
filterText: insert
|
|
478
|
+
}];
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
async codeActions(params) {
|
|
482
|
+
await this.ready;
|
|
483
|
+
const found = this.locate(params.textDocument.uri);
|
|
484
|
+
if (!found) return [];
|
|
485
|
+
const { entry, path } = found;
|
|
486
|
+
const index = createLineIndex(this.text(entry, path));
|
|
487
|
+
const start = index.offset(params.range.start);
|
|
488
|
+
const end = index.offset(params.range.end);
|
|
489
|
+
const actions = [];
|
|
490
|
+
for (const d of entry.project.analyze().diagnostics) {
|
|
491
|
+
if (!d.location || d.location.path !== path || d.location.range.end < start || d.location.range.start > end) continue;
|
|
492
|
+
for (const fix of d.fixes) try {
|
|
493
|
+
actions.push({
|
|
494
|
+
title: fix.title,
|
|
495
|
+
kind: CodeActionKind.QuickFix,
|
|
496
|
+
edit: this.workspaceEdit(entry, entry.project.plan(fix.ops))
|
|
497
|
+
});
|
|
498
|
+
} catch {}
|
|
499
|
+
}
|
|
500
|
+
return [...actions, ...this.extractActions(entry, path, start, end)];
|
|
501
|
+
}
|
|
502
|
+
extractActions(entry, path, start, end) {
|
|
503
|
+
const { project } = entry;
|
|
504
|
+
if (project.nodes(path).length > 0) return [];
|
|
505
|
+
const candidates = findCandidates(project, { files: [path] });
|
|
506
|
+
const inRange = candidates.filter((c) => c.range.start <= end && c.range.end >= start);
|
|
507
|
+
const actions = [];
|
|
508
|
+
const edit = (list) => {
|
|
509
|
+
try {
|
|
510
|
+
return this.workspaceEdit(entry, project.plan([{
|
|
511
|
+
type: "extract",
|
|
512
|
+
items: extractItems(project, list)
|
|
513
|
+
}]));
|
|
514
|
+
} catch {
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
const text = this.text(entry, path);
|
|
519
|
+
const index = createLineIndex(text);
|
|
520
|
+
for (const candidate of inRange) {
|
|
521
|
+
const extracted = edit([candidate]);
|
|
522
|
+
if (extracted) actions.push({
|
|
523
|
+
title: `Extract "${truncate(candidate.text, 30)}" to ${candidate.key}`,
|
|
524
|
+
kind: CodeActionKind.RefactorExtract,
|
|
525
|
+
edit: extracted
|
|
526
|
+
});
|
|
527
|
+
const line = index.position(candidate.range.start).line;
|
|
528
|
+
const indent = /^\s*/.exec(index.line(line))?.[0] ?? "";
|
|
529
|
+
const comment = /-(?:text|attribute)$/.test(candidate.kind) && !candidate.kind.startsWith("js") ? "<!-- x18n-ignore -->" : "// x18n-ignore";
|
|
530
|
+
actions.push({
|
|
531
|
+
title: `Ignore "${truncate(candidate.text, 30)}"`,
|
|
532
|
+
kind: CodeActionKind.QuickFix,
|
|
533
|
+
edit: { changes: { [this.uriOf(entry, path)]: [{
|
|
534
|
+
range: {
|
|
535
|
+
start: {
|
|
536
|
+
line,
|
|
537
|
+
character: 0
|
|
538
|
+
},
|
|
539
|
+
end: {
|
|
540
|
+
line,
|
|
541
|
+
character: 0
|
|
542
|
+
}
|
|
543
|
+
},
|
|
544
|
+
newText: `${indent}${comment}\n`
|
|
545
|
+
}] } }
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
if (inRange.length > 0 && candidates.length > 1) {
|
|
549
|
+
const all = edit(candidates);
|
|
550
|
+
if (all) actions.push({
|
|
551
|
+
title: `Extract all ${candidates.length} hard strings in this file`,
|
|
552
|
+
kind: CodeActionKind.RefactorExtract,
|
|
553
|
+
edit: all
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
return actions;
|
|
557
|
+
}
|
|
558
|
+
async documentSymbols(params) {
|
|
559
|
+
await this.ready;
|
|
560
|
+
const found = this.locate(params.textDocument.uri);
|
|
561
|
+
if (!found) return [];
|
|
562
|
+
const { entry, path } = found;
|
|
563
|
+
const roots = [];
|
|
564
|
+
const stack = [];
|
|
565
|
+
for (const node of entry.project.nodes(path)) {
|
|
566
|
+
while (stack.length && !node.key.startsWith(`${stack.at(-1).key}.`)) stack.pop();
|
|
567
|
+
const parentKey = stack.at(-1)?.key;
|
|
568
|
+
const symbol = {
|
|
569
|
+
name: parentKey ? node.key.slice(parentKey.length + 1) : node.key,
|
|
570
|
+
detail: node.leaf ? display(entry.project.message(node.key, node.locale)?.value) ?? "" : "",
|
|
571
|
+
kind: node.leaf ? SymbolKind.String : SymbolKind.Namespace,
|
|
572
|
+
range: this.range(entry, path, {
|
|
573
|
+
start: node.range.key.start,
|
|
574
|
+
end: Math.max(node.range.value.end, node.range.key.end)
|
|
575
|
+
}),
|
|
576
|
+
selectionRange: this.range(entry, path, node.range.key),
|
|
577
|
+
children: []
|
|
578
|
+
};
|
|
579
|
+
if (stack.length) stack.at(-1).symbol.children.push(symbol);
|
|
580
|
+
else roots.push(symbol);
|
|
581
|
+
if (!node.leaf) stack.push({
|
|
582
|
+
key: node.key,
|
|
583
|
+
symbol
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
return roots;
|
|
587
|
+
}
|
|
588
|
+
async workspaceSymbols(params) {
|
|
589
|
+
await this.ready;
|
|
590
|
+
const query = params.query.toLowerCase();
|
|
591
|
+
const out = [];
|
|
592
|
+
for (const entry of this.projects) for (const key of entry.project.keys()) {
|
|
593
|
+
const value = display(entry.project.message(key, entry.project.sourceLocale)?.value) ?? "";
|
|
594
|
+
if (query && !key.toLowerCase().includes(query) && !value.toLowerCase().includes(query)) continue;
|
|
595
|
+
const node = entry.project.locations(key)[0];
|
|
596
|
+
if (node) out.push({
|
|
597
|
+
name: key,
|
|
598
|
+
kind: SymbolKind.String,
|
|
599
|
+
containerName: value,
|
|
600
|
+
location: this.entryLocation(entry, node)
|
|
601
|
+
});
|
|
602
|
+
if (out.length >= 500) return out;
|
|
603
|
+
}
|
|
604
|
+
return out;
|
|
605
|
+
}
|
|
606
|
+
async codeLenses(params) {
|
|
607
|
+
await this.ready;
|
|
608
|
+
const found = this.locate(params.textDocument.uri);
|
|
609
|
+
if (!this.settings.codeLens || !found) return [];
|
|
610
|
+
const { entry, path } = found;
|
|
611
|
+
return entry.project.nodes(path).filter((node) => node.leaf).map((node) => {
|
|
612
|
+
const info = entry.project.get(node.key);
|
|
613
|
+
const used = info?.usages.length ?? 0;
|
|
614
|
+
const missing = info?.missing ?? [];
|
|
615
|
+
const title = `used ${used} time${used === 1 ? "" : "s"}${missing.length ? ` · missing in ${missing.join(", ")}` : ""}`;
|
|
616
|
+
return {
|
|
617
|
+
range: this.range(entry, path, node.range.key),
|
|
618
|
+
command: {
|
|
619
|
+
title,
|
|
620
|
+
command: ""
|
|
621
|
+
}
|
|
622
|
+
};
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
async executeCommand(params) {
|
|
626
|
+
await this.ready;
|
|
627
|
+
const arg = params.arguments?.[0];
|
|
628
|
+
if (typeof arg !== "object" || arg === null) return null;
|
|
629
|
+
const input = arg;
|
|
630
|
+
if (params.command === commands.extract && typeof input.uri === "string") return this.extractFile(input.uri);
|
|
631
|
+
if (params.command === commands.migrateI18nAlly && typeof input.uri === "string") return migrateI18nAlly(fileURLToPath(input.uri));
|
|
632
|
+
const entry = this.projects.find((p) => p.name === input.project) ?? this.projects[0];
|
|
633
|
+
if (!entry || typeof input.key !== "string") return null;
|
|
634
|
+
const key = entry.project.parseKey(input.key);
|
|
635
|
+
let op;
|
|
636
|
+
if (params.command === commands.set && typeof input.value === "string") op = {
|
|
637
|
+
type: "set",
|
|
638
|
+
key,
|
|
639
|
+
locale: input.locale ?? entry.project.sourceLocale,
|
|
640
|
+
value: input.value
|
|
641
|
+
};
|
|
642
|
+
else if (params.command === commands.delete) op = {
|
|
643
|
+
type: "delete",
|
|
644
|
+
key,
|
|
645
|
+
locales: input.locale ? [input.locale] : "all"
|
|
646
|
+
};
|
|
647
|
+
else if (params.command === commands.renamePrefix && typeof input.to === "string") op = {
|
|
648
|
+
type: "rename-prefix",
|
|
649
|
+
from: key,
|
|
650
|
+
to: entry.project.parseKey(input.to)
|
|
651
|
+
};
|
|
652
|
+
if (!op) return null;
|
|
653
|
+
const edit = this.workspaceEdit(entry, this.plan(entry.project, [op]));
|
|
654
|
+
await this.connection.workspace.applyEdit(edit);
|
|
655
|
+
return edit;
|
|
656
|
+
}
|
|
657
|
+
async extractFile(uri) {
|
|
658
|
+
const found = this.locate(uri);
|
|
659
|
+
if (!found) return null;
|
|
660
|
+
const { entry, path } = found;
|
|
661
|
+
const candidates = findCandidates(entry.project, { files: [path] });
|
|
662
|
+
if (candidates.length === 0) return null;
|
|
663
|
+
const edit = this.workspaceEdit(entry, this.plan(entry.project, [{
|
|
664
|
+
type: "extract",
|
|
665
|
+
items: extractItems(entry.project, candidates)
|
|
666
|
+
}]));
|
|
667
|
+
await this.connection.workspace.applyEdit(edit);
|
|
668
|
+
return edit;
|
|
669
|
+
}
|
|
670
|
+
/** ---- Custom protocol ---- */
|
|
671
|
+
entryByName(name) {
|
|
672
|
+
return name === void 0 ? this.projects[0] : this.projects.find((p) => p.name === name);
|
|
673
|
+
}
|
|
674
|
+
projectInfos() {
|
|
675
|
+
return this.projects.map((entry) => ({
|
|
676
|
+
name: entry.name,
|
|
677
|
+
root: entry.root,
|
|
678
|
+
sourceLocale: entry.project.sourceLocale,
|
|
679
|
+
locales: entry.project.locales.map((code) => ({
|
|
680
|
+
code,
|
|
681
|
+
displayName: entry.project.localeInfo(code).displayName,
|
|
682
|
+
flag: entry.project.localeInfo(code).flag
|
|
683
|
+
})),
|
|
684
|
+
displayLocale: this.displayLocale(entry),
|
|
685
|
+
keys: entry.project.keys().length
|
|
686
|
+
}));
|
|
687
|
+
}
|
|
688
|
+
async keyAt(params) {
|
|
689
|
+
await this.ready;
|
|
690
|
+
const at = this.at({
|
|
691
|
+
textDocument: { uri: params.uri },
|
|
692
|
+
position: params.position
|
|
693
|
+
});
|
|
694
|
+
const target = at && at.entry.project.keyAt(at.path, at.offset);
|
|
695
|
+
if (!at || !target) return null;
|
|
696
|
+
return {
|
|
697
|
+
project: at.entry.name,
|
|
698
|
+
key: target.key,
|
|
699
|
+
type: target.type,
|
|
700
|
+
range: this.range(at.entry, at.path, target.range)
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
tree(params) {
|
|
704
|
+
const entry = this.entryByName(params.project);
|
|
705
|
+
if (!entry) return [];
|
|
706
|
+
const { project } = entry;
|
|
707
|
+
const locale = this.displayLocale(entry);
|
|
708
|
+
const parent = params.parent;
|
|
709
|
+
const children = /* @__PURE__ */ new Map();
|
|
710
|
+
for (const key of project.keys()) {
|
|
711
|
+
if (parent !== void 0 && !key.startsWith(project.namespaced && parent.endsWith(":") ? parent : `${parent}.`)) continue;
|
|
712
|
+
const rest = parent === void 0 ? key : key.slice(parent.length + (parent.endsWith(":") ? 0 : 1));
|
|
713
|
+
const segment = (parent === void 0 && project.namespaced ? rest.slice(0, rest.indexOf(":") + 1) : "") || rest.split(".")[0];
|
|
714
|
+
const child = parent === void 0 ? segment : `${parent}${parent.endsWith(":") ? "" : "."}${segment}`;
|
|
715
|
+
const list = children.get(child) ?? [];
|
|
716
|
+
children.set(child, list);
|
|
717
|
+
list.push(key);
|
|
718
|
+
}
|
|
719
|
+
const others = project.locales.filter((l) => l !== project.sourceLocale);
|
|
720
|
+
return [...children].map(([key, keys]) => {
|
|
721
|
+
const leaf = keys.length === 1 && keys[0] === key;
|
|
722
|
+
const filled = (k, l) => !["", void 0].includes(display(project.message(k, l)?.value));
|
|
723
|
+
const translated = keys.reduce((sum, k) => sum + others.filter((l) => filled(k, l)).length, 0);
|
|
724
|
+
return {
|
|
725
|
+
key,
|
|
726
|
+
label: key.slice((parent?.length ?? -1) + 1).replace(/:$/, "") || key,
|
|
727
|
+
leaf,
|
|
728
|
+
translated,
|
|
729
|
+
total: keys.length * others.length,
|
|
730
|
+
value: leaf ? display(project.message(key, locale)?.value) : void 0
|
|
731
|
+
};
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
progress() {
|
|
735
|
+
return { projects: this.projects.map((entry) => ({
|
|
736
|
+
name: entry.name,
|
|
737
|
+
locales: entry.project.analyze().coverage.map((c) => ({
|
|
738
|
+
locale: c.locale,
|
|
739
|
+
translated: c.translated,
|
|
740
|
+
total: c.total,
|
|
741
|
+
missing: c.missing,
|
|
742
|
+
empty: c.empty
|
|
743
|
+
}))
|
|
744
|
+
})) };
|
|
745
|
+
}
|
|
746
|
+
async fileKeys(params) {
|
|
747
|
+
await this.ready;
|
|
748
|
+
const found = this.locate(params.uri);
|
|
749
|
+
if (!found) return {
|
|
750
|
+
keys: [],
|
|
751
|
+
hardStrings: []
|
|
752
|
+
};
|
|
753
|
+
const { entry, path } = found;
|
|
754
|
+
const locale = this.displayLocale(entry);
|
|
755
|
+
return {
|
|
756
|
+
keys: entry.project.nodes(path).length > 0 ? entry.project.nodes(path).filter((n) => n.leaf).map((n) => ({
|
|
757
|
+
key: n.key,
|
|
758
|
+
range: this.range(entry, path, n.range.key),
|
|
759
|
+
value: display(entry.project.message(n.key, locale)?.value),
|
|
760
|
+
missing: entry.project.get(n.key).missing.length > 0,
|
|
761
|
+
dynamic: false
|
|
762
|
+
})) : entry.project.usages({ path }).flatMap(({ usage }) => usage.key === void 0 ? [] : [{
|
|
763
|
+
key: usage.key,
|
|
764
|
+
range: this.range(entry, path, usage.range),
|
|
765
|
+
value: display(entry.project.lookup(usage.key, locale)?.value),
|
|
766
|
+
missing: usage.dynamic === false && entry.project.lookup(usage.key, entry.project.sourceLocale) === void 0,
|
|
767
|
+
dynamic: usage.dynamic !== false
|
|
768
|
+
}]),
|
|
769
|
+
hardStrings: entry.project.hardStrings(path).map((h) => ({
|
|
770
|
+
text: h.text,
|
|
771
|
+
range: this.range(entry, path, h.range)
|
|
772
|
+
}))
|
|
773
|
+
};
|
|
774
|
+
}
|
|
775
|
+
usageReport() {
|
|
776
|
+
const unused = [];
|
|
777
|
+
const missing = [];
|
|
778
|
+
for (const entry of this.projects) {
|
|
779
|
+
const analysis = entry.project.analyze();
|
|
780
|
+
for (const key of analysis.unused) {
|
|
781
|
+
const node = entry.project.locations(key)[0];
|
|
782
|
+
unused.push({
|
|
783
|
+
project: entry.name,
|
|
784
|
+
key,
|
|
785
|
+
location: node ? this.entryLocation(entry, node) : void 0
|
|
786
|
+
});
|
|
787
|
+
}
|
|
788
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
789
|
+
for (const usage of analysis.undefinedUsages) {
|
|
790
|
+
const list = byKey.get(usage.key) ?? [];
|
|
791
|
+
byKey.set(usage.key, list);
|
|
792
|
+
list.push(this.location(entry, usage.path, usage.range));
|
|
793
|
+
}
|
|
794
|
+
for (const [key, locations] of byKey) missing.push({
|
|
795
|
+
project: entry.name,
|
|
796
|
+
key,
|
|
797
|
+
locations
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
return {
|
|
801
|
+
unused,
|
|
802
|
+
missing
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
async decorations(params) {
|
|
806
|
+
await this.ready;
|
|
807
|
+
const found = this.locate(params.uri);
|
|
808
|
+
if (!found || found.entry.project.nodes(found.path).length > 0) return {
|
|
809
|
+
locale: this.settings.displayLocale ?? "",
|
|
810
|
+
decorations: []
|
|
811
|
+
};
|
|
812
|
+
const { entry, path } = found;
|
|
813
|
+
const locale = this.displayLocale(entry);
|
|
814
|
+
return {
|
|
815
|
+
locale,
|
|
816
|
+
decorations: entry.project.usages({ path }).flatMap(({ usage }) => {
|
|
817
|
+
if (usage.key === void 0) return [];
|
|
818
|
+
const value = display(entry.project.lookup(usage.key, locale)?.value);
|
|
819
|
+
const state = usage.dynamic !== false ? "dynamic" : value === void 0 ? "missing" : "ok";
|
|
820
|
+
return [{
|
|
821
|
+
range: this.range(entry, path, usage.range),
|
|
822
|
+
key: usage.key,
|
|
823
|
+
value,
|
|
824
|
+
state
|
|
825
|
+
}];
|
|
826
|
+
})
|
|
827
|
+
};
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
/** Registers the x18n handlers on a connection. The caller owns transport and `listen()`. */
|
|
831
|
+
function setupServer(connection, options = {}) {
|
|
832
|
+
let ui;
|
|
833
|
+
const openUi = options.openUi ?? (async (entry) => (await (ui ??= import("./ui.mjs").then((m) => m.createUiServers(connection)))).open(entry));
|
|
834
|
+
connection.onShutdown(async () => {
|
|
835
|
+
await (await ui)?.close();
|
|
836
|
+
});
|
|
837
|
+
const server = new Server(connection, options);
|
|
838
|
+
server.documents.listen(connection);
|
|
839
|
+
connection.onInitialize((params) => server.initialize(params));
|
|
840
|
+
connection.onInitialized(() => void server.initialized());
|
|
841
|
+
server.documents.onDidChangeContent(({ document }) => server.schedule(document.uri, () => server.changed(document.uri, document.getText())));
|
|
842
|
+
server.documents.onDidClose(({ document }) => server.schedule(document.uri, () => server.changed(document.uri, void 0)));
|
|
843
|
+
connection.onDidChangeWatchedFiles((params) => void server.watched(params.changes));
|
|
844
|
+
connection.onDefinition((params) => server.definition(params));
|
|
845
|
+
connection.onImplementation((params) => server.implementation(params));
|
|
846
|
+
connection.onReferences((params) => server.references(params));
|
|
847
|
+
connection.onPrepareRename((params) => server.prepareRename(params));
|
|
848
|
+
connection.onRenameRequest((params) => server.rename(params));
|
|
849
|
+
connection.onHover((params) => server.hover(params));
|
|
850
|
+
connection.languages.inlayHint.on((params) => server.inlayHints(params));
|
|
851
|
+
connection.onCompletion((params) => server.completion(params));
|
|
852
|
+
connection.onCodeAction((params) => server.codeActions(params));
|
|
853
|
+
connection.onDocumentSymbol((params) => server.documentSymbols(params));
|
|
854
|
+
connection.onWorkspaceSymbol((params) => server.workspaceSymbols(params));
|
|
855
|
+
connection.onCodeLens((params) => server.codeLenses(params));
|
|
856
|
+
connection.onExecuteCommand((params) => server.executeCommand(params));
|
|
857
|
+
connection.onRequest(methods.projects, async () => {
|
|
858
|
+
await server.ready;
|
|
859
|
+
return server.projectInfos();
|
|
860
|
+
});
|
|
861
|
+
connection.onRequest(methods.keyAt, (params) => server.keyAt(params));
|
|
862
|
+
connection.onRequest(methods.tree, async (params) => {
|
|
863
|
+
await server.ready;
|
|
864
|
+
return server.tree(params ?? {});
|
|
865
|
+
});
|
|
866
|
+
connection.onRequest(methods.progress, async () => {
|
|
867
|
+
await server.ready;
|
|
868
|
+
return server.progress();
|
|
869
|
+
});
|
|
870
|
+
connection.onRequest(methods.fileKeys, (params) => server.fileKeys(params));
|
|
871
|
+
connection.onRequest(methods.usageReport, async () => {
|
|
872
|
+
await server.ready;
|
|
873
|
+
return server.usageReport();
|
|
874
|
+
});
|
|
875
|
+
connection.onRequest(methods.decorations, (params) => server.decorations(params));
|
|
876
|
+
connection.onNotification(methods.setDisplayLocale, (params) => {
|
|
877
|
+
server.settings.displayLocale = params.locale;
|
|
878
|
+
connection.languages.inlayHint.refresh().catch(() => void 0);
|
|
879
|
+
});
|
|
880
|
+
connection.onRequest(methods.openUi, async (params) => {
|
|
881
|
+
await server.ready;
|
|
882
|
+
const entry = server.entryByName(params?.project);
|
|
883
|
+
if (!entry) throw new ResponseError(1, `No project "${params?.project ?? ""}" in this server.`);
|
|
884
|
+
return openUi(entry);
|
|
885
|
+
});
|
|
886
|
+
connection.onRequest(methods.translatePrompt, async (params) => {
|
|
887
|
+
await server.ready;
|
|
888
|
+
const entry = server.entryByName(params.project);
|
|
889
|
+
if (!entry) throw new ResponseError(1, `Unknown project "${params.project}".`);
|
|
890
|
+
return renderPrompt(entry.project.prompt("translate", params.locale, { keys: params.keys }));
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
//#endregion
|
|
894
|
+
export { commands as n, methods as r, setupServer as t };
|
package/dist/ui.mjs
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { pathToFileURL } from "node:url";
|
|
3
|
+
import { createX18nDevframe } from "@x18n/ui";
|
|
4
|
+
import { createDevServer } from "devframe/adapters/dev";
|
|
5
|
+
import { getTempAuthCodeInfo } from "devframe/node/auth";
|
|
6
|
+
//#region src/ui.ts
|
|
7
|
+
/**
|
|
8
|
+
* One devframe dev server per project, on loopback, sharing the server's `Project`.
|
|
9
|
+
* The token is devframe's one-time code: the webview passes it as the magic link
|
|
10
|
+
* fragment (`#devframe_otp=<token>`), which the UI exchanges for a session.
|
|
11
|
+
*/
|
|
12
|
+
function createUiServers(connection) {
|
|
13
|
+
const servers = /* @__PURE__ */ new Map();
|
|
14
|
+
return {
|
|
15
|
+
async open(entry) {
|
|
16
|
+
let server = servers.get(entry);
|
|
17
|
+
if (!server) {
|
|
18
|
+
server = createDevServer(createX18nDevframe([entry], { openInEditor: async ({ path, line, column }) => {
|
|
19
|
+
const position = {
|
|
20
|
+
line: line - 1,
|
|
21
|
+
character: column - 1
|
|
22
|
+
};
|
|
23
|
+
await connection.window.showDocument({
|
|
24
|
+
uri: pathToFileURL(join(entry.root, path)).href,
|
|
25
|
+
takeFocus: true,
|
|
26
|
+
selection: {
|
|
27
|
+
start: position,
|
|
28
|
+
end: position
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
} }), {
|
|
32
|
+
host: "127.0.0.1",
|
|
33
|
+
port: 0,
|
|
34
|
+
auth: true,
|
|
35
|
+
openBrowser: false
|
|
36
|
+
});
|
|
37
|
+
servers.set(entry, server);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
url: `${(await server).origin}/`,
|
|
41
|
+
token: getTempAuthCodeInfo().code
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
async close() {
|
|
45
|
+
await Promise.all([...servers.values()].map(async (server) => (await server).close()));
|
|
46
|
+
servers.clear();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
//#endregion
|
|
51
|
+
export { createUiServers };
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@x18n/lsp",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.5",
|
|
5
5
|
"description": "x18n language server for any LSP editor",
|
|
6
6
|
"author": "Anthony Fu <anthonyfu117@hotmail.com>",
|
|
7
7
|
"license": "MIT",
|
|
8
|
-
"homepage": "https://github.com/
|
|
8
|
+
"homepage": "https://github.com/x18n-dev/x18n#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/
|
|
11
|
+
"url": "git+https://github.com/x18n-dev/x18n.git",
|
|
12
12
|
"directory": "packages/lsp"
|
|
13
13
|
},
|
|
14
|
-
"bugs": "https://github.com/
|
|
14
|
+
"bugs": "https://github.com/x18n-dev/x18n/issues",
|
|
15
15
|
"sideEffects": false,
|
|
16
16
|
"exports": {
|
|
17
17
|
".": "./dist/index.mjs",
|
|
@@ -29,16 +29,16 @@
|
|
|
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.
|
|
36
|
-
"devframe": "^1.2.
|
|
32
|
+
"@x18n/builtins": "0.0.5",
|
|
33
|
+
"@x18n/core": "0.0.5",
|
|
34
|
+
"@x18n/node": "0.0.5",
|
|
35
|
+
"@x18n/ui": "0.0.5",
|
|
36
|
+
"devframe": "^1.2.6",
|
|
37
37
|
"vscode-languageserver": "^10.1.2",
|
|
38
38
|
"vscode-languageserver-textdocument": "^1.0.15"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"vscode-jsonrpc": "^9.0.
|
|
41
|
+
"vscode-jsonrpc": "^9.0.3"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsdown",
|