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.
@@ -1,24 +1,90 @@
1
+ // src/features/members.ts
2
+ import { formatType } from "luaut-parser";
3
+ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
4
+ if (!type || seen.has(type)) return [];
5
+ seen.add(type);
6
+ switch (type.kind) {
7
+ case "object": {
8
+ const out = [];
9
+ for (const [name, property] of type.properties) {
10
+ out.push({ name, property, isMethod: takesSelf(property.type) });
11
+ }
12
+ return out;
13
+ }
14
+ case "intersection": {
15
+ const merged = /* @__PURE__ */ new Map();
16
+ for (const part of type.types) {
17
+ for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
18
+ }
19
+ return [...merged.values()];
20
+ }
21
+ case "union": {
22
+ const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
23
+ if (!perBranch.length) return [];
24
+ const [first, ...rest] = perBranch;
25
+ return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
26
+ }
27
+ case "genericRef": {
28
+ const alias = aliases.get(type.name);
29
+ return alias ? membersOf(alias, aliases, seen) : [];
30
+ }
31
+ case "typeParam":
32
+ return membersOf(type.constraint, aliases, seen);
33
+ default:
34
+ return [];
35
+ }
36
+ }
37
+ function takesSelf(type) {
38
+ for (const signature of signaturesOf(type)) {
39
+ if (signature.params[0]?.name === "self") return true;
40
+ }
41
+ return false;
42
+ }
43
+ function signaturesOf(type, aliases) {
44
+ if (!type) return [];
45
+ if (type.kind === "function") return [type];
46
+ if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
47
+ if (type.kind === "genericRef" && aliases) {
48
+ const alias = aliases.get(type.name);
49
+ return alias ? signaturesOf(alias, aliases) : [];
50
+ }
51
+ return [];
52
+ }
53
+ function signatureLabel(signature) {
54
+ const parameters = signature.params.map((p, i) => {
55
+ const name = p.name ?? `arg${i + 1}`;
56
+ return `${name}${p.optional ? "?" : ""}: ${formatType(p.type)}`;
57
+ });
58
+ const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
59
+ const varargs = signature.varargs ? [`...: ${formatType(signature.varargs)}`] : [];
60
+ const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${formatType(signature.returns)}`;
61
+ return { label, parameters };
62
+ }
63
+
1
64
  // src/analysis.ts
2
65
  import { readFileSync, statSync } from "fs";
3
- import { dirname, join, resolve } from "path";
66
+ import { dirname, resolve } from "path";
4
67
  import { fileURLToPath, pathToFileURL } from "url";
5
68
  import {
69
+ parse,
6
70
  parseWithRecovery,
7
71
  analyzeScopes,
8
72
  analyzeTypes,
9
73
  moduleExports,
10
- defaultLibs,
11
- getBinding
74
+ getBinding,
75
+ findConfig,
76
+ resolveTypeLibraries,
77
+ moduleCandidates,
78
+ sourceMapTypes
12
79
  } from "luaut-parser";
13
80
  function globalsOf(libs) {
14
81
  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);
82
+ for (const lib of libs) {
83
+ for (const statement of lib.body.statements) {
84
+ if (statement.type === "DeclareStatement") names.add(statement.name);
85
+ }
21
86
  }
87
+ return [...names];
22
88
  }
23
89
  function bindingOfNode(analysis, node) {
24
90
  const used = getBinding(analysis.scopes, node);
@@ -56,20 +122,34 @@ function pathKey(path) {
56
122
  return process.platform === "win32" ? normalized.toLowerCase() : normalized;
57
123
  }
58
124
  var CYCLE = { values: /* @__PURE__ */ new Map(), types: /* @__PURE__ */ new Map(), partial: true };
125
+ var NO_PROJECT = { project: { fixed: false, problems: [] }, libs: [], globals: [], reads: /* @__PURE__ */ new Map() };
59
126
  var Analyzer = class {
60
- libs;
61
- builtinGlobals;
127
+ fixed;
62
128
  openDocument;
63
129
  cache = /* @__PURE__ */ new Map();
64
130
  /** Imported modules, by path key. */
65
131
  modules = /* @__PURE__ */ new Map();
132
+ /** Project contexts, by folder. */
133
+ contexts = /* @__PURE__ */ new Map();
134
+ /** Parsed type libraries, by path — reparsed only when the text changes. */
135
+ libraries = /* @__PURE__ */ new Map();
136
+ /** Sourcemaps turned into types, by path, with what they were built from. */
137
+ sourceMaps = /* @__PURE__ */ new Map();
138
+ /** The analysis run in progress, if any. */
139
+ run;
66
140
  constructor(options = {}) {
67
- this.libs = options.libs ?? defaultLibs;
68
- this.builtinGlobals = globalsOf(this.libs);
69
141
  this.openDocument = options.openDocument;
142
+ if (options.libs) {
143
+ this.fixed = {
144
+ project: { fixed: true, problems: [] },
145
+ libs: options.libs,
146
+ globals: globalsOf(options.libs),
147
+ reads: /* @__PURE__ */ new Map()
148
+ };
149
+ }
70
150
  }
71
151
  /** Analyze `document`, reusing the previous result while neither it nor
72
- * anything it imports has changed. */
152
+ * anything it read has changed. */
73
153
  get(document) {
74
154
  const cached = this.cache.get(document.uri);
75
155
  const source = document.getText();
@@ -84,34 +164,41 @@ var Analyzer = class {
84
164
  * completion, which analyzes a speculatively edited copy of the file. */
85
165
  analyze(uri, version, source) {
86
166
  const path = pathOfUri(uri);
87
- return this.analyzeModule(uri, version, source, new Set(path ? [pathKey(path)] : []));
167
+ if (!path) return this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set());
168
+ const key = pathKey(path);
169
+ return this.resolvingCycles(key, () => {
170
+ const analysis = this.analyzeModule(uri, version, source, /* @__PURE__ */ new Set([key]));
171
+ return { result: analysis, exports: () => this.exportsFrom(analysis, /* @__PURE__ */ new Set([key])) };
172
+ });
88
173
  }
89
174
  forget(uri) {
90
175
  this.cache.delete(uri);
91
176
  }
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);
177
+ /** The project a file belongs to. */
178
+ projectOf(uri) {
179
+ return this.contextFor(pathOfUri(uri)).project;
97
180
  }
98
- /** Every file an import could mean, in the order they are tried. */
99
- moduleCandidates(fromUri, specifier) {
181
+ /** The file an import in `fromUri` names: a relative path, or a `paths`
182
+ * alias from the file's config. */
183
+ resolveModulePath(fromUri, specifier) {
100
184
  const from = pathOfUri(fromUri);
101
- if (!from || !(specifier.startsWith("./") || specifier.startsWith("../"))) return [];
102
- const base = resolve(dirname(from), specifier);
103
- return specifier.endsWith(".luaut") ? [base] : [`${base}.luaut`, `${base}.d.luaut`, join(base, "index.luaut")];
185
+ if (!from) return void 0;
186
+ return this.candidatesFor(from, specifier).find((candidate) => this.readFile(candidate) !== void 0);
104
187
  }
105
188
  /** What the module at `path` exports, analyzing it if need be. */
106
189
  exportsAt(path) {
107
- return this.exportsOf(path, /* @__PURE__ */ new Set());
190
+ return this.resolvingCycles(pathKey(path), () => {
191
+ const exports = this.exportsOf(path, /* @__PURE__ */ new Set());
192
+ return { result: exports, exports: () => exports };
193
+ });
108
194
  }
109
195
  /** The analysis of the module at `path`, analyzing it if need be. */
110
196
  moduleAt(path) {
111
- this.exportsOf(path, /* @__PURE__ */ new Set());
197
+ this.exportsAt(path);
112
198
  return this.modules.get(pathKey(path))?.analysis;
113
199
  }
114
- sourceOf(path) {
200
+ /** A file's text: the open document if there is one, else the disk. */
201
+ readFile(path) {
115
202
  const open = this.openDocument?.(path);
116
203
  if (open) return open.getText();
117
204
  try {
@@ -120,60 +207,206 @@ var Analyzer = class {
120
207
  return void 0;
121
208
  }
122
209
  }
210
+ candidatesFor(from, specifier) {
211
+ return moduleCandidates(from, specifier, this.contextFor(from).project.config);
212
+ }
213
+ // ---------------------------------------------------------------- projects
214
+ contextFor(path) {
215
+ if (this.fixed) return this.fixed;
216
+ if (!path) return NO_PROJECT;
217
+ const key = pathKey(dirname(path));
218
+ const cached = this.contexts.get(key);
219
+ if (cached && this.unchanged(cached.reads)) return cached;
220
+ const context = this.buildContext(path);
221
+ this.contexts.set(key, context);
222
+ return context;
223
+ }
224
+ buildContext(path) {
225
+ const reads = /* @__PURE__ */ new Map();
226
+ const host = {
227
+ readFile: (file) => {
228
+ const text = this.readFile(file);
229
+ reads.set(file, text);
230
+ return text;
231
+ }
232
+ };
233
+ const lookup = findConfig(path, host);
234
+ const problems = [...lookup.problems];
235
+ const config = lookup.config;
236
+ if (!config) return { project: { fixed: false, problems }, libs: [], globals: [], reads };
237
+ const libraries = resolveTypeLibraries(config, host);
238
+ problems.push(...libraries.problems);
239
+ const libs = [];
240
+ for (const file of libraries.files) {
241
+ const program = this.library(file, host, problems);
242
+ if (program) libs.push(program);
243
+ }
244
+ let sourceMap;
245
+ if (config.sourceMap) {
246
+ const text = host.readFile(config.sourceMap);
247
+ if (text === void 0) {
248
+ problems.push({
249
+ file: config.path,
250
+ message: `Cannot find the sourceMap file ${config.sourceMap}`,
251
+ ...optionPosition(config, "sourceMap")
252
+ });
253
+ } else {
254
+ const result = this.sourceMap(config.sourceMap, text, libs, libraries.files);
255
+ if (result.problem) problems.push({ file: config.sourceMap, message: result.problem, line: 1, column: 1 });
256
+ sourceMap = result.types;
257
+ if (sourceMap) libs.push(sourceMap.program);
258
+ }
259
+ }
260
+ return { project: { config, fixed: false, problems }, libs, globals: globalsOf(libs), sourceMap, reads };
261
+ }
262
+ /** A type library's definitions, parsed once per text. */
263
+ library(file, host, problems) {
264
+ const source = host.readFile(file);
265
+ if (source === void 0) return void 0;
266
+ const key = pathKey(file);
267
+ let entry = this.libraries.get(key);
268
+ if (!entry || entry.source !== source) {
269
+ try {
270
+ entry = { source, program: parse(source) };
271
+ } catch (error) {
272
+ const { message, line, column } = error;
273
+ entry = {
274
+ source,
275
+ problem: { file, message: `Syntax error in type library: ${message.replace(/\s*\(\d+:\d+\)$/, "")}`, line, column }
276
+ };
277
+ }
278
+ this.libraries.set(key, entry);
279
+ }
280
+ if (entry.problem) problems.push(entry.problem);
281
+ return entry.program;
282
+ }
283
+ /** A sourcemap's types, rebuilt only when it or the libraries change. */
284
+ sourceMap(path, text, libs, files) {
285
+ const key = pathKey(path);
286
+ const libraries = files.join("\n");
287
+ const cached = this.sourceMaps.get(key);
288
+ if (cached && cached.text === text && cached.libraries === libraries) return cached.result;
289
+ const aliases = aliasesOf(libs);
290
+ const members = /* @__PURE__ */ new Map();
291
+ const result = sourceMapTypes(text, path, {
292
+ classes: new Set(aliases.keys()),
293
+ membersOf: (className) => {
294
+ let names = members.get(className);
295
+ if (!names) {
296
+ names = new Set(membersOf(aliases.get(className), aliases).map((member) => member.name));
297
+ members.set(className, names);
298
+ }
299
+ return names;
300
+ }
301
+ });
302
+ this.sourceMaps.set(key, { text, libraries, result });
303
+ return result;
304
+ }
305
+ unchanged(reads) {
306
+ for (const [file, text] of reads) if (this.readFile(file) !== text) return false;
307
+ return true;
308
+ }
309
+ // ----------------------------------------------------------------- modules
310
+ /** Run one analysis of `root` and everything it imports; if that met an
311
+ * import cycle, run it once more with the first pass's exports standing
312
+ * in for the `any` the cycle left (see `Run`). A call made while a run is
313
+ * already going is part of that run. */
314
+ resolvingCycles(root, analyzeRoot) {
315
+ if (this.run) return analyzeRoot().result;
316
+ const run = { cycles: /* @__PURE__ */ new Set(), analyzed: /* @__PURE__ */ new Set(), provisional: /* @__PURE__ */ new Map() };
317
+ this.run = run;
318
+ try {
319
+ const first = analyzeRoot();
320
+ if (!run.cycles.size) return first.result;
321
+ for (const key of run.cycles) {
322
+ const exports = key === root ? first.exports() : this.modules.get(key)?.exports;
323
+ if (exports && !exports.partial) run.provisional.set(key, exports);
324
+ }
325
+ for (const key of run.analyzed) this.modules.delete(key);
326
+ return analyzeRoot().result;
327
+ } finally {
328
+ this.run = void 0;
329
+ }
330
+ }
331
+ /** A module's exports, from its analysis. */
332
+ exportsFrom(analysis, importing) {
333
+ return moduleExports(analysis.program, analysis.scopes, analysis.types, (specifier) => {
334
+ const next = this.resolveModulePath(analysis.uri, specifier);
335
+ return next ? this.exportsOf(next, importing) : void 0;
336
+ });
337
+ }
123
338
  /** `importing` holds every module on the current import chain, so an
124
339
  * import back into one of them is recognized as a cycle. */
125
340
  analyzeModule(uri, version, source, importing) {
341
+ const path = pathOfUri(uri);
342
+ const context = this.contextFor(path);
343
+ const script = path ? context.sourceMap?.scriptFor(path) : void 0;
344
+ const libs = script ? [...context.libs, script] : context.libs;
345
+ const globals = script ? [...context.globals, "script"] : context.globals;
126
346
  const { program, errors } = parseWithRecovery(source);
127
- const scopes = analyzeScopes(program, { builtinGlobals: this.builtinGlobals });
128
- const dependencies = /* @__PURE__ */ new Map();
347
+ const scopes = analyzeScopes(program, { builtinGlobals: [...globals] });
348
+ const dependencies = new Map(context.reads);
129
349
  const types = analyzeTypes(program, scopes, {
130
- libs: this.libs,
350
+ libs,
131
351
  resolveModule: (specifier) => {
132
- const target = this.resolveModulePath(uri, specifier);
352
+ if (!path) return void 0;
353
+ const candidates = this.candidatesFor(path, specifier);
354
+ const target = candidates.find((candidate) => this.readFile(candidate) !== void 0);
133
355
  if (!target) {
134
- for (const candidate of this.moduleCandidates(uri, specifier)) dependencies.set(candidate, void 0);
356
+ for (const candidate of candidates) dependencies.set(candidate, void 0);
135
357
  return void 0;
136
358
  }
137
359
  const exports = this.exportsOf(target, importing);
138
- dependencies.set(target, this.sourceOf(target));
360
+ dependencies.set(target, this.readFile(target));
139
361
  return exports;
140
362
  }
141
363
  });
142
- return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies };
364
+ return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
143
365
  }
144
366
  exportsOf(path, importing) {
145
367
  const key = pathKey(path);
146
- if (importing.has(key)) return CYCLE;
147
- const source = this.sourceOf(path);
368
+ if (importing.has(key)) {
369
+ this.run?.cycles.add(key);
370
+ return this.run?.provisional.get(key) ?? CYCLE;
371
+ }
372
+ const source = this.readFile(path);
148
373
  if (source === void 0) return void 0;
149
374
  const cached = this.modules.get(key);
150
375
  if (cached && cached.analysis.source === source && this.isFresh(cached.analysis)) return cached.exports;
151
376
  importing.add(key);
152
377
  try {
153
378
  const analysis = this.analyzeModule(uriOfPath(path), -1, source, importing);
154
- const exports = 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
- });
379
+ const exports = this.exportsFrom(analysis, importing);
158
380
  this.modules.set(key, { analysis, exports });
381
+ this.run?.analyzed.add(key);
159
382
  return exports;
160
383
  } finally {
161
384
  importing.delete(key);
162
385
  }
163
386
  }
164
- /** Does every module `analysis` imported — and everything those import
165
- * still have the text it was analyzed against? */
387
+ /** Does every file `analysis` read — and everything the modules it
388
+ * imported read — still have the text it was analyzed against? */
166
389
  isFresh(analysis, seen = /* @__PURE__ */ new Set()) {
167
390
  if (seen.has(analysis)) return true;
168
391
  seen.add(analysis);
169
392
  for (const [path, source] of analysis.dependencies) {
170
- if (this.sourceOf(path) !== source) return false;
393
+ if (this.readFile(path) !== source) return false;
171
394
  const module = this.modules.get(pathKey(path));
172
395
  if (module && !this.isFresh(module.analysis, seen)) return false;
173
396
  }
174
397
  return true;
175
398
  }
176
399
  };
400
+ function aliasesOf(libs) {
401
+ const empty = parse("");
402
+ return analyzeTypes(empty, analyzeScopes(empty, {}), { libs, diagnostics: false }).aliases;
403
+ }
404
+ function optionPosition(config, key) {
405
+ const offset = config.source.indexOf(JSON.stringify(key));
406
+ if (offset < 0) return { line: 1, column: 1 };
407
+ const before = config.source.slice(0, offset);
408
+ return { line: before.split("\n").length, column: offset - before.lastIndexOf("\n") };
409
+ }
177
410
 
178
411
  // src/ast-utils.ts
179
412
  function isSpanned(v) {
@@ -203,16 +436,16 @@ function containsPosition(node, pos, inclusive = false) {
203
436
  }
204
437
  function children(node) {
205
438
  const out = [];
206
- collect2(node, out);
439
+ collect(node, out);
207
440
  return out;
208
441
  }
209
- function collect2(container, out) {
442
+ function collect(container, out) {
210
443
  for (const key of Object.keys(container)) {
211
444
  if (key === "line" || key === "column") continue;
212
445
  const value = container[key];
213
446
  for (const item of Array.isArray(value) ? value : [value]) {
214
447
  if (isSpanned(item)) out.push(item);
215
- else if (isSpanlessNode(item)) collect2(item, out);
448
+ else if (isSpanlessNode(item)) collect(item, out);
216
449
  }
217
450
  }
218
451
  }
@@ -253,69 +486,6 @@ function walk(root, visit, parent) {
253
486
  for (const child of children(root)) walk(child, visit, root);
254
487
  }
255
488
 
256
- // src/features/members.ts
257
- import { formatType } from "luaut-parser";
258
- function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
259
- if (!type || seen.has(type)) return [];
260
- seen.add(type);
261
- switch (type.kind) {
262
- case "object": {
263
- const out = [];
264
- for (const [name, property] of type.properties) {
265
- out.push({ name, property, isMethod: takesSelf(property.type) });
266
- }
267
- return out;
268
- }
269
- case "intersection": {
270
- const merged = /* @__PURE__ */ new Map();
271
- for (const part of type.types) {
272
- for (const member of membersOf(part, aliases, seen)) merged.set(member.name, member);
273
- }
274
- return [...merged.values()];
275
- }
276
- case "union": {
277
- const perBranch = type.types.map((part) => membersOf(part, aliases, seen));
278
- if (!perBranch.length) return [];
279
- const [first, ...rest] = perBranch;
280
- return first.filter((member) => rest.every((other) => other.some((m) => m.name === member.name)));
281
- }
282
- case "genericRef": {
283
- const alias = aliases.get(type.name);
284
- return alias ? membersOf(alias, aliases, seen) : [];
285
- }
286
- case "typeParam":
287
- return membersOf(type.constraint, aliases, seen);
288
- default:
289
- return [];
290
- }
291
- }
292
- function takesSelf(type) {
293
- for (const signature of signaturesOf(type)) {
294
- if (signature.params[0]?.name === "self") return true;
295
- }
296
- return false;
297
- }
298
- function signaturesOf(type, aliases) {
299
- if (!type) return [];
300
- if (type.kind === "function") return [type];
301
- if (type.kind === "intersection") return type.types.flatMap((t) => signaturesOf(t, aliases));
302
- if (type.kind === "genericRef" && aliases) {
303
- const alias = aliases.get(type.name);
304
- return alias ? signaturesOf(alias, aliases) : [];
305
- }
306
- return [];
307
- }
308
- function signatureLabel(signature) {
309
- const parameters = signature.params.map((p, i) => {
310
- const name = p.name ?? `arg${i + 1}`;
311
- return `${name}${p.optional ? "?" : ""}: ${formatType(p.type)}`;
312
- });
313
- const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
314
- const varargs = signature.varargs ? [`...: ${formatType(signature.varargs)}`] : [];
315
- const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${formatType(signature.returns)}`;
316
- return { label, parameters };
317
- }
318
-
319
489
  // src/features/imports.ts
320
490
  import { readdirSync } from "fs";
321
491
  import { dirname as dirname2, resolve as resolve2 } from "path";
@@ -335,7 +505,7 @@ function importCompletion(analyzer, document, position) {
335
505
  const after = text.slice(cursor, lineEnd);
336
506
  if (!/^\s*(?:import|export)\b/.test(before)) return void 0;
337
507
  const path = /\bfrom\s*(["'])([^"']*)$/.exec(before);
338
- if (path) return pathItems(document.uri, position, path[2]);
508
+ if (path) return pathItems(analyzer, document.uri, position, path[2]);
339
509
  const braces = /^\s*(import|export)\s+(?:[A-Za-z_][A-Za-z0-9_]*\s*,\s*)?\{[^}]*$/.exec(before);
340
510
  if (braces) {
341
511
  const module = /\}\s*from\s*(["'])([^"']+)\1/.exec(after);
@@ -344,21 +514,52 @@ function importCompletion(analyzer, document, position) {
344
514
  }
345
515
  return void 0;
346
516
  }
347
- function pathItems(fromUri, position, typed) {
517
+ function pathItems(analyzer, fromUri, position, typed) {
348
518
  const from = pathOfUri(fromUri);
349
519
  if (!from) return [];
350
- if (!typed.startsWith("./") && !typed.startsWith("../")) {
351
- const range2 = rangeBack(position, typed.length);
352
- return ["./", "../"].map((label) => ({
520
+ if (typed.startsWith("./") || typed.startsWith("../")) {
521
+ const slash = typed.lastIndexOf("/");
522
+ return entryItems(resolve2(dirname2(from), typed.slice(0, slash + 1)), rangeBack(position, typed.length - slash - 1), from);
523
+ }
524
+ const items = /* @__PURE__ */ new Map();
525
+ const whole = rangeBack(position, typed.length);
526
+ const offer = (label, folder) => {
527
+ if (!label.startsWith(typed) || label === typed) return;
528
+ items.set(label, {
353
529
  label,
354
- kind: CompletionItemKind.Folder,
355
- textEdit: { range: range2, newText: label },
356
- command: SUGGEST_AGAIN
357
- }));
530
+ kind: folder ? CompletionItemKind.Folder : CompletionItemKind.File,
531
+ textEdit: { range: whole, newText: label },
532
+ command: folder ? SUGGEST_AGAIN : void 0
533
+ });
534
+ };
535
+ offer("./", true);
536
+ offer("../", true);
537
+ const config = analyzer.projectOf(fromUri).config;
538
+ for (const [pattern, targets] of Object.entries(config?.paths ?? {})) {
539
+ const star = pattern.indexOf("*");
540
+ if (star < 0) {
541
+ offer(pattern, false);
542
+ continue;
543
+ }
544
+ const prefix = pattern.slice(0, star);
545
+ if (!typed.startsWith(prefix)) {
546
+ offer(prefix, true);
547
+ continue;
548
+ }
549
+ const rest = typed.slice(prefix.length);
550
+ const slash = rest.lastIndexOf("/");
551
+ const range = rangeBack(position, rest.length - slash - 1);
552
+ for (const target of targets) {
553
+ const cut = target.indexOf("*");
554
+ const head = cut < 0 ? target : target.slice(0, cut);
555
+ for (const item of entryItems(resolve2(config.baseUrl, head + rest.slice(0, slash + 1)), range, from)) {
556
+ items.set(item.label, item);
557
+ }
558
+ }
358
559
  }
359
- const slash = typed.lastIndexOf("/");
360
- const directory = resolve2(dirname2(from), typed.slice(0, slash + 1));
361
- const range = rangeBack(position, typed.length - slash - 1);
560
+ return [...items.values()];
561
+ }
562
+ function entryItems(directory, range, from) {
362
563
  let entries;
363
564
  try {
364
565
  entries = readdirSync(directory, { withFileTypes: true });
@@ -843,6 +1044,8 @@ var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
843
1044
  function completion(analyzer, document, position) {
844
1045
  const inImport = importCompletion(analyzer, document, position);
845
1046
  if (inImport) return inImport;
1047
+ const inString = stringCompletion(analyzer, document, position);
1048
+ if (inString) return inString;
846
1049
  const source = document.getText();
847
1050
  const offset = document.offsetAt(position);
848
1051
  let start = offset;
@@ -883,6 +1086,43 @@ function completion(analyzer, document, position) {
883
1086
  }
884
1087
  return valueItems(first.analysis, at);
885
1088
  }
1089
+ function stringCompletion(analyzer, document, position) {
1090
+ const analysis = analyzer.get(document);
1091
+ const path = pathAt(analysis.program, position, false);
1092
+ const literal = [...path].reverse().find((n) => n.type === "StringLiteral");
1093
+ if (!literal) return void 0;
1094
+ const expected = analysis.types.expectedTypeOf.get(literal);
1095
+ const values = stringLiterals(expected, analysis.types.aliases);
1096
+ if (!values.length) return [];
1097
+ const line = literal.line.start - 1;
1098
+ const range = literal.line.start === literal.line.end ? {
1099
+ start: { line, character: literal.column.start },
1100
+ end: { line, character: literal.column.end - 2 }
1101
+ } : void 0;
1102
+ return values.map((value) => ({
1103
+ label: value,
1104
+ kind: CompletionItemKind2.Constant,
1105
+ ...range ? { textEdit: { range, newText: value } } : {}
1106
+ }));
1107
+ }
1108
+ function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
1109
+ if (!type || seen.has(type)) return [];
1110
+ seen.add(type);
1111
+ switch (type.kind) {
1112
+ case "literal":
1113
+ return typeof type.value === "string" ? [type.value] : [];
1114
+ case "union":
1115
+ return [...new Set(type.types.flatMap((t) => stringLiterals(t, aliases, seen)))];
1116
+ case "genericRef": {
1117
+ const alias = aliases.get(type.name);
1118
+ return alias ? stringLiterals(alias, aliases, seen) : [];
1119
+ }
1120
+ case "typeParam":
1121
+ return stringLiterals(type.constraint, aliases, seen);
1122
+ default:
1123
+ return [];
1124
+ }
1125
+ }
886
1126
  function memberOperator(source, wordStart) {
887
1127
  const ch = source[wordStart - 1];
888
1128
  if (ch === ":") return source[wordStart - 2] === ":" ? void 0 : ":";
@@ -1157,7 +1397,7 @@ var TOKEN_TYPES = [
1157
1397
  "method",
1158
1398
  "keyword"
1159
1399
  ];
1160
- var TOKEN_MODIFIERS = ["declaration", "readonly", "defaultLibrary"];
1400
+ var TOKEN_MODIFIERS = ["declaration", "readonly", "defaultLibrary", "control"];
1161
1401
  var semanticTokensLegend = {
1162
1402
  tokenTypes: [...TOKEN_TYPES],
1163
1403
  tokenModifiers: [...TOKEN_MODIFIERS]
@@ -1172,8 +1412,10 @@ var SOFT_KEYWORDS = /* @__PURE__ */ new Set([
1172
1412
  "is",
1173
1413
  "asserts",
1174
1414
  "satisfies",
1175
- "typeof"
1415
+ "typeof",
1416
+ "default"
1176
1417
  ]);
1418
+ var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["default"]);
1177
1419
  var PRIMITIVES3 = /* @__PURE__ */ new Set(["any", "unknown", "never", "nil", "boolean", "number", "string", "thread", "buffer"]);
1178
1420
  function semanticTokens(analysis) {
1179
1421
  const entries = /* @__PURE__ */ new Map();
@@ -1199,12 +1441,8 @@ function semanticTokens(analysis) {
1199
1441
  walk2(analysis.program);
1200
1442
  for (const token of tokens) {
1201
1443
  const value = token.value;
1202
- if (typeof value !== "string") continue;
1203
- if (token.type === "Keyword" && value !== "true" && value !== "false" && value !== "nil") {
1204
- add(token, value.length, "keyword");
1205
- } else if (token.type === "Identifier" && SOFT_KEYWORDS.has(value)) {
1206
- add(token, value.length, "keyword");
1207
- }
1444
+ if (token.type !== "Identifier" || typeof value !== "string" || !SOFT_KEYWORDS.has(value)) continue;
1445
+ add(token, value.length, "keyword", CONTROL_KEYWORDS.has(value) ? ["control"] : []);
1208
1446
  }
1209
1447
  return { data: encode([...entries.values()]) };
1210
1448
  }
@@ -1376,6 +1614,7 @@ function encode(entries) {
1376
1614
  // src/server.ts
1377
1615
  import {
1378
1616
  createConnection,
1617
+ DiagnosticSeverity as DiagnosticSeverity2,
1379
1618
  ProposedFeatures,
1380
1619
  TextDocuments,
1381
1620
  TextDocumentSyncKind
@@ -1417,17 +1656,56 @@ function createServer(connection, options = {}) {
1417
1656
  const document = documents.get(p.textDocument.uri);
1418
1657
  return document ? semanticTokens(analyzer.get(document)) : { data: [] };
1419
1658
  });
1420
- const publish = (document) => {
1421
- void connection.sendDiagnostics({
1422
- uri: document.uri,
1423
- version: document.version,
1424
- diagnostics: diagnostics(analyzer.get(document))
1425
- });
1426
- };
1427
- documents.onDidOpen((e) => publish(e.document));
1659
+ let configUris = /* @__PURE__ */ new Set();
1428
1660
  const publishAll = () => {
1429
- for (const document of documents.all()) publish(document);
1661
+ const problems = /* @__PURE__ */ new Map();
1662
+ for (const document of documents.all()) {
1663
+ const analysis = analyzer.get(document);
1664
+ void connection.sendDiagnostics({
1665
+ uri: document.uri,
1666
+ version: document.version,
1667
+ diagnostics: [...diagnostics(analysis), ...projectHint(analysis)]
1668
+ });
1669
+ for (const problem of analysis.project.problems) {
1670
+ const uri = uriOfPath(problem.file);
1671
+ const list = problems.get(uri) ?? [];
1672
+ if (!list.some((p) => p.message === problem.message && p.line === problem.line)) list.push(problem);
1673
+ problems.set(uri, list);
1674
+ }
1675
+ }
1676
+ for (const [uri, list] of problems) {
1677
+ void connection.sendDiagnostics({ uri, diagnostics: list.map(problemDiagnostic) });
1678
+ }
1679
+ for (const uri of configUris) {
1680
+ if (!problems.has(uri)) void connection.sendDiagnostics({ uri, diagnostics: [] });
1681
+ }
1682
+ configUris = new Set(problems.keys());
1683
+ };
1684
+ const problemDiagnostic = (problem) => {
1685
+ const line = Math.max((problem.line ?? 1) - 1, 0);
1686
+ const character = Math.max((problem.column ?? 1) - 1, 0);
1687
+ const text = analyzer.readFile(problem.file)?.split("\n")[line] ?? "";
1688
+ const end = Math.max(text.replace(/\r$/, "").trimEnd().length, character + 1);
1689
+ return {
1690
+ range: { start: { line, character }, end: { line, character: end } },
1691
+ severity: DiagnosticSeverity2.Error,
1692
+ source: "luaut",
1693
+ code: "config",
1694
+ message: problem.message
1695
+ };
1696
+ };
1697
+ const projectHint = (analysis) => {
1698
+ const { project } = analysis;
1699
+ if (project.fixed || project.config || !pathOfUri(analysis.uri)) return [];
1700
+ return [{
1701
+ range: { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } },
1702
+ severity: DiagnosticSeverity2.Information,
1703
+ source: "luaut",
1704
+ code: "no-config",
1705
+ 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 }'
1706
+ }];
1430
1707
  };
1708
+ documents.onDidOpen(publishAll);
1431
1709
  documents.onDidChangeContent(publishAll);
1432
1710
  connection.onDidChangeWatchedFiles(publishAll);
1433
1711
  documents.onDidClose((e) => {
@@ -1498,6 +1776,9 @@ function startServer(options = {}) {
1498
1776
  }
1499
1777
 
1500
1778
  export {
1779
+ membersOf,
1780
+ signaturesOf,
1781
+ signatureLabel,
1501
1782
  pathOfUri,
1502
1783
  uriOfPath,
1503
1784
  samePath,
@@ -1510,9 +1791,6 @@ export {
1510
1791
  nodeAt,
1511
1792
  enclosing,
1512
1793
  walk,
1513
- membersOf,
1514
- signaturesOf,
1515
- signatureLabel,
1516
1794
  importCompletion,
1517
1795
  importDefinition,
1518
1796
  exportDeclaration,
@@ -1532,4 +1810,4 @@ export {
1532
1810
  createServer,
1533
1811
  startServer
1534
1812
  };
1535
- //# sourceMappingURL=chunk-HK7PKBDB.js.map
1813
+ //# sourceMappingURL=chunk-SXRYJV32.js.map