luaut-language-server 3.0.0 → 4.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
@@ -13,6 +13,19 @@ var import_luaut_parser2 = require("luaut-parser");
13
13
 
14
14
  // src/features/members.ts
15
15
  var import_luaut_parser = require("luaut-parser");
16
+ function literalKeys(key, aliases) {
17
+ if (!key) return [];
18
+ const resolved = key.kind === "genericRef" ? aliases.get(key.name) : key;
19
+ if (!resolved) return [];
20
+ const parts = resolved.kind === "union" ? resolved.types : [resolved];
21
+ const out = [];
22
+ for (const part of parts) {
23
+ const member = part.kind === "genericRef" ? aliases.get(part.name) ?? part : part;
24
+ if (member.kind !== "literal" || typeof member.value !== "string") return [];
25
+ out.push(member.value);
26
+ }
27
+ return out;
28
+ }
16
29
  function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
17
30
  if (!type || seen.has(type)) return [];
18
31
  seen.add(type);
@@ -22,6 +35,11 @@ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
22
35
  for (const [name, property] of type.properties) {
23
36
  out.push({ name, property, isMethod: takesSelf(property.type) });
24
37
  }
38
+ for (const name of literalKeys(type.indexer?.key, aliases)) {
39
+ if (type.properties.has(name)) continue;
40
+ const property = { type: type.indexer.value, optional: true };
41
+ out.push({ name, property, isMethod: takesSelf(property.type) });
42
+ }
25
43
  return out;
26
44
  }
27
45
  case "intersection": {
@@ -43,6 +61,20 @@ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
43
61
  }
44
62
  case "typeParam":
45
63
  return membersOf(type.constraint, aliases, seen);
64
+ // An array and a string answer to the methods the language gives them
65
+ // — `names:filter(f)`, `text:trim()`. They are written in the parser's
66
+ // prelude as `ArrayMethods<T>` and `StringMethods`, so the element
67
+ // type goes in where `T` stands.
68
+ case "array":
69
+ case "tuple": {
70
+ const element = type.kind === "array" ? type.element : (0, import_luaut_parser.union)(type.elements);
71
+ const methods = aliases.get("ArrayMethods");
72
+ return methods ? membersOf((0, import_luaut_parser.substitute)(methods, /* @__PURE__ */ new Map([["T", element]])), aliases, seen) : [];
73
+ }
74
+ case "primitive":
75
+ return type.name === "string" ? membersOf(aliases.get("StringMethods"), aliases, seen) : [];
76
+ case "literal":
77
+ return type.base === "string" ? membersOf(aliases.get("StringMethods"), aliases, seen) : [];
46
78
  default:
47
79
  return [];
48
80
  }
@@ -341,11 +373,13 @@ var Analyzer = class {
341
373
  const script = path ? context.sourceMap?.scriptFor(path) : void 0;
342
374
  const libs = script ? [...context.libs, script] : context.libs;
343
375
  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] });
376
+ const { program, errors, directives } = (0, import_luaut_parser2.parseWithRecovery)(source);
377
+ const reportUndeclared = context.libs.length > 0;
378
+ const scopes = (0, import_luaut_parser2.analyzeScopes)(program, { builtinGlobals: [...globals], reportUndeclared });
346
379
  const dependencies = new Map(context.reads);
347
380
  const types = (0, import_luaut_parser2.analyzeTypes)(program, scopes, {
348
381
  libs,
382
+ reportUnknownTypes: reportUndeclared,
349
383
  resolveModule: (specifier) => {
350
384
  if (!path) return void 0;
351
385
  const candidates = this.candidatesFor(path, specifier);
@@ -359,7 +393,7 @@ var Analyzer = class {
359
393
  return exports2;
360
394
  }
361
395
  });
362
- return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
396
+ return { uri, version, source, program, parseErrors: errors, directives, scopes, types, dependencies, project: context.project };
363
397
  }
364
398
  exportsOf(path, importing) {
365
399
  const key = pathKey(path);
@@ -454,7 +488,7 @@ function collect(container, out) {
454
488
  }
455
489
  }
456
490
  function isSpanlessNode(v) {
457
- return !!v && typeof v === "object" && typeof v.type === "string";
491
+ return !!v && typeof v === "object" && !Array.isArray(v);
458
492
  }
459
493
  function pathAt(root, pos, inclusive = false) {
460
494
  let best;
@@ -714,6 +748,7 @@ function patternNamed(target, name) {
714
748
 
715
749
  // src/features/diagnostics.ts
716
750
  var import_vscode_languageserver2 = require("vscode-languageserver");
751
+ var import_luaut_parser4 = require("luaut-parser");
717
752
  function diagnostics(analysis) {
718
753
  const out = [];
719
754
  for (const error of analysis.parseErrors) {
@@ -727,29 +762,39 @@ function diagnostics(analysis) {
727
762
  message: error.message.replace(/\s*\(\d+:\d+\)$/, "")
728
763
  });
729
764
  }
730
- for (const d of analysis.scopes.diagnostics) {
731
- out.push({
765
+ const semantic = [
766
+ ...analysis.scopes.diagnostics.map((d) => ({
732
767
  range: toRange(d.node),
733
768
  severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
734
769
  source: "luaut",
735
770
  code: d.kind,
736
771
  message: d.message
737
- });
738
- }
739
- for (const d of analysis.types.diagnostics) {
740
- out.push({
772
+ })),
773
+ ...analysis.types.diagnostics.map((d) => ({
741
774
  range: toRange(d.node),
742
775
  severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
743
776
  source: "luaut",
744
777
  code: "type",
745
778
  message: d.message
779
+ }))
780
+ ];
781
+ const { kept, unusedExpectErrors } = (0, import_luaut_parser4.applyDirectives)(analysis.directives, semantic, (d) => d.range.start.line + 1);
782
+ out.push(...kept);
783
+ for (const directive of unusedExpectErrors) {
784
+ const start = toPosition(directive.line, directive.column);
785
+ out.push({
786
+ range: { start, end: { line: start.line, character: start.character + "--@luaut-expect-error".length } },
787
+ severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
788
+ source: "luaut",
789
+ code: "directive",
790
+ message: import_luaut_parser4.UNUSED_EXPECT_ERROR
746
791
  });
747
792
  }
748
793
  return out;
749
794
  }
750
795
 
751
796
  // src/features/hover.ts
752
- var import_luaut_parser4 = require("luaut-parser");
797
+ var import_luaut_parser5 = require("luaut-parser");
753
798
  function hover(analysis, position) {
754
799
  const path = pathAt(analysis.program, position, true);
755
800
  for (let i = path.length - 1; i >= 0; i--) {
@@ -796,6 +841,27 @@ function describe(analysis, path, index) {
796
841
  const type2 = property?.type ?? types.typeOf.get(field.value);
797
842
  return type2 && `(property) ${name}: ${pretty(type2)}`;
798
843
  }
844
+ // `const { name } = t`: a shorthand key *is* the binding it
845
+ // declares, and has the same span, so the cursor can land on
846
+ // either. A renamed key (`{ name: other }`) names the property
847
+ // the value is read from.
848
+ case "ObjectPatternProperty": {
849
+ if (parent.key !== node || parent.computed) break;
850
+ const value = parent.value;
851
+ if (parent.shorthand) return describe(analysis, [...path.slice(0, index), value], index);
852
+ const binding2 = value.type === "IdentifierPattern" ? bindingOfNode(analysis, value) : void 0;
853
+ const type2 = binding2 && types.bindingType.get(binding2.id);
854
+ return type2 && `(property) ${name}: ${pretty(type2)}`;
855
+ }
856
+ // One line of an overload set reads as its own signature.
857
+ // The line the body is on reads as the whole set, which is
858
+ // what the binding says and what the default path gives.
859
+ case "FunctionSignature": {
860
+ if (parent.name !== node) break;
861
+ const own = types.typeOfTypeNode.get(parent);
862
+ if (own) return `function ${name}${pretty(own)}`;
863
+ break;
864
+ }
799
865
  case "ImportSpecifier": {
800
866
  const alias = types.aliases.get(name);
801
867
  const binding2 = bindingOfNode(analysis, identifier2);
@@ -841,7 +907,7 @@ function describe(analysis, path, index) {
841
907
  case "MappedTypeNode":
842
908
  if (parent.parameterId === node) {
843
909
  const keys = typeOfNode(parent.constraint);
844
- return `(type parameter) ${name}${keys ? ` in ${(0, import_luaut_parser4.formatType)(keys)}` : ""}`;
910
+ return `(type parameter) ${name}${keys ? ` in ${(0, import_luaut_parser5.formatType)(keys)}` : ""}`;
845
911
  }
846
912
  break;
847
913
  }
@@ -858,6 +924,11 @@ function describe(analysis, path, index) {
858
924
  }
859
925
  return void 0;
860
926
  }
927
+ // `...` — what this function's extra arguments are.
928
+ case "VarargExpression": {
929
+ const type2 = types.typeOf.get(node);
930
+ return type2 ? `(vararg) ...: ${pretty(type2)}` : void 0;
931
+ }
861
932
  // Declarations: `const x`, a parameter.
862
933
  case "IdentifierPattern":
863
934
  case "FunctionParameter":
@@ -878,7 +949,7 @@ function describe(analysis, path, index) {
878
949
  if (!node.typeArguments.length) {
879
950
  const qualified = node.namespace ? `${node.namespace}.${base}` : base;
880
951
  const alias = types.aliases.get(qualified);
881
- if (alias && (0, import_luaut_parser4.isClassType)(alias)) return classText(analysis, qualified);
952
+ if (alias && (0, import_luaut_parser5.isClassType)(alias)) return classText(analysis, qualified);
882
953
  if (alias) return `type ${qualified} = ${pretty(alias)}`;
883
954
  }
884
955
  const type2 = typeOfNode(node);
@@ -906,17 +977,17 @@ function declareText(analysis, statement) {
906
977
  const total = analysis.program.body.statements.filter((s) => s.type === "DeclareStatement" && s.name === name).reduce((n, s) => n + signaturesOf(analysis.types.typeOfTypeNode.get(s.valueType)).length, 0);
907
978
  const others = total - 1;
908
979
  const overloads = others > 0 ? ` (+${others} overload${others > 1 ? "s" : ""})` : "";
909
- return `declare function ${name}${(0, import_luaut_parser4.formatType)(own)}${overloads}`;
980
+ return `declare function ${name}${(0, import_luaut_parser5.formatType)(own)}${overloads}`;
910
981
  }
911
982
  function classText(analysis, name) {
912
983
  const type = analysis.types.aliases.get(name);
913
- if (!type || !(0, import_luaut_parser4.isClassType)(type)) return void 0;
984
+ if (!type || !(0, import_luaut_parser5.isClassType)(type)) return void 0;
914
985
  const superclass = type.class.superclass;
915
986
  const inherited = superclass ? analysis.types.aliases.get(superclass) : void 0;
916
987
  const own = [...type.properties].filter(([key, property]) => inherited?.kind !== "object" || inherited.properties.get(key) !== property);
917
988
  const head = `declare class ${name}${superclass ? ` extends ${superclass}` : ""}`;
918
989
  if (!own.length) return `${head} {}`;
919
- const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser4.formatType)(property.type)},`);
990
+ const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser5.formatType)(property.type)},`);
920
991
  return `${head} {
921
992
  ${lines.join("\n")}
922
993
  }`;
@@ -928,7 +999,7 @@ function typeParameterSignature(analysis, parameter) {
928
999
  const p = parameter;
929
1000
  if (p.infer) return `infer ${p.name}`;
930
1001
  const constraint = p.constraint ? analysis.types.typeOfTypeNode.get(p.constraint) : void 0;
931
- return `${p.isConst ? "const " : ""}${p.name}${constraint ? ` extends ${(0, import_luaut_parser4.formatType)(constraint)}` : ""}`;
1002
+ return `${p.isConst ? "const " : ""}${p.name}${constraint ? ` extends ${(0, import_luaut_parser5.formatType)(constraint)}` : ""}`;
932
1003
  }
933
1004
  function typeParameterInScope(path, index, name) {
934
1005
  for (let i = index - 1; i >= 0; i--) {
@@ -953,7 +1024,7 @@ function referenceText(analysis, reference) {
953
1024
  if (!args.length) return name;
954
1025
  const resolved = args.map((a) => {
955
1026
  const t = analysis.types.typeOfTypeNode.get(a);
956
- return t ? (0, import_luaut_parser4.formatType)(t) : "?";
1027
+ return t ? (0, import_luaut_parser5.formatType)(t) : "?";
957
1028
  });
958
1029
  return `${name}<${resolved.join(", ")}>`;
959
1030
  }
@@ -962,27 +1033,27 @@ function fieldWithKey(table, key) {
962
1033
  return fields.find((f) => f.type === "TableFieldNamed" && f.key === key);
963
1034
  }
964
1035
  function pretty(type) {
965
- const flat = (0, import_luaut_parser4.formatType)(type);
1036
+ const flat = (0, import_luaut_parser5.formatType)(type);
966
1037
  if (flat.length <= 80) return flat;
967
1038
  if (type.kind === "object") {
968
1039
  const lines = [];
969
- if (type.indexer) lines.push(` [${(0, import_luaut_parser4.formatType)(type.indexer.key)}]: ${(0, import_luaut_parser4.formatType)(type.indexer.value)},`);
1040
+ if (type.indexer) lines.push(` [${(0, import_luaut_parser5.formatType)(type.indexer.key)}]: ${(0, import_luaut_parser5.formatType)(type.indexer.value)},`);
970
1041
  for (const [name, property] of type.properties) {
971
1042
  const readonly = property.readonly ? "readonly " : "";
972
- lines.push(` ${readonly}${name}${property.optional ? "?" : ""}: ${(0, import_luaut_parser4.formatType)(property.type)},`);
1043
+ lines.push(` ${readonly}${name}${property.optional ? "?" : ""}: ${(0, import_luaut_parser5.formatType)(property.type)},`);
973
1044
  }
974
1045
  return `{
975
1046
  ${lines.join("\n")}
976
1047
  }`;
977
1048
  }
978
1049
  if (type.kind === "intersection" && type.types.every((t) => t.kind === "function")) {
979
- return type.types.map(import_luaut_parser4.formatType).join("\n& ");
1050
+ return type.types.map(import_luaut_parser5.formatType).join("\n& ");
980
1051
  }
981
1052
  return flat;
982
1053
  }
983
1054
  function bindingText(binding, type) {
984
1055
  if (binding.declaredBy === "function" && type.kind === "function") {
985
- return `function ${binding.name}${(0, import_luaut_parser4.formatType)(type)}`;
1056
+ return `function ${binding.name}${(0, import_luaut_parser5.formatType)(type)}`;
986
1057
  }
987
1058
  return `${keyword(binding)} ${binding.name}: ${pretty(type)}`;
988
1059
  }
@@ -1059,8 +1130,162 @@ function isIdentifier(name) {
1059
1130
  }
1060
1131
 
1061
1132
  // src/features/completion.ts
1133
+ var import_vscode_languageserver5 = require("vscode-languageserver");
1134
+ var import_luaut_parser6 = require("luaut-parser");
1135
+
1136
+ // src/features/autoImport.ts
1137
+ var import_node_fs3 = require("fs");
1138
+ var import_node_path3 = require("path");
1062
1139
  var import_vscode_languageserver4 = require("vscode-languageserver");
1063
- var import_luaut_parser5 = require("luaut-parser");
1140
+ function importItems(analyzer, analysis, typePosition, taken) {
1141
+ const from = pathOfUri(analysis.uri);
1142
+ if (!from) return [];
1143
+ const config = analysis.project.config;
1144
+ const items = [];
1145
+ const offered = /* @__PURE__ */ new Set();
1146
+ for (const file of projectFiles(config?.directory ?? (0, import_node_path3.dirname)(from))) {
1147
+ if (samePath(file, from)) continue;
1148
+ const exports2 = analyzer.exportsAt(file);
1149
+ if (!exports2 || exports2.partial) continue;
1150
+ const names = typePosition ? [...exports2.types.keys()] : [...exports2.values.keys()];
1151
+ const specifier = specifierFor(from, file, config);
1152
+ for (const name of names) {
1153
+ if (taken.has(name) || offered.has(name)) continue;
1154
+ offered.add(name);
1155
+ const type = typePosition ? exports2.types.get(name)?.type : exports2.values.get(name);
1156
+ items.push({
1157
+ label: name,
1158
+ kind: typePosition ? import_vscode_languageserver4.CompletionItemKind.Interface : type && signaturesOf(type).length ? import_vscode_languageserver4.CompletionItemKind.Function : import_vscode_languageserver4.CompletionItemKind.Variable,
1159
+ labelDetails: { description: specifier },
1160
+ detail: `import { ${name} } from "${specifier}"`,
1161
+ sortText: `4${name}`,
1162
+ additionalTextEdits: [importEdit(analyzer, analysis, file, name, specifier, typePosition)]
1163
+ });
1164
+ }
1165
+ }
1166
+ return items;
1167
+ }
1168
+ function serviceItems(analysis, taken) {
1169
+ const services = analysis.types.aliases.get("Services");
1170
+ if (!services || services.kind !== "object") return [];
1171
+ const at = serviceInsertion(analysis.program.body.statements);
1172
+ const items = [];
1173
+ for (const name of services.properties.keys()) {
1174
+ if (taken.has(name)) continue;
1175
+ const line = `const ${name} = game:GetService("${name}")`;
1176
+ items.push({
1177
+ label: name,
1178
+ kind: import_vscode_languageserver4.CompletionItemKind.Module,
1179
+ labelDetails: { description: "service" },
1180
+ detail: line,
1181
+ sortText: `5${name}`,
1182
+ additionalTextEdits: [{ range: { start: at.position, end: at.position }, newText: `${line}
1183
+ ${at.gap}` }]
1184
+ });
1185
+ }
1186
+ return items;
1187
+ }
1188
+ function importEdit(analyzer, analysis, file, name, specifier, typePosition) {
1189
+ const statements = analysis.program.body.statements;
1190
+ const imports = statements.filter((s) => s.type === "ImportStatement");
1191
+ const existing = imports.find((s) => !s.namespaceImport && (typePosition || !s.isTypeOnly) && samePathOrUndefined(analyzer.resolveModulePath(analysis.uri, s.source.value), file));
1192
+ if (existing) {
1193
+ const last = existing.specifiers[existing.specifiers.length - 1];
1194
+ if (last) {
1195
+ const at2 = endOf(last);
1196
+ return { range: { start: at2, end: at2 }, newText: `, ${name}` };
1197
+ }
1198
+ if (existing.defaultImport) {
1199
+ const at2 = endOf(existing.defaultImport);
1200
+ return { range: { start: at2, end: at2 }, newText: `, { ${name} }` };
1201
+ }
1202
+ }
1203
+ const line = `import { ${name} } from "${specifier}"`;
1204
+ const lastImport = imports[imports.length - 1];
1205
+ if (lastImport) {
1206
+ const at2 = { line: lastImport.line.end, character: 0 };
1207
+ return { range: { start: at2, end: at2 }, newText: `${line}
1208
+ ` };
1209
+ }
1210
+ const first = statements[0];
1211
+ const at = { line: first ? first.line.start - 1 : 0, character: 0 };
1212
+ return { range: { start: at, end: at }, newText: first ? `${line}
1213
+
1214
+ ` : `${line}
1215
+ ` };
1216
+ }
1217
+ function serviceInsertion(statements) {
1218
+ let last;
1219
+ for (const statement of statements) {
1220
+ if (statement.type !== "ImportStatement" && !isServiceDeclaration(statement)) break;
1221
+ last = statement;
1222
+ }
1223
+ if (last) return { position: { line: last.line.end, character: 0 }, gap: "" };
1224
+ const first = statements[0];
1225
+ return first ? { position: { line: first.line.start - 1, character: 0 }, gap: "\n" } : { position: { line: 0, character: 0 }, gap: "" };
1226
+ }
1227
+ function isServiceDeclaration(statement) {
1228
+ if (statement.type !== "VariableDeclaration") return false;
1229
+ const init = statement.init[0];
1230
+ return init?.type === "MethodCallExpression" && init.method.name === "GetService" && init.object.type === "Identifier" && init.object.name === "game";
1231
+ }
1232
+ function endOf(node) {
1233
+ return { line: node.line.end - 1, character: node.column.end - 1 };
1234
+ }
1235
+ function samePathOrUndefined(a, b) {
1236
+ return a !== void 0 && samePath(a, b);
1237
+ }
1238
+ function specifierFor(from, target, config) {
1239
+ const withoutExtension = (path) => {
1240
+ const bare = path.replace(/\\/g, "/").replace(/\.luaut$/, "");
1241
+ return bare.endsWith("/index") ? bare.slice(0, -"/index".length) : bare;
1242
+ };
1243
+ let relativePath = withoutExtension((0, import_node_path3.relative)((0, import_node_path3.dirname)(from), target));
1244
+ if (!relativePath.startsWith(".")) relativePath = `./${relativePath}`;
1245
+ if (!relativePath.startsWith("../") || !config) return relativePath;
1246
+ for (const [pattern, targets] of Object.entries(config.paths)) {
1247
+ const star = pattern.indexOf("*");
1248
+ if (star < 0) continue;
1249
+ for (const targetPattern of targets) {
1250
+ const cut = targetPattern.indexOf("*");
1251
+ if (cut < 0) continue;
1252
+ const head = (0, import_node_path3.resolve)(config.baseUrl, targetPattern.slice(0, cut));
1253
+ const rest = (0, import_node_path3.relative)(head, target);
1254
+ if (rest.startsWith("..") || (0, import_node_path3.resolve)(head, rest) !== (0, import_node_path3.resolve)(target)) continue;
1255
+ return `${pattern.slice(0, star)}${withoutExtension(rest)}${pattern.slice(star + 1)}`;
1256
+ }
1257
+ }
1258
+ return relativePath;
1259
+ }
1260
+ var FILE_LIMIT = 2e3;
1261
+ var LISTING_TTL = 3e3;
1262
+ var listings = /* @__PURE__ */ new Map();
1263
+ function projectFiles(root) {
1264
+ const cached = listings.get(root);
1265
+ if (cached && Date.now() - cached.at < LISTING_TTL) return cached.files;
1266
+ const files = [];
1267
+ const walk2 = (directory, depth) => {
1268
+ if (files.length >= FILE_LIMIT || depth > 12) return;
1269
+ let entries;
1270
+ try {
1271
+ entries = (0, import_node_fs3.readdirSync)(directory, { withFileTypes: true });
1272
+ } catch {
1273
+ return;
1274
+ }
1275
+ for (const entry of entries) {
1276
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1277
+ const path = (0, import_node_path3.join)(directory, entry.name);
1278
+ if (entry.isDirectory()) walk2(path, depth + 1);
1279
+ else if (entry.name.endsWith(".luaut") && !entry.name.endsWith(".d.luaut")) files.push(path);
1280
+ if (files.length >= FILE_LIMIT) return;
1281
+ }
1282
+ };
1283
+ walk2(root, 0);
1284
+ listings.set(root, { at: Date.now(), files });
1285
+ return files;
1286
+ }
1287
+
1288
+ // src/features/completion.ts
1064
1289
  var PLACEHOLDER = "__luautCompletion__";
1065
1290
  var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
1066
1291
  function completion(analyzer, document, position) {
@@ -1093,28 +1318,58 @@ function completion(analyzer, document, position) {
1093
1318
  first ??= { analysis, path };
1094
1319
  }
1095
1320
  if (operator || !first) return [];
1321
+ const keys = objectKeyItems(first.analysis, first.path, source.slice(end));
1322
+ if (keys) return keys;
1096
1323
  if (inTypePosition(first.path)) {
1097
1324
  const named = [...first.analysis.types.aliases].map(([name, type]) => ({
1098
1325
  label: name,
1099
- kind: (0, import_luaut_parser5.isClassType)(type) ? import_vscode_languageserver4.CompletionItemKind.Class : import_vscode_languageserver4.CompletionItemKind.Interface,
1100
- detail: (0, import_luaut_parser5.isClassType)(type) ? "class" : "type"
1326
+ kind: (0, import_luaut_parser6.isClassType)(type) ? import_vscode_languageserver5.CompletionItemKind.Class : import_vscode_languageserver5.CompletionItemKind.Interface,
1327
+ detail: (0, import_luaut_parser6.isClassType)(type) ? "class" : "type"
1101
1328
  }));
1102
1329
  const primitives = PRIMITIVES2.map((name) => ({
1103
1330
  label: name,
1104
- kind: import_vscode_languageserver4.CompletionItemKind.Keyword,
1331
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1105
1332
  detail: "type"
1106
1333
  }));
1107
- return [...named, ...primitives];
1108
- }
1109
- return valueItems(first.analysis, at);
1334
+ const keywords = TYPE_KEYWORDS.map((name) => ({
1335
+ label: name,
1336
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1337
+ sortText: `3${name}`
1338
+ }));
1339
+ const typeNames = new Set(first.analysis.types.aliases.keys());
1340
+ const imported = importItems(analyzer, analyzer.get(document), true, typeNames);
1341
+ return [...named, ...primitives, ...keywords, ...imported];
1342
+ }
1343
+ const taken = /* @__PURE__ */ new Set();
1344
+ for (const binding of first.analysis.scopes.bindings.values()) taken.add(binding.name);
1345
+ const current = analyzer.get(document);
1346
+ return [
1347
+ ...valueItems(first.analysis, at),
1348
+ ...contextKeywords(source.slice(0, start)),
1349
+ ...importItems(analyzer, current, false, taken),
1350
+ ...serviceItems(current, taken)
1351
+ ];
1110
1352
  }
1111
1353
  function stringCompletion(analyzer, document, position) {
1112
- const analysis = analyzer.get(document);
1113
- const path = pathAt(analysis.program, position, false);
1114
- const literal = [...path].reverse().find((n) => n.type === "StringLiteral");
1115
- if (!literal) return void 0;
1354
+ let analysis = analyzer.get(document);
1355
+ let literal = stringAt(analysis, position);
1356
+ if (!literal) {
1357
+ const repaired = repairedStrings(document, position);
1358
+ for (const text of repaired) {
1359
+ const candidate = analyzer.analyze(document.uri, -1, text);
1360
+ literal = stringAt(candidate, position);
1361
+ if (literal) {
1362
+ analysis = candidate;
1363
+ break;
1364
+ }
1365
+ }
1366
+ if (!literal) return repaired.length ? [] : void 0;
1367
+ }
1116
1368
  const expected = analysis.types.expectedTypeOf.get(literal);
1117
- const values = stringLiterals(expected, analysis.types.aliases);
1369
+ const values = [.../* @__PURE__ */ new Set([
1370
+ ...stringLiterals(expected, analysis.types.aliases),
1371
+ ...indexKeys(analysis, position, literal)
1372
+ ])];
1118
1373
  if (!values.length) return [];
1119
1374
  const line = literal.line.start - 1;
1120
1375
  const range = literal.line.start === literal.line.end ? {
@@ -1123,10 +1378,81 @@ function stringCompletion(analyzer, document, position) {
1123
1378
  } : void 0;
1124
1379
  return values.map((value) => ({
1125
1380
  label: value,
1126
- kind: import_vscode_languageserver4.CompletionItemKind.Constant,
1381
+ kind: import_vscode_languageserver5.CompletionItemKind.Constant,
1127
1382
  ...range ? { textEdit: { range, newText: value } } : {}
1128
1383
  }));
1129
1384
  }
1385
+ function stringAt(analysis, position) {
1386
+ const path = pathAt(analysis.program, position, false);
1387
+ return [...path].reverse().find((n) => n.type === "StringLiteral" || n.type === "TypeLiteralString");
1388
+ }
1389
+ function objectKeyItems(analysis, path, after) {
1390
+ const index = path.findLastIndex(
1391
+ (n) => n.type === "Identifier" && n.name === PLACEHOLDER
1392
+ );
1393
+ const literal = index > 0 ? path[index - 1] : void 0;
1394
+ if (literal?.type !== "TableExpression") return void 0;
1395
+ const fields = literal.fields;
1396
+ const atKey = fields.some((f) => f.type === "TableFieldShorthand" && f.name?.name === PLACEHOLDER);
1397
+ if (!atKey) return void 0;
1398
+ let expected = analysis.types.expectedTypeOf.get(literal);
1399
+ for (let up = index - 2; expected === void 0 && up >= 0; up--) {
1400
+ const outer = path[up];
1401
+ if (outer.type !== "AsConstExpression" && outer.type !== "ParenthesizedExpression") break;
1402
+ expected = analysis.types.expectedTypeOf.get(outer);
1403
+ }
1404
+ const members = membersOf(expected, analysis.types.aliases);
1405
+ if (!members.length) return void 0;
1406
+ const written = /* @__PURE__ */ new Set();
1407
+ for (const field of fields) {
1408
+ if (field.type === "TableFieldNamed") written.add(field.key?.name ?? field.key?.value ?? "");
1409
+ else if (field.type === "TableFieldShorthand" && field.name?.name !== PLACEHOLDER) written.add(field.name.name);
1410
+ }
1411
+ const colon = /^\s*:/.test(after);
1412
+ return members.filter((member) => !written.has(member.name)).map((member) => ({
1413
+ label: member.name,
1414
+ kind: import_vscode_languageserver5.CompletionItemKind.Field,
1415
+ detail: `${member.property.optional ? "?" : ""}: ${(0, import_luaut_parser6.formatType)(member.property.type)}`,
1416
+ insertText: colon ? member.name : `${member.name}: `
1417
+ }));
1418
+ }
1419
+ function indexKeys(analysis, position, literal) {
1420
+ const path = pathAt(analysis.program, position, false);
1421
+ const at = path.indexOf(literal);
1422
+ const parent = at > 0 ? path[at - 1] : void 0;
1423
+ if (!parent) return [];
1424
+ let indexed;
1425
+ if (parent.type === "IndexedAccessTypeNode" && parent.indexType === literal) {
1426
+ indexed = analysis.types.typeOfTypeNode.get(parent.objectType);
1427
+ } else if (parent.type === "IndexExpression" && parent.index === literal) {
1428
+ indexed = withoutNil(analysis.types.typeOf.get(parent.object));
1429
+ }
1430
+ return membersOf(indexed, analysis.types.aliases).map((member) => member.name);
1431
+ }
1432
+ function repairedStrings(document, position) {
1433
+ const source = document.getText();
1434
+ const offset = document.offsetAt(position);
1435
+ const lineStart = offset - position.character;
1436
+ const lineEndIndex = source.indexOf("\n", offset);
1437
+ const lineEnd = lineEndIndex < 0 ? source.length : lineEndIndex;
1438
+ const before = source.slice(lineStart, offset);
1439
+ let quote;
1440
+ for (let i = 0; i < before.length; i++) {
1441
+ const ch = before[i];
1442
+ if (quote) {
1443
+ if (ch === "\\") i++;
1444
+ else if (ch === quote) quote = void 0;
1445
+ } else if (ch === '"' || ch === "'") {
1446
+ quote = ch;
1447
+ }
1448
+ }
1449
+ if (!quote) return [];
1450
+ let rest = source.slice(offset, lineEnd).replace(/\r$/, "");
1451
+ if (!rest.includes(quote)) rest += quote;
1452
+ const line = before + rest;
1453
+ const endings = ["", " then end", " do end", ")", ") then end", "]"];
1454
+ return endings.map((ending) => source.slice(0, lineStart) + line + ending + source.slice(lineEnd));
1455
+ }
1130
1456
  function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
1131
1457
  if (!type || seen.has(type)) return [];
1132
1458
  seen.add(type);
@@ -1158,27 +1484,29 @@ function memberOperator(source, wordStart) {
1158
1484
  }
1159
1485
  function memberItems(analysis, access) {
1160
1486
  const object = access.object;
1161
- const type = analysis.types.typeOf.get(object);
1487
+ const type = withoutNil(analysis.types.typeOf.get(object));
1162
1488
  const colon = access.type === "MethodCallExpression";
1163
- if (isStringLike(type)) {
1164
- if (!colon) return [];
1165
- const id = analysis.scopes.globalsByName.get("string");
1166
- const library = id === void 0 ? void 0 : analysis.types.bindingType.get(id);
1167
- return membersOf(library, analysis.types.aliases).filter((member) => signaturesOf(member.property.type).length > 0).map((member) => memberItem(member.name, member.property.type, member.property.readonly));
1168
- }
1489
+ if (isMethodOnly(type) && !colon) return [];
1169
1490
  return membersOf(type, analysis.types.aliases).filter((member) => colon ? member.isMethod : true).map((member) => memberItem(member.name, member.property.type, member.property.readonly));
1170
1491
  }
1171
- function isStringLike(type) {
1492
+ function withoutNil(type) {
1493
+ if (type?.kind !== "union") return type;
1494
+ const kept = type.types.filter((t) => !(t.kind === "primitive" && t.name === "nil"));
1495
+ return kept.length === 1 ? kept[0] : { ...type, types: kept };
1496
+ }
1497
+ function isMethodOnly(type) {
1172
1498
  if (!type) return false;
1173
1499
  switch (type.kind) {
1500
+ case "array":
1501
+ case "tuple":
1502
+ case "templateLiteral":
1503
+ return true;
1174
1504
  case "primitive":
1175
1505
  return type.name === "string";
1176
1506
  case "literal":
1177
1507
  return typeof type.value === "string";
1178
- case "templateLiteral":
1179
- return true;
1180
1508
  case "union":
1181
- return type.types.length > 0 && type.types.every(isStringLike);
1509
+ return type.types.length > 0 && type.types.every(isMethodOnly);
1182
1510
  default:
1183
1511
  return false;
1184
1512
  }
@@ -1196,13 +1524,13 @@ function valueItems(analysis, at) {
1196
1524
  items.push({
1197
1525
  label: binding.name,
1198
1526
  kind: kindOf(type, binding.kind),
1199
- detail: type ? (0, import_luaut_parser5.formatType)(type) : void 0,
1527
+ detail: type ? (0, import_luaut_parser6.formatType)(type) : void 0,
1200
1528
  // Locals before globals, and globals before library names.
1201
1529
  sortText: `${binding.isBuiltin ? 2 : binding.kind === "global" ? 1 : 0}${binding.name}`
1202
1530
  });
1203
1531
  }
1204
1532
  for (const keyword2 of KEYWORDS) {
1205
- items.push({ label: keyword2, kind: import_vscode_languageserver4.CompletionItemKind.Keyword, sortText: `3${keyword2}` });
1533
+ items.push({ label: keyword2, kind: import_vscode_languageserver5.CompletionItemKind.Keyword, sortText: `3${keyword2}` });
1206
1534
  }
1207
1535
  return items;
1208
1536
  }
@@ -1211,22 +1539,22 @@ function memberItem(name, type, readonly) {
1211
1539
  if (signatures.length) {
1212
1540
  return {
1213
1541
  label: name,
1214
- kind: import_vscode_languageserver4.CompletionItemKind.Method,
1542
+ kind: import_vscode_languageserver5.CompletionItemKind.Method,
1215
1543
  detail: signatureLabel(signatures[0]).label,
1216
1544
  insertText: `${name}($0)`,
1217
- insertTextFormat: import_vscode_languageserver4.InsertTextFormat.Snippet
1545
+ insertTextFormat: import_vscode_languageserver5.InsertTextFormat.Snippet
1218
1546
  };
1219
1547
  }
1220
1548
  return {
1221
1549
  label: name,
1222
- kind: import_vscode_languageserver4.CompletionItemKind.Field,
1223
- detail: `${readonly ? "readonly " : ""}${(0, import_luaut_parser5.formatType)(type)}`
1550
+ kind: import_vscode_languageserver5.CompletionItemKind.Field,
1551
+ detail: `${readonly ? "readonly " : ""}${(0, import_luaut_parser6.formatType)(type)}`
1224
1552
  };
1225
1553
  }
1226
1554
  function kindOf(type, bindingKind) {
1227
- if (type && signaturesOf(type).length) return import_vscode_languageserver4.CompletionItemKind.Function;
1228
- if (bindingKind === "param" || bindingKind === "self") return import_vscode_languageserver4.CompletionItemKind.Variable;
1229
- return import_vscode_languageserver4.CompletionItemKind.Variable;
1555
+ if (type && signaturesOf(type).length) return import_vscode_languageserver5.CompletionItemKind.Function;
1556
+ if (bindingKind === "param" || bindingKind === "self") return import_vscode_languageserver5.CompletionItemKind.Variable;
1557
+ return import_vscode_languageserver5.CompletionItemKind.Variable;
1230
1558
  }
1231
1559
  function inTypePosition(path) {
1232
1560
  return path.some(
@@ -1244,6 +1572,17 @@ var PRIMITIVES2 = [
1244
1572
  "thread",
1245
1573
  "buffer"
1246
1574
  ];
1575
+ var TYPE_KEYWORDS = ["keyof", "typeof", "infer", "extends"];
1576
+ function contextKeywords(before) {
1577
+ const keyword2 = (name) => ({
1578
+ label: name,
1579
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1580
+ sortText: `3${name}`
1581
+ });
1582
+ if (/<\s*[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*\s+$/.test(before)) return [keyword2("extends")];
1583
+ if (/[)\]}"'`\w][^\S\n]+$/.test(before)) return [keyword2("as"), keyword2("satisfies")];
1584
+ return [];
1585
+ }
1247
1586
  var KEYWORDS = [
1248
1587
  "const",
1249
1588
  "let",
@@ -1349,8 +1688,8 @@ function activeArgument(call, position) {
1349
1688
  }
1350
1689
 
1351
1690
  // src/features/symbols.ts
1352
- var import_vscode_languageserver5 = require("vscode-languageserver");
1353
- var import_luaut_parser6 = require("luaut-parser");
1691
+ var import_vscode_languageserver6 = require("vscode-languageserver");
1692
+ var import_luaut_parser7 = require("luaut-parser");
1354
1693
  function documentSymbols(analysis) {
1355
1694
  const out = [];
1356
1695
  walk(analysis.program, (node) => {
@@ -1358,7 +1697,7 @@ function documentSymbols(analysis) {
1358
1697
  case "FunctionDeclaration":
1359
1698
  case "FunctionDeclarationStatement": {
1360
1699
  const name = functionName(node);
1361
- if (name) out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Function, node, detailOf(analysis, node)));
1700
+ if (name) out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Function, node, detailOf(analysis, node)));
1362
1701
  break;
1363
1702
  }
1364
1703
  case "TypeAliasStatement":
@@ -1367,20 +1706,20 @@ function documentSymbols(analysis) {
1367
1706
  const name = typeof named === "string" ? named : named?.name;
1368
1707
  if (name) {
1369
1708
  const alias = analysis.types.aliases.get(name);
1370
- out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Interface, node, alias ? (0, import_luaut_parser6.formatType)(alias) : void 0));
1709
+ out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Interface, node, alias ? (0, import_luaut_parser7.formatType)(alias) : void 0));
1371
1710
  }
1372
1711
  break;
1373
1712
  }
1374
1713
  case "DeclareClassStatement": {
1375
1714
  const name = node.name.name;
1376
1715
  const superclass = node.superclass?.base;
1377
- out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Class, node, superclass && `extends ${superclass}`));
1716
+ out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Class, node, superclass && `extends ${superclass}`));
1378
1717
  break;
1379
1718
  }
1380
1719
  case "VariableDeclaration": {
1381
1720
  for (const target of node.names ?? []) {
1382
1721
  const name = target.name;
1383
- if (name) out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Variable, target));
1722
+ if (name) out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Variable, target));
1384
1723
  }
1385
1724
  break;
1386
1725
  }
@@ -1404,7 +1743,7 @@ function detailOf(analysis, node) {
1404
1743
  if (name && typeof name === "object") {
1405
1744
  const binding = bindingOfNode(analysis, name);
1406
1745
  const type = binding && analysis.types.bindingType.get(binding.id);
1407
- if (type) return (0, import_luaut_parser6.formatType)(type);
1746
+ if (type) return (0, import_luaut_parser7.formatType)(type);
1408
1747
  }
1409
1748
  return void 0;
1410
1749
  }
@@ -1414,7 +1753,7 @@ function symbol(name, kind, node, detail) {
1414
1753
  }
1415
1754
 
1416
1755
  // src/features/semanticTokens.ts
1417
- var import_luaut_parser7 = require("luaut-parser");
1756
+ var import_luaut_parser8 = require("luaut-parser");
1418
1757
  var TOKEN_TYPES = [
1419
1758
  "namespace",
1420
1759
  "type",
@@ -1458,7 +1797,7 @@ function semanticTokens(analysis) {
1458
1797
  };
1459
1798
  let tokens = [];
1460
1799
  try {
1461
- tokens = (0, import_luaut_parser7.tokenize)(analysis.source);
1800
+ tokens = (0, import_luaut_parser8.tokenize)(analysis.source);
1462
1801
  } catch {
1463
1802
  }
1464
1803
  const identifiers = tokens.filter((t) => t.type === "Identifier");
@@ -1502,7 +1841,7 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1502
1841
  if (!baseToken) return;
1503
1842
  if (!namespace && typeParameterInScope2(ancestors, base)) {
1504
1843
  add(baseToken, base.length, "typeParameter");
1505
- } else if ((0, import_luaut_parser7.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser7.unknownType)) {
1844
+ } else if ((0, import_luaut_parser8.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser8.unknownType)) {
1506
1845
  add(baseToken, base.length, "class");
1507
1846
  } else {
1508
1847
  add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);
@@ -1735,7 +2074,7 @@ function createServer(connection, options = {}) {
1735
2074
  severity: import_node.DiagnosticSeverity.Information,
1736
2075
  source: "luaut",
1737
2076
  code: "no-config",
1738
- 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 }'
2077
+ 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: { "types": [], "paths": {}, "sourceMap": null }, listing in `types` the type libraries the project has installed.'
1739
2078
  }];
1740
2079
  };
1741
2080
  documents.onDidOpen(publishAll);