luaut-language-server 1.1.1 → 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/cli.cjs CHANGED
@@ -9,19 +9,83 @@ var import_vscode_languageserver_textdocument = require("vscode-languageserver-t
9
9
  var import_node_fs = require("fs");
10
10
  var import_node_path = require("path");
11
11
  var import_node_url = require("url");
12
+ var import_luaut_parser2 = require("luaut-parser");
13
+
14
+ // src/features/members.ts
12
15
  var import_luaut_parser = require("luaut-parser");
16
+ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
17
+ if (!type || seen.has(type)) return [];
18
+ seen.add(type);
19
+ switch (type.kind) {
20
+ case "object": {
21
+ const out = [];
22
+ for (const [name, property] of type.properties) {
23
+ out.push({ name, property, isMethod: takesSelf(property.type) });
24
+ }
25
+ return out;
26
+ }
27
+ case "intersection": {
28
+ const merged = /* @__PURE__ */ new Map();
29
+ for (const part of type.types) {
30
+ for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
31
+ }
32
+ return [...merged.values()];
33
+ }
34
+ case "union": {
35
+ const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
36
+ if (!perBranch.length) return [];
37
+ const [first, ...rest] = perBranch;
38
+ return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
39
+ }
40
+ case "genericRef": {
41
+ const alias = aliases.get(type.name);
42
+ return alias ? membersOf(alias, aliases, seen) : [];
43
+ }
44
+ case "typeParam":
45
+ return membersOf(type.constraint, aliases, seen);
46
+ default:
47
+ return [];
48
+ }
49
+ }
50
+ function takesSelf(type) {
51
+ for (const signature of signaturesOf(type)) {
52
+ if (signature.params[0]?.name === "self") return true;
53
+ }
54
+ return false;
55
+ }
56
+ function signaturesOf(type, aliases) {
57
+ if (!type) return [];
58
+ if (type.kind === "function") return [type];
59
+ if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
60
+ if (type.kind === "genericRef" && aliases) {
61
+ const alias = aliases.get(type.name);
62
+ return alias ? signaturesOf(alias, aliases) : [];
63
+ }
64
+ return [];
65
+ }
66
+ function signatureLabel(signature) {
67
+ const parameters = signature.params.map((p, i) => {
68
+ const name = p.name ?? `arg${i + 1}`;
69
+ return `${name}${p.optional ? "?" : ""}: ${(0, import_luaut_parser.formatType)(p.type)}`;
70
+ });
71
+ const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
72
+ const varargs = signature.varargs ? [`...: ${(0, import_luaut_parser.formatType)(signature.varargs)}`] : [];
73
+ const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${(0, import_luaut_parser.formatType)(signature.returns)}`;
74
+ return { label, parameters };
75
+ }
76
+
77
+ // src/analysis.ts
13
78
  function globalsOf(libs) {
14
79
  const names = /* @__PURE__ */ new Set();
15
- for (const lib of libs) collect(lib.body.statements, names);
16
- return [...names];
17
- }
18
- function collect(statements, into) {
19
- for (const statement of statements) {
20
- if (statement.type === "DeclareStatement") into.add(statement.name);
80
+ for (const lib of libs) {
81
+ for (const statement of lib.body.statements) {
82
+ if (statement.type === "DeclareStatement") names.add(statement.name);
83
+ }
21
84
  }
85
+ return [...names];
22
86
  }
23
87
  function bindingOfNode(analysis, node) {
24
- const used = (0, import_luaut_parser.getBinding)(analysis.scopes, node);
88
+ const used = (0, import_luaut_parser2.getBinding)(analysis.scopes, node);
25
89
  if (used) return used;
26
90
  return declarationIndex(analysis).get(node);
27
91
  }
@@ -56,20 +120,34 @@ function pathKey(path) {
56
120
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
57
121
  }
58
122
  var CYCLE = { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
123
+ var NO_PROJECT = { project: { fixed: false, problems: [] }, libs: [], globals: [], reads: /* @__PURE__ */ new Map() };
59
124
  var Analyzer = class {
60
- libs;
61
- builtinGlobals;
125
+ fixed;
62
126
  openDocument;
63
127
  cache = /* @__PURE__ */ new Map();
64
128
  /** Imported modules, by path key. */
65
129
  modules = /* @__PURE__ */ new Map();
130
+ /** Project contexts, by folder. */
131
+ contexts = /* @__PURE__ */ new Map();
132
+ /** Parsed type libraries, by path — reparsed only when the text changes. */
133
+ libraries = /* @__PURE__ */ new Map();
134
+ /** Sourcemaps turned into types, by path, with what they were built from. */
135
+ sourceMaps = /* @__PURE__ */ new Map();
136
+ /** The analysis run in progress, if any. */
137
+ run;
66
138
  constructor(options = {}) {
67
- this.libs = options.libs ?? import_luaut_parser.defaultLibs;
68
- this.builtinGlobals = globalsOf(this.libs);
69
139
  this.openDocument = options.openDocument;
140
+ if (options.libs) {
141
+ this.fixed = {
142
+ project: { fixed: true, problems: [] },
143
+ libs: options.libs,
144
+ globals: globalsOf(options.libs),
145
+ reads: /* @__PURE__ */ new Map()
146
+ };
147
+ }
70
148
  }
71
149
  /** Analyze `document`, reusing the previous result while neither it nor
72
- * anything it imports has changed. */
150
+ * anything it read has changed. */
73
151
  get(document) {
74
152
  const cached = this.cache.get(document.uri);
75
153
  const source = document.getText();
@@ -84,34 +162,41 @@ var Analyzer = class {
84
162
  * completion, which analyzes a speculatively edited copy of the file. */
85
163
  analyze(uri, version, source) {
86
164
  const path = pathOfUri(uri);
87
- return this.analyzeModule(uri, version, source, new Set(path ? [pathKey(path)] : []));
165
+ if (!path) return this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set());
166
+ const key = pathKey(path);
167
+ return this.resolvingCycles(key, () => {
168
+ const analysis = this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set([key]));
169
+ return { result: analysis, exports: () => this.exportsFrom(analysis, /* @__PURE__ */ new Set([key])) };
170
+ });
88
171
  }
89
172
  forget(uri) {
90
173
  this.cache.delete(uri);
91
174
  }
92
- /** The file an import in `fromUri` names. Relative paths only (`./x`,
93
- * `../x`); the extension may be left off, and a folder means its
94
- * `index.luaut`. */
95
- resolveModulePath(fromUri, specifier) {
96
- return this.moduleCandidates(fromUri, specifier).find((candidate) => this.sourceOf(candidate) !== void 0);
175
+ /** The project a file belongs to. */
176
+ projectOf(uri) {
177
+ return this.contextFor(pathOfUri(uri)).project;
97
178
  }
98
- /** Every file an import could mean, in the order they are tried. */
99
- moduleCandidates(fromUri, specifier) {
179
+ /** The file an import in `fromUri` names: a relative path, or a `paths`
180
+ * alias from the file's config. */
181
+ resolveModulePath(fromUri, specifier) {
100
182
  const from = pathOfUri(fromUri);
101
- if (!from || !(specifier.startsWith("./") || specifier.startsWith("../"))) return [];
102
- const base = (0, import_node_path.resolve)((0, import_node_path.dirname)(from), specifier);
103
- return specifier.endsWith(".luaut") ? [base] : [`${base}.luaut`, `${base}.d.luaut`, (0, import_node_path.join)(base, "index.luaut")];
183
+ if (!from) return void 0;
184
+ return this.candidatesFor(from, specifier).find((candidate) => this.readFile(candidate) !== void 0);
104
185
  }
105
186
  /** What the module at `path` exports, analyzing it if need be. */
106
187
  exportsAt(path) {
107
- return this.exportsOf(path, /* @__PURE__ */ new Set());
188
+ return this.resolvingCycles(pathKey(path), () => {
189
+ const exports2 = this.exportsOf(path, /* @__PURE__ */ new Set());
190
+ return { result: exports2, exports: () => exports2 };
191
+ });
108
192
  }
109
193
  /** The analysis of the module at `path`, analyzing it if need be. */
110
194
  moduleAt(path) {
111
- this.exportsOf(path, /* @__PURE__ */ new Set());
195
+ this.exportsAt(path);
112
196
  return this.modules.get(pathKey(path))?.analysis;
113
197
  }
114
- sourceOf(path) {
198
+ /** A file's text: the open document if there is one, else the disk. */
199
+ readFile(path) {
115
200
  const open = this.openDocument?.(path);
116
201
  if (open) return open.getText();
117
202
  try {
@@ -120,60 +205,206 @@ var Analyzer = class {
120
205
  return void 0;
121
206
  }
122
207
  }
208
+ candidatesFor(from, specifier) {
209
+ return (0, import_luaut_parser2.moduleCandidates)(from, specifier, this.contextFor(from).project.config);
210
+ }
211
+ // ---------------------------------------------------------------- projects
212
+ contextFor(path) {
213
+ if (this.fixed) return this.fixed;
214
+ if (!path) return NO_PROJECT;
215
+ const key = pathKey((0, import_node_path.dirname)(path));
216
+ const cached = this.contexts.get(key);
217
+ if (cached && this.unchanged(cached.reads)) return cached;
218
+ const context = this.buildContext(path);
219
+ this.contexts.set(key, context);
220
+ return context;
221
+ }
222
+ buildContext(path) {
223
+ const reads = /* @__PURE__ */ new Map();
224
+ const host = {
225
+ readFile: (file) => {
226
+ const text = this.readFile(file);
227
+ reads.set(file, text);
228
+ return text;
229
+ }
230
+ };
231
+ const lookup = (0, import_luaut_parser2.findConfig)(path, host);
232
+ const problems = [...lookup.problems];
233
+ const config = lookup.config;
234
+ if (!config) return { project: { fixed: false, problems }, libs: [], globals: [], reads };
235
+ const libraries = (0, import_luaut_parser2.resolveTypeLibraries)(config, host);
236
+ problems.push(...libraries.problems);
237
+ const libs = [];
238
+ for (const file of libraries.files) {
239
+ const program = this.library(file, host, problems);
240
+ if (program) libs.push(program);
241
+ }
242
+ let sourceMap;
243
+ if (config.sourceMap) {
244
+ const text = host.readFile(config.sourceMap);
245
+ if (text === void 0) {
246
+ problems.push({
247
+ file: config.path,
248
+ message: `Cannot find the sourceMap file ${config.sourceMap}`,
249
+ ...optionPosition(config, "sourceMap")
250
+ });
251
+ } else {
252
+ const result = this.sourceMap(config.sourceMap, text, libs, libraries.files);
253
+ if (result.problem) problems.push({ file: config.sourceMap, message: result.problem, line: 1, column: 1 });
254
+ sourceMap = result.types;
255
+ if (sourceMap) libs.push(sourceMap.program);
256
+ }
257
+ }
258
+ return { project: { config, fixed: false, problems }, libs, globals: globalsOf(libs), sourceMap, reads };
259
+ }
260
+ /** A type library's definitions, parsed once per text. */
261
+ library(file, host, problems) {
262
+ const source = host.readFile(file);
263
+ if (source === void 0) return void 0;
264
+ const key = pathKey(file);
265
+ let entry = this.libraries.get(key);
266
+ if (!entry || entry.source !== source) {
267
+ try {
268
+ entry = { source, program: (0, import_luaut_parser2.parse)(source) };
269
+ } catch (error) {
270
+ const { message, line, column } = error;
271
+ entry = {
272
+ source,
273
+ problem: { file, message: `Syntax error in type library: ${message.replace(/\s*\(\d+:\d+\)$/, "")}`, line, column }
274
+ };
275
+ }
276
+ this.libraries.set(key, entry);
277
+ }
278
+ if (entry.problem) problems.push(entry.problem);
279
+ return entry.program;
280
+ }
281
+ /** A sourcemap's types, rebuilt only when it or the libraries change. */
282
+ sourceMap(path, text, libs, files) {
283
+ const key = pathKey(path);
284
+ const libraries = files.join("\n");
285
+ const cached = this.sourceMaps.get(key);
286
+ if (cached && cached.text === text && cached.libraries === libraries) return cached.result;
287
+ const aliases = aliasesOf(libs);
288
+ const members = /* @__PURE__ */ new Map();
289
+ const result = (0, import_luaut_parser2.sourceMapTypes)(text, path, {
290
+ classes: new Set(aliases.keys()),
291
+ membersOf: (className) => {
292
+ let names = members.get(className);
293
+ if (!names) {
294
+ names = new Set(membersOf(aliases.get(className), aliases).map((member) => member.name));
295
+ members.set(className, names);
296
+ }
297
+ return names;
298
+ }
299
+ });
300
+ this.sourceMaps.set(key, { text, libraries, result });
301
+ return result;
302
+ }
303
+ unchanged(reads) {
304
+ for (const [file, text] of reads) if (this.readFile(file) !== text) return false;
305
+ return true;
306
+ }
307
+ // ----------------------------------------------------------------- modules
308
+ /** Run one analysis of `root` and everything it imports; if that met an
309
+ * import cycle, run it once more with the first pass's exports standing
310
+ * in for the `any` the cycle left (see `Run`). A call made while a run is
311
+ * already going is part of that run. */
312
+ resolvingCycles(root, analyzeRoot) {
313
+ if (this.run) return analyzeRoot().result;
314
+ const run = { cycles: /* @__PURE__ */ new Set(), analyzed: /* @__PURE__ */ new Set(), provisional: /* @__PURE__ */ new Map() };
315
+ this.run = run;
316
+ try {
317
+ const first = analyzeRoot();
318
+ if (!run.cycles.size) return first.result;
319
+ for (const key of run.cycles) {
320
+ const exports2 = key === root ? first.exports() : this.modules.get(key)?.exports;
321
+ if (exports2 && !exports2.partial) run.provisional.set(key, exports2);
322
+ }
323
+ for (const key of run.analyzed) this.modules.delete(key);
324
+ return analyzeRoot().result;
325
+ } finally {
326
+ this.run = void 0;
327
+ }
328
+ }
329
+ /** A module's exports, from its analysis. */
330
+ exportsFrom(analysis, importing) {
331
+ return (0, import_luaut_parser2.moduleExports)(analysis.program, analysis.scopes, analysis.types, (specifier) => {
332
+ const next = this.resolveModulePath(analysis.uri, specifier);
333
+ return next ? this.exportsOf(next, importing) : void 0;
334
+ });
335
+ }
123
336
  /** `importing` holds every module on the current import chain, so an
124
337
  * import back into one of them is recognized as a cycle. */
125
338
  analyzeModule(uri, version, source, importing) {
126
- const { program, errors } = (0, import_luaut_parser.parseWithRecovery)(source);
127
- const scopes = (0, import_luaut_parser.analyzeScopes)(program, { builtinGlobals: this.builtinGlobals });
128
- const dependencies = /* @__PURE__ */ new Map();
129
- const types = (0, import_luaut_parser.analyzeTypes)(program, scopes, {
130
- libs: this.libs,
339
+ const path = pathOfUri(uri);
340
+ const context = this.contextFor(path);
341
+ const script = path ? context.sourceMap?.scriptFor(path) : void 0;
342
+ const libs = script ? [...context.libs, script] : context.libs;
343
+ const globals = script ? [...context.globals, "script"] : context.globals;
344
+ const { program, errors } = (0, import_luaut_parser2.parseWithRecovery)(source);
345
+ const scopes = (0, import_luaut_parser2.analyzeScopes)(program, { builtinGlobals: [...globals] });
346
+ const dependencies = new Map(context.reads);
347
+ const types = (0, import_luaut_parser2.analyzeTypes)(program, scopes, {
348
+ libs,
131
349
  resolveModule: (specifier) => {
132
- const target = this.resolveModulePath(uri, specifier);
350
+ if (!path) return void 0;
351
+ const candidates = this.candidatesFor(path, specifier);
352
+ const target = candidates.find((candidate) => this.readFile(candidate) !== void 0);
133
353
  if (!target) {
134
- for (const candidate of this.moduleCandidates(uri, specifier)) dependencies.set(candidate, void 0);
354
+ for (const candidate of candidates) dependencies.set(candidate, void 0);
135
355
  return void 0;
136
356
  }
137
357
  const exports2 = this.exportsOf(target, importing);
138
- dependencies.set(target, this.sourceOf(target));
358
+ dependencies.set(target, this.readFile(target));
139
359
  return exports2;
140
360
  }
141
361
  });
142
- return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies };
362
+ return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
143
363
  }
144
364
  exportsOf(path, importing) {
145
365
  const key = pathKey(path);
146
- if (importing.has(key)) return CYCLE;
147
- const source = this.sourceOf(path);
366
+ if (importing.has(key)) {
367
+ this.run?.cycles.add(key);
368
+ return this.run?.provisional.get(key) ?? CYCLE;
369
+ }
370
+ const source = this.readFile(path);
148
371
  if (source === void 0) return void 0;
149
372
  const cached = this.modules.get(key);
150
373
  if (cached && cached.analysis.source === source && this.isFresh(cached.analysis)) return cached.exports;
151
374
  importing.add(key);
152
375
  try {
153
376
  const analysis = this.analyzeModule(uriOfPath(path), -1, source, importing);
154
- const exports2 = (0, import_luaut_parser.moduleExports)(analysis.program, analysis.scopes, analysis.types, (specifier) => {
155
- const next = this.resolveModulePath(analysis.uri, specifier);
156
- return next ? this.exportsOf(next, importing) : void 0;
157
- });
377
+ const exports2 = this.exportsFrom(analysis, importing);
158
378
  this.modules.set(key, { analysis, exports: exports2 });
379
+ this.run?.analyzed.add(key);
159
380
  return exports2;
160
381
  } finally {
161
382
  importing.delete(key);
162
383
  }
163
384
  }
164
- /** Does every module `analysis` imported — and everything those import
165
- * still have the text it was analyzed against? */
385
+ /** Does every file `analysis` read — and everything the modules it
386
+ * imported read — still have the text it was analyzed against? */
166
387
  isFresh(analysis, seen = /* @__PURE__ */ new Set()) {
167
388
  if (seen.has(analysis)) return true;
168
389
  seen.add(analysis);
169
390
  for (const [path, source] of analysis.dependencies) {
170
- if (this.sourceOf(path) !== source) return false;
391
+ if (this.readFile(path) !== source) return false;
171
392
  const module2 = this.modules.get(pathKey(path));
172
393
  if (module2 && !this.isFresh(module2.analysis, seen)) return false;
173
394
  }
174
395
  return true;
175
396
  }
176
397
  };
398
+ function aliasesOf(libs) {
399
+ const empty = (0, import_luaut_parser2.parse)("");
400
+ return (0, import_luaut_parser2.analyzeTypes)(empty, (0, import_luaut_parser2.analyzeScopes)(empty, {}), { libs, diagnostics: false }).aliases;
401
+ }
402
+ function optionPosition(config, key) {
403
+ const offset = config.source.indexOf(JSON.stringify(key));
404
+ if (offset < 0) return { line: 1, column: 1 };
405
+ const before = config.source.slice(0, offset);
406
+ return { line: before.split("\n").length, column: offset - before.lastIndexOf("\n") };
407
+ }
177
408
 
178
409
  // src/features/imports.ts
179
410
  var import_node_fs2 = require("fs");
@@ -209,16 +440,16 @@ function containsPosition(node, pos, inclusive = false) {
209
440
  }
210
441
  function children(node) {
211
442
  const out = [];
212
- collect2(node, out);
443
+ collect(node, out);
213
444
  return out;
214
445
  }
215
- function collect2(container, out) {
446
+ function collect(container, out) {
216
447
  for (const key of Object.keys(container)) {
217
448
  if (key === "line" || key === "column") continue;
218
449
  const value = container[key];
219
450
  for (const item of Array.isArray(value) ? value : [value]) {
220
451
  if (isSpanned(item)) out.push(item);
221
- else if (isSpanlessNode(item)) collect2(item, out);
452
+ else if (isSpanlessNode(item)) collect(item, out);
222
453
  }
223
454
  }
224
455
  }
@@ -248,69 +479,6 @@ function walk(root, visit, parent) {
248
479
  for (const child of children(root)) walk(child, visit, root);
249
480
  }
250
481
 
251
- // src/features/members.ts
252
- var import_luaut_parser2 = require("luaut-parser");
253
- function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
254
- if (!type || seen.has(type)) return [];
255
- seen.add(type);
256
- switch (type.kind) {
257
- case "object": {
258
- const out = [];
259
- for (const [name, property] of type.properties) {
260
- out.push({ name, property, isMethod: takesSelf(property.type) });
261
- }
262
- return out;
263
- }
264
- case "intersection": {
265
- const merged = /* @__PURE__ */ new Map();
266
- for (const part of type.types) {
267
- for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
268
- }
269
- return [...merged.values()];
270
- }
271
- case "union": {
272
- const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
273
- if (!perBranch.length) return [];
274
- const [first, ...rest] = perBranch;
275
- return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
276
- }
277
- case "genericRef": {
278
- const alias = aliases.get(type.name);
279
- return alias ? membersOf(alias, aliases, seen) : [];
280
- }
281
- case "typeParam":
282
- return membersOf(type.constraint, aliases, seen);
283
- default:
284
- return [];
285
- }
286
- }
287
- function takesSelf(type) {
288
- for (const signature of signaturesOf(type)) {
289
- if (signature.params[0]?.name === "self") return true;
290
- }
291
- return false;
292
- }
293
- function signaturesOf(type, aliases) {
294
- if (!type) return [];
295
- if (type.kind === "function") return [type];
296
- if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
297
- if (type.kind === "genericRef" && aliases) {
298
- const alias = aliases.get(type.name);
299
- return alias ? signaturesOf(alias, aliases) : [];
300
- }
301
- return [];
302
- }
303
- function signatureLabel(signature) {
304
- const parameters = signature.params.map((p, i) => {
305
- const name = p.name ?? `arg${i + 1}`;
306
- return `${name}${p.optional ? "?" : ""}: ${(0, import_luaut_parser2.formatType)(p.type)}`;
307
- });
308
- const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
309
- const varargs = signature.varargs ? [`...: ${(0, import_luaut_parser2.formatType)(signature.varargs)}`] : [];
310
- const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${(0, import_luaut_parser2.formatType)(signature.returns)}`;
311
- return { label, parameters };
312
- }
313
-
314
482
  // src/features/imports.ts
315
483
  var SUGGEST_AGAIN = { title: "Suggest", command: "editor.action.triggerSuggest" };
316
484
  function importCompletion(analyzer, document, position) {
@@ -322,7 +490,7 @@ function importCompletion(analyzer, document, position) {
322
490
  const after = text.slice(cursor, lineEnd);
323
491
  if (!/^\s*(?:import|export)\b/.test(before)) return void 0;
324
492
  const path = /\bfrom\s*(["'])([^"']*)$/.exec(before);
325
- if (path) return pathItems(document.uri, position, path[2]);
493
+ if (path) return pathItems(analyzer, document.uri, position, path[2]);
326
494
  const braces = /^\s*(import|export)\s+(?:[A-Za-z_][A-Za-z0-9_]*\s*,\s*)?\{[^}]*$/.exec(before);
327
495
  if (braces) {
328
496
  const module2 = /\}\s*from\s*(["'])([^"']+)\1/.exec(after);
@@ -331,21 +499,52 @@ function importCompletion(analyzer, document, position) {
331
499
  }
332
500
  return void 0;
333
501
  }
334
- function pathItems(fromUri, position, typed) {
502
+ function pathItems(analyzer, fromUri, position, typed) {
335
503
  const from = pathOfUri(fromUri);
336
504
  if (!from) return [];
337
- if (!typed.startsWith("./") && !typed.startsWith("../")) {
338
- const range2 = rangeBack(position, typed.length);
339
- return ["./", "../"].map((label) => ({
505
+ if (typed.startsWith("./") || typed.startsWith("../")) {
506
+ const slash = typed.lastIndexOf("/");
507
+ 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);
508
+ }
509
+ const items = /* @__PURE__ */ new Map();
510
+ const whole = rangeBack(position, typed.length);
511
+ const offer = (label, folder) => {
512
+ if (!label.startsWith(typed) || label === typed) return;
513
+ items.set(label, {
340
514
  label,
341
- kind: import_vscode_languageserver.CompletionItemKind.Folder,
342
- textEdit: { range: range2, newText: label },
343
- command: SUGGEST_AGAIN
344
- }));
515
+ kind: folder ? import_vscode_languageserver.CompletionItemKind.Folder : import_vscode_languageserver.CompletionItemKind.File,
516
+ textEdit: { range: whole, newText: label },
517
+ command: folder ? SUGGEST_AGAIN : void 0
518
+ });
519
+ };
520
+ offer("./", true);
521
+ offer("../", true);
522
+ const config = analyzer.projectOf(fromUri).config;
523
+ for (const [pattern, targets] of Object.entries(config?.paths ?? {})) {
524
+ const star = pattern.indexOf("*");
525
+ if (star < 0) {
526
+ offer(pattern, false);
527
+ continue;
528
+ }
529
+ const prefix = pattern.slice(0, star);
530
+ if (!typed.startsWith(prefix)) {
531
+ offer(prefix, true);
532
+ continue;
533
+ }
534
+ const rest = typed.slice(prefix.length);
535
+ const slash = rest.lastIndexOf("/");
536
+ const range = rangeBack(position, rest.length - slash - 1);
537
+ for (const target of targets) {
538
+ const cut = target.indexOf("*");
539
+ const head = cut < 0 ? target : target.slice(0, cut);
540
+ for (const item of entryItems((0, import_node_path2.resolve)(config.baseUrl, head + rest.slice(0, slash + 1)), range, from)) {
541
+ items.set(item.label, item);
542
+ }
543
+ }
345
544
  }
346
- const slash = typed.lastIndexOf("/");
347
- const directory = (0, import_node_path2.resolve)((0, import_node_path2.dirname)(from), typed.slice(0, slash + 1));
348
- const range = rangeBack(position, typed.length - slash - 1);
545
+ return [...items.values()];
546
+ }
547
+ function entryItems(directory, range, from) {
349
548
  let entries;
350
549
  try {
351
550
  entries = (0, import_node_fs2.readdirSync)(directory, { withFileTypes: true });
@@ -1427,17 +1626,56 @@ function createServer(connection, options = {}) {
1427
1626
  const document = documents.get(p.textDocument.uri);
1428
1627
  return document ? semanticTokens(analyzer.get(document)) : { data: [] };
1429
1628
  });
1430
- const publish = (document) => {
1431
- void connection.sendDiagnostics({
1432
- uri: document.uri,
1433
- version: document.version,
1434
- diagnostics: diagnostics(analyzer.get(document))
1435
- });
1436
- };
1437
- documents.onDidOpen((e) => publish(e.document));
1629
+ let configUris = /* @__PURE__ */ new Set();
1438
1630
  const publishAll = () => {
1439
- for (const document of documents.all()) publish(document);
1631
+ const problems = /* @__PURE__ */ new Map();
1632
+ for (const document of documents.all()) {
1633
+ const analysis = analyzer.get(document);
1634
+ void connection.sendDiagnostics({
1635
+ uri: document.uri,
1636
+ version: document.version,
1637
+ diagnostics: [...diagnostics(analysis), ...projectHint(analysis)]
1638
+ });
1639
+ for (const problem of analysis.project.problems) {
1640
+ const uri = uriOfPath(problem.file);
1641
+ const list = problems.get(uri) ?? [];
1642
+ if (!list.some((p) => p.message === problem.message && p.line === problem.line)) list.push(problem);
1643
+ problems.set(uri, list);
1644
+ }
1645
+ }
1646
+ for (const [uri, list] of problems) {
1647
+ void connection.sendDiagnostics({ uri, diagnostics: list.map(problemDiagnostic) });
1648
+ }
1649
+ for (const uri of configUris) {
1650
+ if (!problems.has(uri)) void connection.sendDiagnostics({ uri, diagnostics: [] });
1651
+ }
1652
+ configUris = new Set(problems.keys());
1653
+ };
1654
+ const problemDiagnostic = (problem) => {
1655
+ const line = Math.max((problem.line ?? 1) - 1, 0);
1656
+ const character = Math.max((problem.column ?? 1) - 1, 0);
1657
+ const text = analyzer.readFile(problem.file)?.split("\n")[line] ?? "";
1658
+ const end = Math.max(text.replace(/\r$/, "").trimEnd().length, character + 1);
1659
+ return {
1660
+ range: { start: { line, character }, end: { line, character: end } },
1661
+ severity: import_node.DiagnosticSeverity.Error,
1662
+ source: "luaut",
1663
+ code: "config",
1664
+ message: problem.message
1665
+ };
1666
+ };
1667
+ const projectHint = (analysis) => {
1668
+ const { project } = analysis;
1669
+ if (project.fixed || project.config || !pathOfUri(analysis.uri)) return [];
1670
+ return [{
1671
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
1672
+ severity: import_node.DiagnosticSeverity.Information,
1673
+ source: "luaut",
1674
+ code: "no-config",
1675
+ 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 }'
1676
+ }];
1440
1677
  };
1678
+ documents.onDidOpen(publishAll);
1441
1679
  documents.onDidChangeContent(publishAll);
1442
1680
  connection.onDidChangeWatchedFiles(publishAll);
1443
1681
  documents.onDidClose((e) => {