luaut-language-server 1.1.0 → 2.0.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.cjs CHANGED
@@ -64,19 +64,83 @@ var import_vscode_languageserver_textdocument = require("vscode-languageserver-t
64
64
  var import_node_fs = require("fs");
65
65
  var import_node_path = require("path");
66
66
  var import_node_url = require("url");
67
+ var import_luaut_parser2 = require("luaut-parser");
68
+
69
+ // src/features/members.ts
67
70
  var import_luaut_parser = require("luaut-parser");
71
+ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
72
+ if (!type || seen.has(type)) return [];
73
+ seen.add(type);
74
+ switch (type.kind) {
75
+ case "object": {
76
+ const out = [];
77
+ for (const [name, property] of type.properties) {
78
+ out.push({ name, property, isMethod: takesSelf(property.type) });
79
+ }
80
+ return out;
81
+ }
82
+ case "intersection": {
83
+ const merged = /* @__PURE__ */ new Map();
84
+ for (const part of type.types) {
85
+ for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
86
+ }
87
+ return [...merged.values()];
88
+ }
89
+ case "union": {
90
+ const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
91
+ if (!perBranch.length) return [];
92
+ const [first, ...rest] = perBranch;
93
+ return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
94
+ }
95
+ case "genericRef": {
96
+ const alias = aliases.get(type.name);
97
+ return alias ? membersOf(alias, aliases, seen) : [];
98
+ }
99
+ case "typeParam":
100
+ return membersOf(type.constraint, aliases, seen);
101
+ default:
102
+ return [];
103
+ }
104
+ }
105
+ function takesSelf(type) {
106
+ for (const signature of signaturesOf(type)) {
107
+ if (signature.params[0]?.name === "self") return true;
108
+ }
109
+ return false;
110
+ }
111
+ function signaturesOf(type, aliases) {
112
+ if (!type) return [];
113
+ if (type.kind === "function") return [type];
114
+ if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
115
+ if (type.kind === "genericRef" && aliases) {
116
+ const alias = aliases.get(type.name);
117
+ return alias ? signaturesOf(alias, aliases) : [];
118
+ }
119
+ return [];
120
+ }
121
+ function signatureLabel(signature) {
122
+ const parameters = signature.params.map((p, i) => {
123
+ const name = p.name ?? `arg${i + 1}`;
124
+ return `${name}${p.optional ? "?" : ""}: ${(0, import_luaut_parser.formatType)(p.type)}`;
125
+ });
126
+ const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
127
+ const varargs = signature.varargs ? [`...: ${(0, import_luaut_parser.formatType)(signature.varargs)}`] : [];
128
+ const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${(0, import_luaut_parser.formatType)(signature.returns)}`;
129
+ return { label, parameters };
130
+ }
131
+
132
+ // src/analysis.ts
68
133
  function globalsOf(libs) {
69
134
  const names = /* @__PURE__ */ new Set();
70
- for (const lib of libs) collect(lib.body.statements, names);
71
- return [...names];
72
- }
73
- function collect(statements, into) {
74
- for (const statement of statements) {
75
- if (statement.type === "DeclareStatement") into.add(statement.name);
135
+ for (const lib of libs) {
136
+ for (const statement of lib.body.statements) {
137
+ if (statement.type === "DeclareStatement") names.add(statement.name);
138
+ }
76
139
  }
140
+ return [...names];
77
141
  }
78
142
  function bindingOfNode(analysis, node) {
79
- const used = (0, import_luaut_parser.getBinding)(analysis.scopes, node);
143
+ const used = (0, import_luaut_parser2.getBinding)(analysis.scopes, node);
80
144
  if (used) return used;
81
145
  return declarationIndex(analysis).get(node);
82
146
  }
@@ -111,20 +175,34 @@ function pathKey(path) {
111
175
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
112
176
  }
113
177
  var CYCLE = { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
178
+ var NO_PROJECT = { project: { fixed: false, problems: [] }, libs: [], globals: [], reads: /* @__PURE__ */ new Map() };
114
179
  var Analyzer = class {
115
- libs;
116
- builtinGlobals;
180
+ fixed;
117
181
  openDocument;
118
182
  cache = /* @__PURE__ */ new Map();
119
183
  /** Imported modules, by path key. */
120
184
  modules = /* @__PURE__ */ new Map();
185
+ /** Project contexts, by folder. */
186
+ contexts = /* @__PURE__ */ new Map();
187
+ /** Parsed type libraries, by path — reparsed only when the text changes. */
188
+ libraries = /* @__PURE__ */ new Map();
189
+ /** Sourcemaps turned into types, by path, with what they were built from. */
190
+ sourceMaps = /* @__PURE__ */ new Map();
191
+ /** The analysis run in progress, if any. */
192
+ run;
121
193
  constructor(options = {}) {
122
- this.libs = options.libs ?? import_luaut_parser.defaultLibs;
123
- this.builtinGlobals = globalsOf(this.libs);
124
194
  this.openDocument = options.openDocument;
195
+ if (options.libs) {
196
+ this.fixed = {
197
+ project: { fixed: true, problems: [] },
198
+ libs: options.libs,
199
+ globals: globalsOf(options.libs),
200
+ reads: /* @__PURE__ */ new Map()
201
+ };
202
+ }
125
203
  }
126
204
  /** Analyze `document`, reusing the previous result while neither it nor
127
- * anything it imports has changed. */
205
+ * anything it read has changed. */
128
206
  get(document) {
129
207
  const cached = this.cache.get(document.uri);
130
208
  const source = document.getText();
@@ -139,34 +217,41 @@ var Analyzer = class {
139
217
  * completion, which analyzes a speculatively edited copy of the file. */
140
218
  analyze(uri, version, source) {
141
219
  const path = pathOfUri(uri);
142
- return this.analyzeModule(uri, version, source, new Set(path ? [pathKey(path)] : []));
220
+ if (!path) return this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set());
221
+ const key = pathKey(path);
222
+ return this.resolvingCycles(key, () => {
223
+ const analysis = this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set([key]));
224
+ return { result: analysis, exports: () => this.exportsFrom(analysis, /* @__PURE__ */ new Set([key])) };
225
+ });
143
226
  }
144
227
  forget(uri) {
145
228
  this.cache.delete(uri);
146
229
  }
147
- /** The file an import in `fromUri` names. Relative paths only (`./x`,
148
- * `../x`); the extension may be left off, and a folder means its
149
- * `index.luaut`. */
150
- resolveModulePath(fromUri, specifier) {
151
- return this.moduleCandidates(fromUri, specifier).find((candidate) => this.sourceOf(candidate) !== void 0);
230
+ /** The project a file belongs to. */
231
+ projectOf(uri) {
232
+ return this.contextFor(pathOfUri(uri)).project;
152
233
  }
153
- /** Every file an import could mean, in the order they are tried. */
154
- moduleCandidates(fromUri, specifier) {
234
+ /** The file an import in `fromUri` names: a relative path, or a `paths`
235
+ * alias from the file's config. */
236
+ resolveModulePath(fromUri, specifier) {
155
237
  const from = pathOfUri(fromUri);
156
- if (!from || !(specifier.startsWith("./") || specifier.startsWith("../"))) return [];
157
- const base = (0, import_node_path.resolve)((0, import_node_path.dirname)(from), specifier);
158
- return specifier.endsWith(".luaut") ? [base] : [`${base}.luaut`, `${base}.d.luaut`, (0, import_node_path.join)(base, "index.luaut")];
238
+ if (!from) return void 0;
239
+ return this.candidatesFor(from, specifier).find((candidate) => this.readFile(candidate) !== void 0);
159
240
  }
160
241
  /** What the module at `path` exports, analyzing it if need be. */
161
242
  exportsAt(path) {
162
- return this.exportsOf(path, /* @__PURE__ */ new Set());
243
+ return this.resolvingCycles(pathKey(path), () => {
244
+ const exports2 = this.exportsOf(path, /* @__PURE__ */ new Set());
245
+ return { result: exports2, exports: () => exports2 };
246
+ });
163
247
  }
164
248
  /** The analysis of the module at `path`, analyzing it if need be. */
165
249
  moduleAt(path) {
166
- this.exportsOf(path, /* @__PURE__ */ new Set());
250
+ this.exportsAt(path);
167
251
  return this.modules.get(pathKey(path))?.analysis;
168
252
  }
169
- sourceOf(path) {
253
+ /** A file's text: the open document if there is one, else the disk. */
254
+ readFile(path) {
170
255
  const open = this.openDocument?.(path);
171
256
  if (open) return open.getText();
172
257
  try {
@@ -175,60 +260,206 @@ var Analyzer = class {
175
260
  return void 0;
176
261
  }
177
262
  }
263
+ candidatesFor(from, specifier) {
264
+ return (0, import_luaut_parser2.moduleCandidates)(from, specifier, this.contextFor(from).project.config);
265
+ }
266
+ // ---------------------------------------------------------------- projects
267
+ contextFor(path) {
268
+ if (this.fixed) return this.fixed;
269
+ if (!path) return NO_PROJECT;
270
+ const key = pathKey((0, import_node_path.dirname)(path));
271
+ const cached = this.contexts.get(key);
272
+ if (cached && this.unchanged(cached.reads)) return cached;
273
+ const context = this.buildContext(path);
274
+ this.contexts.set(key, context);
275
+ return context;
276
+ }
277
+ buildContext(path) {
278
+ const reads = /* @__PURE__ */ new Map();
279
+ const host = {
280
+ readFile: (file) => {
281
+ const text = this.readFile(file);
282
+ reads.set(file, text);
283
+ return text;
284
+ }
285
+ };
286
+ const lookup = (0, import_luaut_parser2.findConfig)(path, host);
287
+ const problems = [...lookup.problems];
288
+ const config = lookup.config;
289
+ if (!config) return { project: { fixed: false, problems }, libs: [], globals: [], reads };
290
+ const libraries = (0, import_luaut_parser2.resolveTypeLibraries)(config, host);
291
+ problems.push(...libraries.problems);
292
+ const libs = [];
293
+ for (const file of libraries.files) {
294
+ const program = this.library(file, host, problems);
295
+ if (program) libs.push(program);
296
+ }
297
+ let sourceMap;
298
+ if (config.sourceMap) {
299
+ const text = host.readFile(config.sourceMap);
300
+ if (text === void 0) {
301
+ problems.push({
302
+ file: config.path,
303
+ message: `Cannot find the sourceMap file ${config.sourceMap}`,
304
+ ...optionPosition(config, "sourceMap")
305
+ });
306
+ } else {
307
+ const result = this.sourceMap(config.sourceMap, text, libs, libraries.files);
308
+ if (result.problem) problems.push({ file: config.sourceMap, message: result.problem, line: 1, column: 1 });
309
+ sourceMap = result.types;
310
+ if (sourceMap) libs.push(sourceMap.program);
311
+ }
312
+ }
313
+ return { project: { config, fixed: false, problems }, libs, globals: globalsOf(libs), sourceMap, reads };
314
+ }
315
+ /** A type library's definitions, parsed once per text. */
316
+ library(file, host, problems) {
317
+ const source = host.readFile(file);
318
+ if (source === void 0) return void 0;
319
+ const key = pathKey(file);
320
+ let entry = this.libraries.get(key);
321
+ if (!entry || entry.source !== source) {
322
+ try {
323
+ entry = { source, program: (0, import_luaut_parser2.parse)(source) };
324
+ } catch (error) {
325
+ const { message, line, column } = error;
326
+ entry = {
327
+ source,
328
+ problem: { file, message: `Syntax error in type library: ${message.replace(/\s*\(\d+:\d+\)$/, "")}`, line, column }
329
+ };
330
+ }
331
+ this.libraries.set(key, entry);
332
+ }
333
+ if (entry.problem) problems.push(entry.problem);
334
+ return entry.program;
335
+ }
336
+ /** A sourcemap's types, rebuilt only when it or the libraries change. */
337
+ sourceMap(path, text, libs, files) {
338
+ const key = pathKey(path);
339
+ const libraries = files.join("\n");
340
+ const cached = this.sourceMaps.get(key);
341
+ if (cached && cached.text === text && cached.libraries === libraries) return cached.result;
342
+ const aliases = aliasesOf(libs);
343
+ const members = /* @__PURE__ */ new Map();
344
+ const result = (0, import_luaut_parser2.sourceMapTypes)(text, path, {
345
+ classes: new Set(aliases.keys()),
346
+ membersOf: (className) => {
347
+ let names = members.get(className);
348
+ if (!names) {
349
+ names = new Set(membersOf(aliases.get(className), aliases).map((member) => member.name));
350
+ members.set(className, names);
351
+ }
352
+ return names;
353
+ }
354
+ });
355
+ this.sourceMaps.set(key, { text, libraries, result });
356
+ return result;
357
+ }
358
+ unchanged(reads) {
359
+ for (const [file, text] of reads) if (this.readFile(file) !== text) return false;
360
+ return true;
361
+ }
362
+ // ----------------------------------------------------------------- modules
363
+ /** Run one analysis of `root` and everything it imports; if that met an
364
+ * import cycle, run it once more with the first pass's exports standing
365
+ * in for the `any` the cycle left (see `Run`). A call made while a run is
366
+ * already going is part of that run. */
367
+ resolvingCycles(root, analyzeRoot) {
368
+ if (this.run) return analyzeRoot().result;
369
+ const run = { cycles: /* @__PURE__ */ new Set(), analyzed: /* @__PURE__ */ new Set(), provisional: /* @__PURE__ */ new Map() };
370
+ this.run = run;
371
+ try {
372
+ const first = analyzeRoot();
373
+ if (!run.cycles.size) return first.result;
374
+ for (const key of run.cycles) {
375
+ const exports2 = key === root ? first.exports() : this.modules.get(key)?.exports;
376
+ if (exports2 && !exports2.partial) run.provisional.set(key, exports2);
377
+ }
378
+ for (const key of run.analyzed) this.modules.delete(key);
379
+ return analyzeRoot().result;
380
+ } finally {
381
+ this.run = void 0;
382
+ }
383
+ }
384
+ /** A module's exports, from its analysis. */
385
+ exportsFrom(analysis, importing) {
386
+ return (0, import_luaut_parser2.moduleExports)(analysis.program, analysis.scopes, analysis.types, (specifier) => {
387
+ const next = this.resolveModulePath(analysis.uri, specifier);
388
+ return next ? this.exportsOf(next, importing) : void 0;
389
+ });
390
+ }
178
391
  /** `importing` holds every module on the current import chain, so an
179
392
  * import back into one of them is recognized as a cycle. */
180
393
  analyzeModule(uri, version, source, importing) {
181
- const { program, errors } = (0, import_luaut_parser.parseWithRecovery)(source);
182
- const scopes = (0, import_luaut_parser.analyzeScopes)(program, { builtinGlobals: this.builtinGlobals });
183
- const dependencies = /* @__PURE__ */ new Map();
184
- const types = (0, import_luaut_parser.analyzeTypes)(program, scopes, {
185
- libs: this.libs,
394
+ const path = pathOfUri(uri);
395
+ const context = this.contextFor(path);
396
+ const script = path ? context.sourceMap?.scriptFor(path) : void 0;
397
+ const libs = script ? [...context.libs, script] : context.libs;
398
+ const globals = script ? [...context.globals, "script"] : context.globals;
399
+ const { program, errors } = (0, import_luaut_parser2.parseWithRecovery)(source);
400
+ const scopes = (0, import_luaut_parser2.analyzeScopes)(program, { builtinGlobals: [...globals] });
401
+ const dependencies = new Map(context.reads);
402
+ const types = (0, import_luaut_parser2.analyzeTypes)(program, scopes, {
403
+ libs,
186
404
  resolveModule: (specifier) => {
187
- const target = this.resolveModulePath(uri, specifier);
405
+ if (!path) return void 0;
406
+ const candidates = this.candidatesFor(path, specifier);
407
+ const target = candidates.find((candidate) => this.readFile(candidate) !== void 0);
188
408
  if (!target) {
189
- for (const candidate of this.moduleCandidates(uri, specifier)) dependencies.set(candidate, void 0);
409
+ for (const candidate of candidates) dependencies.set(candidate, void 0);
190
410
  return void 0;
191
411
  }
192
412
  const exports2 = this.exportsOf(target, importing);
193
- dependencies.set(target, this.sourceOf(target));
413
+ dependencies.set(target, this.readFile(target));
194
414
  return exports2;
195
415
  }
196
416
  });
197
- return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies };
417
+ return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
198
418
  }
199
419
  exportsOf(path, importing) {
200
420
  const key = pathKey(path);
201
- if (importing.has(key)) return CYCLE;
202
- const source = this.sourceOf(path);
421
+ if (importing.has(key)) {
422
+ this.run?.cycles.add(key);
423
+ return this.run?.provisional.get(key) ?? CYCLE;
424
+ }
425
+ const source = this.readFile(path);
203
426
  if (source === void 0) return void 0;
204
427
  const cached = this.modules.get(key);
205
428
  if (cached && cached.analysis.source === source && this.isFresh(cached.analysis)) return cached.exports;
206
429
  importing.add(key);
207
430
  try {
208
431
  const analysis = this.analyzeModule(uriOfPath(path), -1, source, importing);
209
- const exports2 = (0, import_luaut_parser.moduleExports)(analysis.program, analysis.scopes, analysis.types, (specifier) => {
210
- const next = this.resolveModulePath(analysis.uri, specifier);
211
- return next ? this.exportsOf(next, importing) : void 0;
212
- });
432
+ const exports2 = this.exportsFrom(analysis, importing);
213
433
  this.modules.set(key, { analysis, exports: exports2 });
434
+ this.run?.analyzed.add(key);
214
435
  return exports2;
215
436
  } finally {
216
437
  importing.delete(key);
217
438
  }
218
439
  }
219
- /** Does every module `analysis` imported — and everything those import
220
- * still have the text it was analyzed against? */
440
+ /** Does every file `analysis` read — and everything the modules it
441
+ * imported read — still have the text it was analyzed against? */
221
442
  isFresh(analysis, seen = /* @__PURE__ */ new Set()) {
222
443
  if (seen.has(analysis)) return true;
223
444
  seen.add(analysis);
224
445
  for (const [path, source] of analysis.dependencies) {
225
- if (this.sourceOf(path) !== source) return false;
446
+ if (this.readFile(path) !== source) return false;
226
447
  const module2 = this.modules.get(pathKey(path));
227
448
  if (module2 && !this.isFresh(module2.analysis, seen)) return false;
228
449
  }
229
450
  return true;
230
451
  }
231
452
  };
453
+ function aliasesOf(libs) {
454
+ const empty = (0, import_luaut_parser2.parse)("");
455
+ return (0, import_luaut_parser2.analyzeTypes)(empty, (0, import_luaut_parser2.analyzeScopes)(empty, {}), { libs, diagnostics: false }).aliases;
456
+ }
457
+ function optionPosition(config, key) {
458
+ const offset = config.source.indexOf(JSON.stringify(key));
459
+ if (offset < 0) return { line: 1, column: 1 };
460
+ const before = config.source.slice(0, offset);
461
+ return { line: before.split("\n").length, column: offset - before.lastIndexOf("\n") };
462
+ }
232
463
 
233
464
  // src/features/imports.ts
234
465
  var import_node_fs2 = require("fs");
@@ -264,16 +495,16 @@ function containsPosition(node, pos, inclusive = false) {
264
495
  }
265
496
  function children(node) {
266
497
  const out = [];
267
- collect2(node, out);
498
+ collect(node, out);
268
499
  return out;
269
500
  }
270
- function collect2(container, out) {
501
+ function collect(container, out) {
271
502
  for (const key of Object.keys(container)) {
272
503
  if (key === "line" || key === "column") continue;
273
504
  const value = container[key];
274
505
  for (const item of Array.isArray(value) ? value : [value]) {
275
506
  if (isSpanned(item)) out.push(item);
276
- else if (isSpanlessNode(item)) collect2(item, out);
507
+ else if (isSpanlessNode(item)) collect(item, out);
277
508
  }
278
509
  }
279
510
  }
@@ -314,69 +545,6 @@ function walk(root, visit, parent) {
314
545
  for (const child of children(root)) walk(child, visit, root);
315
546
  }
316
547
 
317
- // src/features/members.ts
318
- var import_luaut_parser2 = require("luaut-parser");
319
- function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
320
- if (!type || seen.has(type)) return [];
321
- seen.add(type);
322
- switch (type.kind) {
323
- case "object": {
324
- const out = [];
325
- for (const [name, property] of type.properties) {
326
- out.push({ name, property, isMethod: takesSelf(property.type) });
327
- }
328
- return out;
329
- }
330
- case "intersection": {
331
- const merged = /* @__PURE__ */ new Map();
332
- for (const part of type.types) {
333
- for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
334
- }
335
- return [...merged.values()];
336
- }
337
- case "union": {
338
- const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
339
- if (!perBranch.length) return [];
340
- const [first, ...rest] = perBranch;
341
- return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
342
- }
343
- case "genericRef": {
344
- const alias = aliases.get(type.name);
345
- return alias ? membersOf(alias, aliases, seen) : [];
346
- }
347
- case "typeParam":
348
- return membersOf(type.constraint, aliases, seen);
349
- default:
350
- return [];
351
- }
352
- }
353
- function takesSelf(type) {
354
- for (const signature of signaturesOf(type)) {
355
- if (signature.params[0]?.name === "self") return true;
356
- }
357
- return false;
358
- }
359
- function signaturesOf(type, aliases) {
360
- if (!type) return [];
361
- if (type.kind === "function") return [type];
362
- if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
363
- if (type.kind === "genericRef" && aliases) {
364
- const alias = aliases.get(type.name);
365
- return alias ? signaturesOf(alias, aliases) : [];
366
- }
367
- return [];
368
- }
369
- function signatureLabel(signature) {
370
- const parameters = signature.params.map((p, i) => {
371
- const name = p.name ?? `arg${i + 1}`;
372
- return `${name}${p.optional ? "?" : ""}: ${(0, import_luaut_parser2.formatType)(p.type)}`;
373
- });
374
- const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
375
- const varargs = signature.varargs ? [`...: ${(0, import_luaut_parser2.formatType)(signature.varargs)}`] : [];
376
- const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${(0, import_luaut_parser2.formatType)(signature.returns)}`;
377
- return { label, parameters };
378
- }
379
-
380
548
  // src/features/imports.ts
381
549
  var SUGGEST_AGAIN = { title: "Suggest", command: "editor.action.triggerSuggest" };
382
550
  function importCompletion(analyzer, document, position) {
@@ -388,7 +556,7 @@ function importCompletion(analyzer, document, position) {
388
556
  const after = text.slice(cursor, lineEnd);
389
557
  if (!/^\s*(?:import|export)\b/.test(before)) return void 0;
390
558
  const path = /\bfrom\s*(["'])([^"']*)$/.exec(before);
391
- if (path) return pathItems(document.uri, position, path[2]);
559
+ if (path) return pathItems(analyzer, document.uri, position, path[2]);
392
560
  const braces = /^\s*(import|export)\s+(?:[A-Za-z_][A-Za-z0-9_]*\s*,\s*)?\{[^}]*$/.exec(before);
393
561
  if (braces) {
394
562
  const module2 = /\}\s*from\s*(["'])([^"']+)\1/.exec(after);
@@ -397,21 +565,52 @@ function importCompletion(analyzer, document, position) {
397
565
  }
398
566
  return void 0;
399
567
  }
400
- function pathItems(fromUri, position, typed) {
568
+ function pathItems(analyzer, fromUri, position, typed) {
401
569
  const from = pathOfUri(fromUri);
402
570
  if (!from) return [];
403
- if (!typed.startsWith("./") && !typed.startsWith("../")) {
404
- const range2 = rangeBack(position, typed.length);
405
- return ["./", "../"].map((label) => ({
571
+ if (typed.startsWith("./") || typed.startsWith("../")) {
572
+ const slash = typed.lastIndexOf("/");
573
+ return entryItems((0, import_node_path2.resolve)((0, import_node_path2.dirname)(from), typed.slice(0, slash + 1)), rangeBack(position, typed.length - slash - 1), from);
574
+ }
575
+ const items = /* @__PURE__ */ new Map();
576
+ const whole = rangeBack(position, typed.length);
577
+ const offer = (label, folder) => {
578
+ if (!label.startsWith(typed) || label === typed) return;
579
+ items.set(label, {
406
580
  label,
407
- kind: import_vscode_languageserver.CompletionItemKind.Folder,
408
- textEdit: { range: range2, newText: label },
409
- command: SUGGEST_AGAIN
410
- }));
581
+ kind: folder ? import_vscode_languageserver.CompletionItemKind.Folder : import_vscode_languageserver.CompletionItemKind.File,
582
+ textEdit: { range: whole, newText: label },
583
+ command: folder ? SUGGEST_AGAIN : void 0
584
+ });
585
+ };
586
+ offer("./", true);
587
+ offer("../", true);
588
+ const config = analyzer.projectOf(fromUri).config;
589
+ for (const [pattern, targets] of Object.entries(config?.paths ?? {})) {
590
+ const star = pattern.indexOf("*");
591
+ if (star < 0) {
592
+ offer(pattern, false);
593
+ continue;
594
+ }
595
+ const prefix = pattern.slice(0, star);
596
+ if (!typed.startsWith(prefix)) {
597
+ offer(prefix, true);
598
+ continue;
599
+ }
600
+ const rest = typed.slice(prefix.length);
601
+ const slash = rest.lastIndexOf("/");
602
+ const range = rangeBack(position, rest.length - slash - 1);
603
+ for (const target of targets) {
604
+ const cut = target.indexOf("*");
605
+ const head = cut < 0 ? target : target.slice(0, cut);
606
+ for (const item of entryItems((0, import_node_path2.resolve)(config.baseUrl, head + rest.slice(0, slash + 1)), range, from)) {
607
+ items.set(item.label, item);
608
+ }
609
+ }
411
610
  }
412
- const slash = typed.lastIndexOf("/");
413
- const directory = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(from), typed.slice(0, slash + 1));
414
- const range = rangeBack(position, typed.length - slash - 1);
611
+ return [...items.values()];
612
+ }
613
+ function entryItems(directory, range, from) {
415
614
  let entries;
416
615
  try {
417
616
  entries = (0, import_node_fs2.readdirSync)(directory, { withFileTypes: true });
@@ -889,6 +1088,8 @@ var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
889
1088
  function completion(analyzer, document, position) {
890
1089
  const inImport = importCompletion(analyzer, document, position);
891
1090
  if (inImport) return inImport;
1091
+ const inString = stringCompletion(analyzer, document, position);
1092
+ if (inString) return inString;
892
1093
  const source = document.getText();
893
1094
  const offset = document.offsetAt(position);
894
1095
  let start = offset;
@@ -929,6 +1130,43 @@ function completion(analyzer, document, position) {
929
1130
  }
930
1131
  return valueItems(first.analysis, at);
931
1132
  }
1133
+ function stringCompletion(analyzer, document, position) {
1134
+ const analysis = analyzer.get(document);
1135
+ const path = pathAt(analysis.program, position, false);
1136
+ const literal = [...path].reverse().find((n) => n.type === "StringLiteral");
1137
+ if (!literal) return void 0;
1138
+ const expected = analysis.types.expectedTypeOf.get(literal);
1139
+ const values = stringLiterals(expected, analysis.types.aliases);
1140
+ if (!values.length) return [];
1141
+ const line = literal.line.start - 1;
1142
+ const range = literal.line.start === literal.line.end ? {
1143
+ start: { line, character: literal.column.start },
1144
+ end: { line, character: literal.column.end - 2 }
1145
+ } : void 0;
1146
+ return values.map((value) => ({
1147
+ label: value,
1148
+ kind: import_vscode_languageserver4.CompletionItemKind.Constant,
1149
+ ...range ? { textEdit: { range, newText: value } } : {}
1150
+ }));
1151
+ }
1152
+ function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
1153
+ if (!type || seen.has(type)) return [];
1154
+ seen.add(type);
1155
+ switch (type.kind) {
1156
+ case "literal":
1157
+ return typeof type.value === "string" ? [type.value] : [];
1158
+ case "union":
1159
+ return [...new Set(type.types.flatMap((t) => stringLiterals(t, aliases, seen)))];
1160
+ case "genericRef": {
1161
+ const alias = aliases.get(type.name);
1162
+ return alias ? stringLiterals(alias, aliases, seen) : [];
1163
+ }
1164
+ case "typeParam":
1165
+ return stringLiterals(type.constraint, aliases, seen);
1166
+ default:
1167
+ return [];
1168
+ }
1169
+ }
932
1170
  function memberOperator(source, wordStart) {
933
1171
  const ch = source[wordStart - 1];
934
1172
  if (ch === ":") return source[wordStart - 2] === ":" ? void 0 : ":";
@@ -1203,7 +1441,7 @@ var TOKEN_TYPES = [
1203
1441
  "method",
1204
1442
  "keyword"
1205
1443
  ];
1206
- var TOKEN_MODIFIERS = ["declaration", "readonly", "defaultLibrary"];
1444
+ var TOKEN_MODIFIERS = ["declaration", "readonly", "defaultLibrary", "control"];
1207
1445
  var semanticTokensLegend = {
1208
1446
  tokenTypes: [...TOKEN_TYPES],
1209
1447
  tokenModifiers: [...TOKEN_MODIFIERS]
@@ -1218,8 +1456,10 @@ var SOFT_KEYWORDS = /* @__PURE__ */ new Set([
1218
1456
  "is",
1219
1457
  "asserts",
1220
1458
  "satisfies",
1221
- "typeof"
1459
+ "typeof",
1460
+ "default"
1222
1461
  ]);
1462
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["default"]);
1223
1463
  var PRIMITIVES3 = /* @__PURE__ */ new Set(["any", "unknown", "never", "nil", "boolean", "number", "string", "thread", "buffer"]);
1224
1464
  function semanticTokens(analysis) {
1225
1465
  const entries = /* @__PURE__ */ new Map();
@@ -1245,12 +1485,8 @@ function semanticTokens(analysis) {
1245
1485
  walk2(analysis.program);
1246
1486
  for (const token of tokens) {
1247
1487
  const value = token.value;
1248
- if (typeof value !== "string") continue;
1249
- if (token.type === "Keyword" && value !== "true" && value !== "false" && value !== "nil") {
1250
- add(token, value.length, "keyword");
1251
- } else if (token.type === "Identifier" && SOFT_KEYWORDS.has(value)) {
1252
- add(token, value.length, "keyword");
1253
- }
1488
+ if (token.type !== "Identifier" || typeof value !== "string" || !SOFT_KEYWORDS.has(value)) continue;
1489
+ add(token, value.length, "keyword", CONTROL_KEYWORDS.has(value) ? ["control"] : []);
1254
1490
  }
1255
1491
  return { data: encode([...entries.values()]) };
1256
1492
  }
@@ -1456,17 +1692,56 @@ function createServer(connection, options = {}) {
1456
1692
  const document = documents.get(p.textDocument.uri);
1457
1693
  return document ? semanticTokens(analyzer.get(document)) : { data: [] };
1458
1694
  });
1459
- const publish = (document) => {
1460
- void connection.sendDiagnostics({
1461
- uri: document.uri,
1462
- version: document.version,
1463
- diagnostics: diagnostics(analyzer.get(document))
1464
- });
1465
- };
1466
- documents.onDidOpen((e) => publish(e.document));
1695
+ let configUris = /* @__PURE__ */ new Set();
1467
1696
  const publishAll = () => {
1468
- for (const document of documents.all()) publish(document);
1697
+ const problems = /* @__PURE__ */ new Map();
1698
+ for (const document of documents.all()) {
1699
+ const analysis = analyzer.get(document);
1700
+ void connection.sendDiagnostics({
1701
+ uri: document.uri,
1702
+ version: document.version,
1703
+ diagnostics: [...diagnostics(analysis), ...projectHint(analysis)]
1704
+ });
1705
+ for (const problem of analysis.project.problems) {
1706
+ const uri = uriOfPath(problem.file);
1707
+ const list = problems.get(uri) ?? [];
1708
+ if (!list.some((p) => p.message === problem.message && p.line === problem.line)) list.push(problem);
1709
+ problems.set(uri, list);
1710
+ }
1711
+ }
1712
+ for (const [uri, list] of problems) {
1713
+ void connection.sendDiagnostics({ uri, diagnostics: list.map(problemDiagnostic) });
1714
+ }
1715
+ for (const uri of configUris) {
1716
+ if (!problems.has(uri)) void connection.sendDiagnostics({ uri, diagnostics: [] });
1717
+ }
1718
+ configUris = new Set(problems.keys());
1719
+ };
1720
+ const problemDiagnostic = (problem) => {
1721
+ const line = Math.max((problem.line ?? 1) - 1, 0);
1722
+ const character = Math.max((problem.column ?? 1) - 1, 0);
1723
+ const text = analyzer.readFile(problem.file)?.split("\n")[line] ?? "";
1724
+ const end = Math.max(text.replace(/\r$/, "").trimEnd().length, character + 1);
1725
+ return {
1726
+ range: { start: { line, character }, end: { line, character: end } },
1727
+ severity: import_node.DiagnosticSeverity.Error,
1728
+ source: "luaut",
1729
+ code: "config",
1730
+ message: problem.message
1731
+ };
1732
+ };
1733
+ const projectHint = (analysis) => {
1734
+ const { project } = analysis;
1735
+ if (project.fixed || project.config || !pathOfUri(analysis.uri)) return [];
1736
+ return [{
1737
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
1738
+ severity: import_node.DiagnosticSeverity.Information,
1739
+ source: "luaut",
1740
+ code: "no-config",
1741
+ message: 'No luaut.config.json applies to this file, so no types are loaded \u2014 not even `print`. Add one to this folder or a folder above, such as { "types": ["luau"], "paths": {}, "sourceMap": null }'
1742
+ }];
1469
1743
  };
1744
+ documents.onDidOpen(publishAll);
1470
1745
  documents.onDidChangeContent(publishAll);
1471
1746
  connection.onDidChangeWatchedFiles(publishAll);
1472
1747
  documents.onDidClose((e) => {