@webgal/language-server 0.0.2-alpha.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.
@@ -0,0 +1,423 @@
1
+ import { n as getDiagnosticInformation, r as warningConfig, t as getState } from "./providerState-CLCXHs37.mjs";
2
+ import { GlobalMap, cleartGlobalMapAll, fsAccessor, runCode, source } from "@webgal/language-core";
3
+ import { DiagnosticSeverity, Position, Range } from "@volar/language-server";
4
+ import { FileType } from "@volar/language-service";
5
+
6
+ //#region src/utils/index.ts
7
+ function bindCoreFileAccessorToClientVfs(connection) {
8
+ const isWindows = typeof process !== "undefined" && !!process.platform && process.platform === "win32";
9
+ const encoder = new TextEncoder();
10
+ const decoder = new TextDecoder();
11
+ fsAccessor.isWindows = isWindows;
12
+ fsAccessor.readFile = async (path) => {
13
+ const content = await connection.sendRequest("client/vfs/readFile", path);
14
+ if (content === null) throw new Error("File not found");
15
+ return encoder.encode(content);
16
+ };
17
+ fsAccessor.writeFile = async (path, contents) => {
18
+ const content = decoder.decode(contents);
19
+ await connection.sendRequest("client/vfs/writeFile", {
20
+ path,
21
+ content
22
+ });
23
+ };
24
+ }
25
+ function getVariableTypeDesc(ALL_ARR, _start_line) {
26
+ let _desc_arr = [];
27
+ for (let _d_index = _start_line - 2; _d_index > 0; _d_index--) {
28
+ const _data = ALL_ARR[_d_index];
29
+ if (_data.startsWith(";") && _data.length > 0) _desc_arr.unshift(_data.substring(1));
30
+ else if (_data.length > 0) break;
31
+ else continue;
32
+ }
33
+ return _desc_arr.join("\n");
34
+ }
35
+ function getVariableType(expr) {
36
+ if (expr.includes("$stage") || expr.includes("$userData")) return "expression";
37
+ return typeof runCode(expr)();
38
+ }
39
+ /** 获取位置的指令单词 */
40
+ function getWordAtPosition(doc, pos, charRegex) {
41
+ const text = doc.getText();
42
+ const offset = doc.offsetAt(pos);
43
+ if (offset < 0 || offset > text.length) return null;
44
+ const testChar = (ch) => {
45
+ if (!charRegex) return /[\p{L}\p{N}_]_?[\p{L}\p{N}_]*/u.test(ch);
46
+ const flags = (charRegex.flags || "").replace("g", "");
47
+ return new RegExp(charRegex.source, flags).test(ch);
48
+ };
49
+ let i = offset - 1;
50
+ while (i >= 0 && testChar(text.charAt(i))) i--;
51
+ const start = i + 1;
52
+ let j = offset;
53
+ while (j < text.length && testChar(text.charAt(j))) j++;
54
+ const end = j;
55
+ if (start >= end) return null;
56
+ return {
57
+ word: text.slice(start, end),
58
+ start,
59
+ end
60
+ };
61
+ }
62
+ function getPatternAtPosition(doc, pos, pattern, maxRadius = 512) {
63
+ const text = doc.getText();
64
+ const offset = doc.offsetAt(pos);
65
+ if (offset < 0 || offset > text.length) return null;
66
+ const cleanFlags = (pattern.flags || "").replace(/[gy]/g, "");
67
+ const re = new RegExp(pattern.source, cleanFlags + "g");
68
+ let startSearch = Math.max(0, offset - maxRadius);
69
+ const lookBehindLimit = Math.max(0, offset - maxRadius - 2048);
70
+ for (let i = offset - 1; i >= lookBehindLimit; i--) {
71
+ const char = text[i];
72
+ if (/[\s\[\(\;\,\>]/.test(char)) {
73
+ startSearch = i + 1;
74
+ break;
75
+ }
76
+ }
77
+ const endSearch = Math.min(text.length, offset + maxRadius);
78
+ const substr = text.slice(startSearch, endSearch);
79
+ let m;
80
+ re.lastIndex = 0;
81
+ while ((m = re.exec(substr)) !== null) {
82
+ const matchStart = startSearch + m.index;
83
+ const matchEnd = matchStart + m[0].length;
84
+ if (offset >= matchStart && offset <= matchEnd) {
85
+ const startPos = doc.positionAt(matchStart);
86
+ const endPos = doc.positionAt(matchEnd);
87
+ return {
88
+ text: m[0],
89
+ start: matchStart,
90
+ end: matchEnd,
91
+ startPos,
92
+ endPos,
93
+ groups: m
94
+ };
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+ function getTokenOrPatternAtPosition(doc, pos, charRegex, pattern) {
100
+ const basic = getWordAtPosition(doc, pos, charRegex);
101
+ if (basic && pattern) {
102
+ const flags = (pattern.flags || "").replace("g", "");
103
+ if (new RegExp(`^(?:${pattern.source})$`, flags).test(basic.word)) return basic;
104
+ }
105
+ if (basic) return basic;
106
+ if (pattern) {
107
+ const found = getPatternAtPosition(doc, pos, pattern);
108
+ if (found) return {
109
+ word: found.text,
110
+ start: found.start,
111
+ end: found.end
112
+ };
113
+ }
114
+ return null;
115
+ }
116
+ /** Helper: 判断是否属于 路径字符 */
117
+ function isPathChar(ch) {
118
+ return /[A-Za-z0-9_.\-\/~]/.test(ch);
119
+ }
120
+ /** 从 position 找到当前 token 的 start offset(用于 replacement range) */
121
+ function findTokenRange(doc, pos) {
122
+ const text = doc.getText();
123
+ const offset = doc.offsetAt(pos);
124
+ let i = offset - 1;
125
+ while (i >= 0 && isPathChar(text.charAt(i))) i--;
126
+ const start = i + 1;
127
+ let j = offset;
128
+ while (j < text.length && isPathChar(text.charAt(j))) j++;
129
+ const end = j;
130
+ return {
131
+ startOffset: start,
132
+ endOffset: end,
133
+ token: text.slice(start, end)
134
+ };
135
+ }
136
+ async function listPathCandidates(docUri, token) {
137
+ try {
138
+ if (!(typeof process !== "undefined" && !!process.versions && !!process.versions.node)) return [];
139
+ const [{ fileURLToPath }, path, fs] = await Promise.all([
140
+ import("url"),
141
+ import("path"),
142
+ import("fs/promises")
143
+ ]);
144
+ let filePath = null;
145
+ if (docUri.startsWith("file://")) filePath = fileURLToPath(docUri);
146
+ else return [];
147
+ const baseDir = path.dirname(filePath);
148
+ let resolvedBase;
149
+ let partialName = "";
150
+ if (token.startsWith("/")) {
151
+ const maybeDir = token.endsWith("/") ? token : path.dirname(token);
152
+ resolvedBase = path.resolve(maybeDir);
153
+ partialName = token.endsWith("/") ? "" : path.basename(token);
154
+ } else if (token.startsWith("~")) {
155
+ const homedir = process.env.HOME || process.env.USERPROFILE || "";
156
+ const afterTilde = token === "~" ? "" : token.slice(2);
157
+ const joined = path.join(homedir, afterTilde);
158
+ resolvedBase = path.dirname(joined);
159
+ partialName = path.basename(joined);
160
+ } else if (token.includes("/")) {
161
+ const tokenDir = token.slice(0, token.lastIndexOf("/"));
162
+ partialName = token.slice(token.lastIndexOf("/") + 1);
163
+ resolvedBase = path.resolve(baseDir, tokenDir || ".");
164
+ } else {
165
+ resolvedBase = baseDir;
166
+ partialName = token;
167
+ }
168
+ return (await fs.readdir(resolvedBase, { withFileTypes: true }).catch(() => [])).filter((e) => e.name.startsWith(partialName)).map((e) => {
169
+ const isDir = e.isDirectory();
170
+ const name = e.name + (isDir ? "/" : "");
171
+ let insertText;
172
+ if (token.includes("/")) insertText = token.slice(0, token.lastIndexOf("/") + 1) + e.name + (isDir ? "/" : "");
173
+ else insertText = e.name + (isDir ? "/" : "");
174
+ return {
175
+ label: name,
176
+ insertText,
177
+ isDirectory: isDir
178
+ };
179
+ });
180
+ } catch (err) {
181
+ return [];
182
+ }
183
+ }
184
+ function getStageCompletionContext(document, cursorPos, match) {
185
+ const cursorOffset = document.offsetAt(cursorPos);
186
+ const relCursor = Math.max(0, Math.min(cursorOffset - match.start, match.text.length));
187
+ const rawBeforeCursor = match.text.slice(0, relCursor);
188
+ const lastDotIndex = rawBeforeCursor.lastIndexOf(".");
189
+ const replaceStartOffset = match.start + (lastDotIndex === -1 ? 0 : lastDotIndex + 1);
190
+ const replaceRange = Range.create(document.positionAt(replaceStartOffset), cursorPos);
191
+ const normalizedBeforeCursor = rawBeforeCursor.startsWith("$") ? rawBeforeCursor.slice(1) : rawBeforeCursor;
192
+ const rawSegments = normalizedBeforeCursor.split(".");
193
+ const fullSegments = rawSegments.filter((part) => part);
194
+ const hasTrailingDot = normalizedBeforeCursor.endsWith(".");
195
+ const prefix = hasTrailingDot ? "" : rawSegments[rawSegments.length - 1] || "";
196
+ return {
197
+ replaceRange,
198
+ fullSegments,
199
+ querySegments: hasTrailingDot ? fullSegments : fullSegments.slice(0, -1),
200
+ prefix
201
+ };
202
+ }
203
+ /** 更新全局映射表 */
204
+ function updateGlobalMap(documentTextArray) {
205
+ cleartGlobalMapAll();
206
+ for (let lineNumber = 0; lineNumber < documentTextArray.length; lineNumber++) {
207
+ const currentLine = documentTextArray[lineNumber];
208
+ const setVarExec = /setVar:\s*(\w+)\s*=\s*([^;]*\S+);?/g.exec(currentLine);
209
+ const labelExec = /label:\s*(\S+);/g.exec(currentLine);
210
+ const getUserInputExec = /getUserInput:\s*([^\s;]+)/g.exec(currentLine);
211
+ const chooseExec = /choose:\s*([^\s;]+)/g.exec(currentLine);
212
+ if (setVarExec !== null) {
213
+ const currentVariablePool = GlobalMap.setVar[setVarExec[1]] ??= [];
214
+ const isGlobal = currentLine.indexOf("-global") === -1 ? false : true;
215
+ currentVariablePool.push({
216
+ word: setVarExec[1],
217
+ value: setVarExec[2],
218
+ input: setVarExec.input,
219
+ isGlobal,
220
+ isGetUserInput: false,
221
+ position: Position.create(lineNumber, setVarExec.index + 7)
222
+ });
223
+ const currentVariableLatest = currentVariablePool[currentVariablePool.length - 1];
224
+ if (currentVariableLatest && currentVariableLatest?.position) {
225
+ const _v_pos = currentVariableLatest.position;
226
+ currentVariableLatest.desc = getVariableTypeDesc(documentTextArray, _v_pos?.line ? _v_pos.line : -1);
227
+ }
228
+ } else if (labelExec !== null) (GlobalMap.label[labelExec[1]] ??= []).push({
229
+ word: labelExec[1],
230
+ value: labelExec.input,
231
+ input: labelExec.input,
232
+ position: Position.create(lineNumber, 6)
233
+ });
234
+ else if (getUserInputExec !== null) (GlobalMap.setVar[getUserInputExec[1]] ??= []).push({
235
+ word: getUserInputExec[1],
236
+ value: getUserInputExec.input,
237
+ input: getUserInputExec.input,
238
+ isGetUserInput: true,
239
+ position: Position.create(lineNumber, 13)
240
+ });
241
+ else if (chooseExec !== null) {
242
+ const options = [];
243
+ const text = chooseExec[1];
244
+ for (const machChooseOption of text.split("|")) {
245
+ const sliceArray = machChooseOption.split(":");
246
+ options.push({
247
+ text: sliceArray[0]?.trim(),
248
+ value: sliceArray[1]?.trim()
249
+ });
250
+ }
251
+ GlobalMap.choose[lineNumber] = {
252
+ options,
253
+ line: lineNumber
254
+ };
255
+ }
256
+ }
257
+ }
258
+ const defaultSettings = {
259
+ maxNumberOfProblems: 1e3,
260
+ isShowWarning: true,
261
+ isShowHint: "变量名后"
262
+ };
263
+ const documentSettings = /* @__PURE__ */ new Map();
264
+ const StateConfig = {
265
+ hasConfigurationCapability: false,
266
+ hasWorkspaceFolderCapability: false,
267
+ hasDiagnosticRelatedInformationCapability: false
268
+ };
269
+ let globalSettings = defaultSettings;
270
+ function setGlobalSettings(settings) {
271
+ globalSettings = settings;
272
+ }
273
+ function getDocumentSettings(connection, url) {
274
+ if (!StateConfig.hasConfigurationCapability) return Promise.resolve(globalSettings);
275
+ let result = documentSettings.get(url);
276
+ if (!result) {
277
+ result = connection.workspace.getConfiguration({
278
+ scopeUri: url,
279
+ section: "WEBGAL Language Server"
280
+ });
281
+ documentSettings.set(url, result);
282
+ }
283
+ return result;
284
+ }
285
+ async function validateTextDocument(connection, textDocument) {
286
+ const settings = await getDocumentSettings(connection, textDocument.uri);
287
+ if (!settings?.isShowWarning) return [];
288
+ const text = textDocument.getText();
289
+ let m;
290
+ let problems = 0;
291
+ const diagnostics = [];
292
+ let _sp = text.split(/\n|\t\n|\r\n/);
293
+ for (let i in warningConfig) {
294
+ const _token = warningConfig[i];
295
+ const _pattern = _token.pattern;
296
+ if (_token.is_line) continue;
297
+ if (_token.customCheck && _token.customCheck instanceof Function) {
298
+ const _custom_res = _token.customCheck(textDocument, text);
299
+ if (typeof _custom_res === "object" && _custom_res !== null) diagnostics.push(_custom_res);
300
+ continue;
301
+ }
302
+ while ((m = _pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
303
+ if (_token?.enable === false) continue;
304
+ problems++;
305
+ const range = {
306
+ start: textDocument.positionAt(m.index),
307
+ end: textDocument.positionAt(m.index + m[0].length)
308
+ };
309
+ const diagnostic = {
310
+ severity: DiagnosticSeverity.Warning,
311
+ range,
312
+ message: `(${i})${m[0].trim()}`,
313
+ source
314
+ };
315
+ if (StateConfig.hasDiagnosticRelatedInformationCapability) diagnostic.relatedInformation = [{
316
+ location: {
317
+ uri: textDocument.uri,
318
+ range: Object.assign({}, diagnostic.range)
319
+ },
320
+ message: getDiagnosticInformation(i)
321
+ }];
322
+ diagnostics.push(diagnostic);
323
+ }
324
+ }
325
+ for (let _line_index = 0; _line_index < _sp.length; _line_index++) {
326
+ const _line_text = _sp[_line_index];
327
+ for (let i in warningConfig) {
328
+ const _token = warningConfig[i];
329
+ const _pattern = _token.pattern;
330
+ if (!_token.is_line) continue;
331
+ const _newarr = _sp.slice(0, _line_index).join();
332
+ if (_token.customCheck && _token.customCheck instanceof Function) {
333
+ const _custom_res = _token.customCheck(textDocument, _line_text, _newarr.length, _sp.slice(0, _line_index));
334
+ if (typeof _custom_res === "object" && _custom_res !== null) diagnostics.push(_custom_res);
335
+ continue;
336
+ }
337
+ while ((m = _pattern.exec(_line_text)) && problems < settings.maxNumberOfProblems) {
338
+ if (_token?.enable === false) continue;
339
+ problems++;
340
+ const range = {
341
+ start: textDocument.positionAt(_newarr.length + 1),
342
+ end: textDocument.positionAt(_newarr.length + m.input.length)
343
+ };
344
+ const diagnostic = {
345
+ severity: DiagnosticSeverity.Warning,
346
+ range,
347
+ message: `(${i})${m.input.trim()}`,
348
+ source: "WebGal Script"
349
+ };
350
+ if (StateConfig.hasDiagnosticRelatedInformationCapability) diagnostic.relatedInformation = [{
351
+ location: {
352
+ uri: textDocument.uri,
353
+ range: Object.assign({}, diagnostic.range)
354
+ },
355
+ message: getDiagnosticInformation(i)
356
+ }];
357
+ diagnostics.push(diagnostic);
358
+ }
359
+ }
360
+ }
361
+ return diagnostics;
362
+ }
363
+ function createVolarFileSystemFromVirtualFileSystem(vfs, options) {
364
+ const uriToPath = options?.uriToPath ?? ((uri) => uri.scheme === "file" ? uri.fsPath : uri.path);
365
+ return {
366
+ stat: async (uri) => {
367
+ const pathValue = uriToPath(uri);
368
+ const info = await vfs.stat(pathValue);
369
+ if (!info) return;
370
+ return {
371
+ type: info.isDirectory ? FileType.Directory : info.isFile ? FileType.File : FileType.Unknown,
372
+ ctime: 0,
373
+ mtime: 0,
374
+ size: 0
375
+ };
376
+ },
377
+ readFile: async (uri) => {
378
+ const pathValue = uriToPath(uri);
379
+ return await vfs.readFile(pathValue) ?? void 0;
380
+ },
381
+ readDirectory: async (uri) => {
382
+ const pathValue = uriToPath(uri);
383
+ const entries = await vfs.readDirectory(pathValue);
384
+ if (!entries) return [];
385
+ return entries.map((entry) => [entry.name, entry.isDirectory ? FileType.Directory : FileType.File]);
386
+ }
387
+ };
388
+ }
389
+ function createClientVfsFileSystem(connection, options) {
390
+ const uriToPath = options?.uriToPath ?? ((uri) => {
391
+ if (uri.scheme === "file") {
392
+ const pathValue = uri.path;
393
+ if (/^\/[a-zA-Z]:\//.test(pathValue)) return pathValue.slice(1);
394
+ return pathValue;
395
+ }
396
+ return uri.path;
397
+ });
398
+ return {
399
+ stat: async (uri) => {
400
+ const pathValue = uriToPath(uri);
401
+ const info = await connection.sendRequest("client/FStat", pathValue);
402
+ if (!info) return;
403
+ return {
404
+ type: info.isDirectory ? FileType.Directory : info.isFile ? FileType.File : FileType.Unknown,
405
+ ctime: 0,
406
+ mtime: 0,
407
+ size: 0
408
+ };
409
+ },
410
+ readFile: async (uri) => {
411
+ const pathValue = uriToPath(uri);
412
+ return await connection.sendRequest("client/vfs/readFile", pathValue) ?? void 0;
413
+ },
414
+ readDirectory: async (uri) => {
415
+ const entries = await connection.sendRequest("client/readDirectory", uri.toString());
416
+ if (!entries) return [];
417
+ return entries.map((entry) => [entry.name, entry.isDirectory ? FileType.Directory : FileType.File]);
418
+ }
419
+ };
420
+ }
421
+
422
+ //#endregion
423
+ export { StateConfig, bindCoreFileAccessorToClientVfs, createClientVfsFileSystem, createVolarFileSystemFromVirtualFileSystem, defaultSettings, documentSettings, findTokenRange, getDocumentSettings, getPatternAtPosition, getStageCompletionContext, getState, getTokenOrPatternAtPosition, getVariableType, getVariableTypeDesc, getWordAtPosition, globalSettings, isPathChar, listPathCandidates, setGlobalSettings, updateGlobalMap, validateTextDocument };
package/package.json ADDED
@@ -0,0 +1,72 @@
1
+ {
2
+ "name": "@webgal/language-server",
3
+ "version": "0.0.2-alpha.0",
4
+ "main": "build/index.cjs",
5
+ "module": "build/index.mjs",
6
+ "types": "build/index.d.mts",
7
+ "license": "MPL-2.0",
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "registry": "https://registry.npmjs.org/"
11
+ },
12
+ "files": [
13
+ "build/**/*",
14
+ "bin"
15
+ ],
16
+ "exports": {
17
+ ".": {
18
+ "types": "./build/index.d.mts",
19
+ "browser": "./build/browser.mjs",
20
+ "import": "./build/index.mjs",
21
+ "require": "./build/index.cjs"
22
+ },
23
+ "./utils": {
24
+ "types": "./build/utils.d.mts",
25
+ "import": "./build/utils.mjs",
26
+ "require": "./build/utils.cjs"
27
+ },
28
+ "./browser": {
29
+ "types": "./build/browser.d.mts",
30
+ "import": "./build/browser.mjs",
31
+ "require": "./build/browser.cjs"
32
+ },
33
+ "./browser-worker": {
34
+ "types": "./build/browser.worker.d.mts",
35
+ "import": "./build/browser.worker.mjs",
36
+ "require": "./build/browser.worker.cjs"
37
+ }
38
+ },
39
+ "bin": {
40
+ "webgal-language-server": "./bin/webgal-language-server.js"
41
+ },
42
+ "scripts": {
43
+ "build": "tsdown",
44
+ "gen": "tsx scripts/gen.ts",
45
+ "dev": "pnpm build && node bin/webgal-language-server.js --stdio",
46
+ "dev:ws": "pnpm build && node bin/webgal-language-server.js --ws=3001",
47
+ "format": " prettier --write \"src/**/*.ts\"",
48
+ "lint": "eslint src --ext ts --fix",
49
+ "typecheck": "tsc --noEmit",
50
+ "watch": "tsdown --watch"
51
+ },
52
+ "dependencies": {
53
+ "@volar/language-core": "~2.4.0",
54
+ "@volar/language-server": "~2.4.0",
55
+ "@volar/language-service": "~2.4.0",
56
+ "@volar/typescript": "~2.4.0",
57
+ "@webgal/language-core": "0.0.2-alpha.0",
58
+ "@webgal/language-service": "0.0.2-alpha.0",
59
+ "tsc-alias": "^1.8.16",
60
+ "vscode-languageserver": "^9.0.1",
61
+ "vscode-languageserver-textdocument": "^1.0.11",
62
+ "vscode-uri": "^3.0.8",
63
+ "vscode-ws-jsonrpc": "^3.4.0",
64
+ "ws": "^8.18.3"
65
+ },
66
+ "devDependencies": {
67
+ "@types/ws": "^8.18.1",
68
+ "tsdown": "^0.20.3",
69
+ "tsx": "^4.21.0"
70
+ },
71
+ "gitHead": "2efa929ed7f0f62a003ab621e15e279651362d2d"
72
+ }