luaut-language-server 1.1.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.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 });
@@ -666,6 +865,9 @@ function describe(analysis, path, index) {
666
865
  case "DeclareStatement":
667
866
  if (parent.id === node) return declareText(analysis, parent);
668
867
  break;
868
+ case "DeclareClassStatement":
869
+ if (parent.name === node) return classText(analysis, name);
870
+ break;
669
871
  case "TableTypeProperty":
670
872
  if (parent.key === node) {
671
873
  const type2 = typeOfNode(parent.valueType);
@@ -722,9 +924,11 @@ function describe(analysis, path, index) {
722
924
  if (parameter) return typeParameterText(analysis, parameter);
723
925
  if (PRIMITIVES.has(base)) return `type ${base}`;
724
926
  }
725
- if (!node.namespace && !node.typeArguments.length) {
726
- const alias = types.aliases.get(base);
727
- if (alias) return `type ${base} = ${pretty(alias)}`;
927
+ if (!node.typeArguments.length) {
928
+ const qualified = node.namespace ? `${node.namespace}.${base}` : base;
929
+ const alias = types.aliases.get(qualified);
930
+ if (alias && (0, import_luaut_parser4.isClassType)(alias)) return classText(analysis, qualified);
931
+ if (alias) return `type ${qualified} = ${pretty(alias)}`;
728
932
  }
729
933
  const type2 = typeOfNode(node);
730
934
  return type2 && `type ${referenceText(analysis, node)} = ${pretty(type2)}`;
@@ -753,6 +957,19 @@ function declareText(analysis, statement) {
753
957
  const overloads = others > 0 ? ` (+${others} overload${others > 1 ? "s" : ""})` : "";
754
958
  return `declare function ${name}${(0, import_luaut_parser4.formatType)(own)}${overloads}`;
755
959
  }
960
+ function classText(analysis, name) {
961
+ const type = analysis.types.aliases.get(name);
962
+ if (!type || !(0, import_luaut_parser4.isClassType)(type)) return void 0;
963
+ const superclass = type.class.superclass;
964
+ const inherited = superclass ? analysis.types.aliases.get(superclass) : void 0;
965
+ const own = [...type.properties].filter(([key, property]) => inherited?.kind !== "object" || inherited.properties.get(key) !== property);
966
+ const head = `declare class ${name}${superclass ? ` extends ${superclass}` : ""}`;
967
+ if (!own.length) return `${head} {}`;
968
+ const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser4.formatType)(property.type)},`);
969
+ return `${head} {
970
+ ${lines.join("\n")}
971
+ }`;
972
+ }
756
973
  function typeParameterText(analysis, parameter) {
757
974
  return `(type parameter) ${typeParameterSignature(analysis, parameter)}`;
758
975
  }
@@ -917,10 +1134,10 @@ function completion(analyzer, document, position) {
917
1134
  }
918
1135
  if (operator || !first) return [];
919
1136
  if (inTypePosition(first.path)) {
920
- const named = [...first.analysis.types.aliases.keys()].map((name) => ({
1137
+ const named = [...first.analysis.types.aliases].map(([name, type]) => ({
921
1138
  label: name,
922
- kind: import_vscode_languageserver4.CompletionItemKind.Interface,
923
- detail: "type"
1139
+ kind: (0, import_luaut_parser5.isClassType)(type) ? import_vscode_languageserver4.CompletionItemKind.Class : import_vscode_languageserver4.CompletionItemKind.Interface,
1140
+ detail: (0, import_luaut_parser5.isClassType)(type) ? "class" : "type"
924
1141
  }));
925
1142
  const primitives = PRIMITIVES2.map((name) => ({
926
1143
  label: name,
@@ -1052,7 +1269,7 @@ function kindOf(type, bindingKind) {
1052
1269
  }
1053
1270
  function inTypePosition(path) {
1054
1271
  return path.some(
1055
- (n) => !!n.type && (n.type.endsWith("TypeNode") || n.type === "TypeReference" || n.type === "TypeAliasStatement" || n.type === "ExportTypeAliasStatement")
1272
+ (n) => !!n.type && (n.type.endsWith("TypeNode") || n.type === "TypeReference" || n.type === "TypeAliasStatement" || n.type === "ExportTypeAliasStatement" || n.type === "DeclareClassStatement")
1056
1273
  );
1057
1274
  }
1058
1275
  var PRIMITIVES2 = [
@@ -1193,6 +1410,12 @@ function documentSymbols(analysis) {
1193
1410
  }
1194
1411
  break;
1195
1412
  }
1413
+ case "DeclareClassStatement": {
1414
+ const name = node.name.name;
1415
+ const superclass = node.superclass?.base;
1416
+ out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Class, node, superclass && `extends ${superclass}`));
1417
+ break;
1418
+ }
1196
1419
  case "VariableDeclaration": {
1197
1420
  for (const target of node.names ?? []) {
1198
1421
  const name = target.name;
@@ -1234,6 +1457,7 @@ var import_luaut_parser7 = require("luaut-parser");
1234
1457
  var TOKEN_TYPES = [
1235
1458
  "namespace",
1236
1459
  "type",
1460
+ "class",
1237
1461
  "typeParameter",
1238
1462
  "parameter",
1239
1463
  "variable",
@@ -1250,6 +1474,7 @@ var semanticTokensLegend = {
1250
1474
  var SOFT_KEYWORDS = /* @__PURE__ */ new Set([
1251
1475
  "type",
1252
1476
  "declare",
1477
+ "class",
1253
1478
  "extends",
1254
1479
  "keyof",
1255
1480
  "infer",
@@ -1316,6 +1541,8 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1316
1541
  if (!baseToken) return;
1317
1542
  if (!namespace && typeParameterInScope2(ancestors, base)) {
1318
1543
  add(baseToken, base.length, "typeParameter");
1544
+ } else if ((0, import_luaut_parser7.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser7.unknownType)) {
1545
+ add(baseToken, base.length, "class");
1319
1546
  } else {
1320
1547
  add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);
1321
1548
  }
@@ -1343,6 +1570,9 @@ function identifier(analysis, node, parent, add) {
1343
1570
  case "ExportTypeAliasStatement":
1344
1571
  if (parent.name === node) return as("type", ["declaration"]);
1345
1572
  break;
1573
+ case "DeclareClassStatement":
1574
+ if (parent.name === node) return as("class", ["declaration"]);
1575
+ break;
1346
1576
  case "DeclareStatement":
1347
1577
  if (parent.id === node) return as(isFunction(typeOfNode(parent.valueType)) ? "function" : "variable", ["declaration"]);
1348
1578
  break;
@@ -1493,17 +1723,56 @@ function createServer(connection, options = {}) {
1493
1723
  const document = documents.get(p.textDocument.uri);
1494
1724
  return document ? semanticTokens(analyzer.get(document)) : { data: [] };
1495
1725
  });
1496
- const publish = (document) => {
1497
- void connection.sendDiagnostics({
1498
- uri: document.uri,
1499
- version: document.version,
1500
- diagnostics: diagnostics(analyzer.get(document))
1501
- });
1502
- };
1503
- documents.onDidOpen((e) => publish(e.document));
1726
+ let configUris = /* @__PURE__ */ new Set();
1504
1727
  const publishAll = () => {
1505
- for (const document of documents.all()) publish(document);
1728
+ const problems = /* @__PURE__ */ new Map();
1729
+ for (const document of documents.all()) {
1730
+ const analysis = analyzer.get(document);
1731
+ void connection.sendDiagnostics({
1732
+ uri: document.uri,
1733
+ version: document.version,
1734
+ diagnostics: [...diagnostics(analysis), ...projectHint(analysis)]
1735
+ });
1736
+ for (const problem of analysis.project.problems) {
1737
+ const uri = uriOfPath(problem.file);
1738
+ const list = problems.get(uri) ?? [];
1739
+ if (!list.some((p) => p.message === problem.message && p.line === problem.line)) list.push(problem);
1740
+ problems.set(uri, list);
1741
+ }
1742
+ }
1743
+ for (const [uri, list] of problems) {
1744
+ void connection.sendDiagnostics({ uri, diagnostics: list.map(problemDiagnostic) });
1745
+ }
1746
+ for (const uri of configUris) {
1747
+ if (!problems.has(uri)) void connection.sendDiagnostics({ uri, diagnostics: [] });
1748
+ }
1749
+ configUris = new Set(problems.keys());
1750
+ };
1751
+ const problemDiagnostic = (problem) => {
1752
+ const line = Math.max((problem.line ?? 1) - 1, 0);
1753
+ const character = Math.max((problem.column ?? 1) - 1, 0);
1754
+ const text = analyzer.readFile(problem.file)?.split("\n")[line] ?? "";
1755
+ const end = Math.max(text.replace(/\r$/, "").trimEnd().length, character + 1);
1756
+ return {
1757
+ range: { start: { line, character }, end: { line, character: end } },
1758
+ severity: import_node.DiagnosticSeverity.Error,
1759
+ source: "luaut",
1760
+ code: "config",
1761
+ message: problem.message
1762
+ };
1763
+ };
1764
+ const projectHint = (analysis) => {
1765
+ const { project } = analysis;
1766
+ if (project.fixed || project.config || !pathOfUri(analysis.uri)) return [];
1767
+ return [{
1768
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
1769
+ severity: import_node.DiagnosticSeverity.Information,
1770
+ source: "luaut",
1771
+ code: "no-config",
1772
+ 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 }'
1773
+ }];
1506
1774
  };
1775
+ documents.onDidOpen(publishAll);
1507
1776
  documents.onDidChangeContent(publishAll);
1508
1777
  connection.onDidChangeWatchedFiles(publishAll);
1509
1778
  documents.onDidClose((e) => {