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/index.cjs CHANGED
@@ -68,6 +68,19 @@ var import_luaut_parser2 = require("luaut-parser");
68
68
 
69
69
  // src/features/members.ts
70
70
  var import_luaut_parser = require("luaut-parser");
71
+ function literalKeys(key, aliases) {
72
+ if (!key) return [];
73
+ const resolved = key.kind === "genericRef" ? aliases.get(key.name) : key;
74
+ if (!resolved) return [];
75
+ const parts = resolved.kind === "union" ? resolved.types : [resolved];
76
+ const out = [];
77
+ for (const part of parts) {
78
+ const member = part.kind === "genericRef" ? aliases.get(part.name) ?? part : part;
79
+ if (member.kind !== "literal" || typeof member.value !== "string") return [];
80
+ out.push(member.value);
81
+ }
82
+ return out;
83
+ }
71
84
  function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
72
85
  if (!type || seen.has(type)) return [];
73
86
  seen.add(type);
@@ -77,6 +90,11 @@ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
77
90
  for (const [name, property] of type.properties) {
78
91
  out.push({ name, property, isMethod: takesSelf(property.type) });
79
92
  }
93
+ for (const name of literalKeys(type.indexer?.key, aliases)) {
94
+ if (type.properties.has(name)) continue;
95
+ const property = { type: type.indexer.value, optional: true };
96
+ out.push({ name, property, isMethod: takesSelf(property.type) });
97
+ }
80
98
  return out;
81
99
  }
82
100
  case "intersection": {
@@ -98,6 +116,20 @@ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
98
116
  }
99
117
  case "typeParam":
100
118
  return membersOf(type.constraint, aliases, seen);
119
+ // An array and a string answer to the methods the language gives them
120
+ // — `names:filter(f)`, `text:trim()`. They are written in the parser's
121
+ // prelude as `ArrayMethods<T>` and `StringMethods`, so the element
122
+ // type goes in where `T` stands.
123
+ case "array":
124
+ case "tuple": {
125
+ const element = type.kind === "array" ? type.element : (0, import_luaut_parser.union)(type.elements);
126
+ const methods = aliases.get("ArrayMethods");
127
+ return methods ? membersOf((0, import_luaut_parser.substitute)(methods, /* @__PURE__ */ new Map([["T", element]])), aliases, seen) : [];
128
+ }
129
+ case "primitive":
130
+ return type.name === "string" ? membersOf(aliases.get("StringMethods"), aliases, seen) : [];
131
+ case "literal":
132
+ return type.base === "string" ? membersOf(aliases.get("StringMethods"), aliases, seen) : [];
101
133
  default:
102
134
  return [];
103
135
  }
@@ -396,11 +428,13 @@ var Analyzer = class {
396
428
  const script = path ? context.sourceMap?.scriptFor(path) : void 0;
397
429
  const libs = script ? [...context.libs, script] : context.libs;
398
430
  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] });
431
+ const { program, errors, directives } = (0, import_luaut_parser2.parseWithRecovery)(source);
432
+ const reportUndeclared = context.libs.length > 0;
433
+ const scopes = (0, import_luaut_parser2.analyzeScopes)(program, { builtinGlobals: [...globals], reportUndeclared });
401
434
  const dependencies = new Map(context.reads);
402
435
  const types = (0, import_luaut_parser2.analyzeTypes)(program, scopes, {
403
436
  libs,
437
+ reportUnknownTypes: reportUndeclared,
404
438
  resolveModule: (specifier) => {
405
439
  if (!path) return void 0;
406
440
  const candidates = this.candidatesFor(path, specifier);
@@ -414,7 +448,7 @@ var Analyzer = class {
414
448
  return exports2;
415
449
  }
416
450
  });
417
- return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
451
+ return { uri, version, source, program, parseErrors: errors, directives, scopes, types, dependencies, project: context.project };
418
452
  }
419
453
  exportsOf(path, importing) {
420
454
  const key = pathKey(path);
@@ -509,7 +543,7 @@ function collect(container, out) {
509
543
  }
510
544
  }
511
545
  function isSpanlessNode(v) {
512
- return !!v && typeof v === "object" && typeof v.type === "string";
546
+ return !!v && typeof v === "object" && !Array.isArray(v);
513
547
  }
514
548
  function pathAt(root, pos, inclusive = false) {
515
549
  let best;
@@ -780,6 +814,7 @@ function patternNamed(target, name) {
780
814
 
781
815
  // src/features/diagnostics.ts
782
816
  var import_vscode_languageserver2 = require("vscode-languageserver");
817
+ var import_luaut_parser4 = require("luaut-parser");
783
818
  function diagnostics(analysis) {
784
819
  const out = [];
785
820
  for (const error of analysis.parseErrors) {
@@ -793,29 +828,39 @@ function diagnostics(analysis) {
793
828
  message: error.message.replace(/\s*\(\d+:\d+\)$/, "")
794
829
  });
795
830
  }
796
- for (const d of analysis.scopes.diagnostics) {
797
- out.push({
831
+ const semantic = [
832
+ ...analysis.scopes.diagnostics.map((d) => ({
798
833
  range: toRange(d.node),
799
834
  severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
800
835
  source: "luaut",
801
836
  code: d.kind,
802
837
  message: d.message
803
- });
804
- }
805
- for (const d of analysis.types.diagnostics) {
806
- out.push({
838
+ })),
839
+ ...analysis.types.diagnostics.map((d) => ({
807
840
  range: toRange(d.node),
808
841
  severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
809
842
  source: "luaut",
810
843
  code: "type",
811
844
  message: d.message
845
+ }))
846
+ ];
847
+ const { kept, unusedExpectErrors } = (0, import_luaut_parser4.applyDirectives)(analysis.directives, semantic, (d) => d.range.start.line + 1);
848
+ out.push(...kept);
849
+ for (const directive of unusedExpectErrors) {
850
+ const start = toPosition(directive.line, directive.column);
851
+ out.push({
852
+ range: { start, end: { line: start.line, character: start.character + "--@luaut-expect-error".length } },
853
+ severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
854
+ source: "luaut",
855
+ code: "directive",
856
+ message: import_luaut_parser4.UNUSED_EXPECT_ERROR
812
857
  });
813
858
  }
814
859
  return out;
815
860
  }
816
861
 
817
862
  // src/features/hover.ts
818
- var import_luaut_parser4 = require("luaut-parser");
863
+ var import_luaut_parser5 = require("luaut-parser");
819
864
  function hover(analysis, position) {
820
865
  const path = pathAt(analysis.program, position, true);
821
866
  for (let i = path.length - 1; i >= 0; i--) {
@@ -862,6 +907,27 @@ function describe(analysis, path, index) {
862
907
  const type2 = property?.type ?? types.typeOf.get(field.value);
863
908
  return type2 && `(property) ${name}: ${pretty(type2)}`;
864
909
  }
910
+ // `const { name } = t`: a shorthand key *is* the binding it
911
+ // declares, and has the same span, so the cursor can land on
912
+ // either. A renamed key (`{ name: other }`) names the property
913
+ // the value is read from.
914
+ case "ObjectPatternProperty": {
915
+ if (parent.key !== node || parent.computed) break;
916
+ const value = parent.value;
917
+ if (parent.shorthand) return describe(analysis, [...path.slice(0, index), value], index);
918
+ const binding2 = value.type === "IdentifierPattern" ? bindingOfNode(analysis, value) : void 0;
919
+ const type2 = binding2 && types.bindingType.get(binding2.id);
920
+ return type2 && `(property) ${name}: ${pretty(type2)}`;
921
+ }
922
+ // One line of an overload set reads as its own signature.
923
+ // The line the body is on reads as the whole set, which is
924
+ // what the binding says and what the default path gives.
925
+ case "FunctionSignature": {
926
+ if (parent.name !== node) break;
927
+ const own = types.typeOfTypeNode.get(parent);
928
+ if (own) return `function ${name}${pretty(own)}`;
929
+ break;
930
+ }
865
931
  case "ImportSpecifier": {
866
932
  const alias = types.aliases.get(name);
867
933
  const binding2 = bindingOfNode(analysis, identifier2);
@@ -907,7 +973,7 @@ function describe(analysis, path, index) {
907
973
  case "MappedTypeNode":
908
974
  if (parent.parameterId === node) {
909
975
  const keys = typeOfNode(parent.constraint);
910
- return `(type parameter) ${name}${keys ? ` in ${(0, import_luaut_parser4.formatType)(keys)}` : ""}`;
976
+ return `(type parameter) ${name}${keys ? ` in ${(0, import_luaut_parser5.formatType)(keys)}` : ""}`;
911
977
  }
912
978
  break;
913
979
  }
@@ -924,6 +990,11 @@ function describe(analysis, path, index) {
924
990
  }
925
991
  return void 0;
926
992
  }
993
+ // `...` — what this function's extra arguments are.
994
+ case "VarargExpression": {
995
+ const type2 = types.typeOf.get(node);
996
+ return type2 ? `(vararg) ...: ${pretty(type2)}` : void 0;
997
+ }
927
998
  // Declarations: `const x`, a parameter.
928
999
  case "IdentifierPattern":
929
1000
  case "FunctionParameter":
@@ -944,7 +1015,7 @@ function describe(analysis, path, index) {
944
1015
  if (!node.typeArguments.length) {
945
1016
  const qualified = node.namespace ? `${node.namespace}.${base}` : base;
946
1017
  const alias = types.aliases.get(qualified);
947
- if (alias && (0, import_luaut_parser4.isClassType)(alias)) return classText(analysis, qualified);
1018
+ if (alias && (0, import_luaut_parser5.isClassType)(alias)) return classText(analysis, qualified);
948
1019
  if (alias) return `type ${qualified} = ${pretty(alias)}`;
949
1020
  }
950
1021
  const type2 = typeOfNode(node);
@@ -972,17 +1043,17 @@ function declareText(analysis, statement) {
972
1043
  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);
973
1044
  const others = total - 1;
974
1045
  const overloads = others > 0 ? ` (+${others} overload${others > 1 ? "s" : ""})` : "";
975
- return `declare function ${name}${(0, import_luaut_parser4.formatType)(own)}${overloads}`;
1046
+ return `declare function ${name}${(0, import_luaut_parser5.formatType)(own)}${overloads}`;
976
1047
  }
977
1048
  function classText(analysis, name) {
978
1049
  const type = analysis.types.aliases.get(name);
979
- if (!type || !(0, import_luaut_parser4.isClassType)(type)) return void 0;
1050
+ if (!type || !(0, import_luaut_parser5.isClassType)(type)) return void 0;
980
1051
  const superclass = type.class.superclass;
981
1052
  const inherited = superclass ? analysis.types.aliases.get(superclass) : void 0;
982
1053
  const own = [...type.properties].filter(([key, property]) => inherited?.kind !== "object" || inherited.properties.get(key) !== property);
983
1054
  const head = `declare class ${name}${superclass ? ` extends ${superclass}` : ""}`;
984
1055
  if (!own.length) return `${head} {}`;
985
- const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser4.formatType)(property.type)},`);
1056
+ const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser5.formatType)(property.type)},`);
986
1057
  return `${head} {
987
1058
  ${lines.join("\n")}
988
1059
  }`;
@@ -994,7 +1065,7 @@ function typeParameterSignature(analysis, parameter) {
994
1065
  const p = parameter;
995
1066
  if (p.infer) return `infer ${p.name}`;
996
1067
  const constraint = p.constraint ? analysis.types.typeOfTypeNode.get(p.constraint) : void 0;
997
- return `${p.isConst ? "const " : ""}${p.name}${constraint ? ` extends ${(0, import_luaut_parser4.formatType)(constraint)}` : ""}`;
1068
+ return `${p.isConst ? "const " : ""}${p.name}${constraint ? ` extends ${(0, import_luaut_parser5.formatType)(constraint)}` : ""}`;
998
1069
  }
999
1070
  function typeParameterInScope(path, index, name) {
1000
1071
  for (let i = index - 1; i >= 0; i--) {
@@ -1019,7 +1090,7 @@ function referenceText(analysis, reference) {
1019
1090
  if (!args.length) return name;
1020
1091
  const resolved = args.map((a) => {
1021
1092
  const t = analysis.types.typeOfTypeNode.get(a);
1022
- return t ? (0, import_luaut_parser4.formatType)(t) : "?";
1093
+ return t ? (0, import_luaut_parser5.formatType)(t) : "?";
1023
1094
  });
1024
1095
  return `${name}<${resolved.join(", ")}>`;
1025
1096
  }
@@ -1028,27 +1099,27 @@ function fieldWithKey(table, key) {
1028
1099
  return fields.find((f) => f.type === "TableFieldNamed" && f.key === key);
1029
1100
  }
1030
1101
  function pretty(type) {
1031
- const flat = (0, import_luaut_parser4.formatType)(type);
1102
+ const flat = (0, import_luaut_parser5.formatType)(type);
1032
1103
  if (flat.length <= 80) return flat;
1033
1104
  if (type.kind === "object") {
1034
1105
  const lines = [];
1035
- if (type.indexer) lines.push(` [${(0, import_luaut_parser4.formatType)(type.indexer.key)}]: ${(0, import_luaut_parser4.formatType)(type.indexer.value)},`);
1106
+ if (type.indexer) lines.push(` [${(0, import_luaut_parser5.formatType)(type.indexer.key)}]: ${(0, import_luaut_parser5.formatType)(type.indexer.value)},`);
1036
1107
  for (const [name, property] of type.properties) {
1037
1108
  const readonly = property.readonly ? "readonly " : "";
1038
- lines.push(` ${readonly}${name}${property.optional ? "?" : ""}: ${(0, import_luaut_parser4.formatType)(property.type)},`);
1109
+ lines.push(` ${readonly}${name}${property.optional ? "?" : ""}: ${(0, import_luaut_parser5.formatType)(property.type)},`);
1039
1110
  }
1040
1111
  return `{
1041
1112
  ${lines.join("\n")}
1042
1113
  }`;
1043
1114
  }
1044
1115
  if (type.kind === "intersection" && type.types.every((t) => t.kind === "function")) {
1045
- return type.types.map(import_luaut_parser4.formatType).join("\n& ");
1116
+ return type.types.map(import_luaut_parser5.formatType).join("\n& ");
1046
1117
  }
1047
1118
  return flat;
1048
1119
  }
1049
1120
  function bindingText(binding, type) {
1050
1121
  if (binding.declaredBy === "function" && type.kind === "function") {
1051
- return `function ${binding.name}${(0, import_luaut_parser4.formatType)(type)}`;
1122
+ return `function ${binding.name}${(0, import_luaut_parser5.formatType)(type)}`;
1052
1123
  }
1053
1124
  return `${keyword(binding)} ${binding.name}: ${pretty(type)}`;
1054
1125
  }
@@ -1125,8 +1196,162 @@ function isIdentifier(name) {
1125
1196
  }
1126
1197
 
1127
1198
  // src/features/completion.ts
1199
+ var import_vscode_languageserver5 = require("vscode-languageserver");
1200
+ var import_luaut_parser6 = require("luaut-parser");
1201
+
1202
+ // src/features/autoImport.ts
1203
+ var import_node_fs3 = require("fs");
1204
+ var import_node_path3 = require("path");
1128
1205
  var import_vscode_languageserver4 = require("vscode-languageserver");
1129
- var import_luaut_parser5 = require("luaut-parser");
1206
+ function importItems(analyzer, analysis, typePosition, taken) {
1207
+ const from = pathOfUri(analysis.uri);
1208
+ if (!from) return [];
1209
+ const config = analysis.project.config;
1210
+ const items = [];
1211
+ const offered = /* @__PURE__ */ new Set();
1212
+ for (const file of projectFiles(config?.directory ?? (0, import_node_path3.dirname)(from))) {
1213
+ if (samePath(file, from)) continue;
1214
+ const exports2 = analyzer.exportsAt(file);
1215
+ if (!exports2 || exports2.partial) continue;
1216
+ const names = typePosition ? [...exports2.types.keys()] : [...exports2.values.keys()];
1217
+ const specifier = specifierFor(from, file, config);
1218
+ for (const name of names) {
1219
+ if (taken.has(name) || offered.has(name)) continue;
1220
+ offered.add(name);
1221
+ const type = typePosition ? exports2.types.get(name)?.type : exports2.values.get(name);
1222
+ items.push({
1223
+ label: name,
1224
+ kind: typePosition ? import_vscode_languageserver4.CompletionItemKind.Interface : type && signaturesOf(type).length ? import_vscode_languageserver4.CompletionItemKind.Function : import_vscode_languageserver4.CompletionItemKind.Variable,
1225
+ labelDetails: { description: specifier },
1226
+ detail: `import { ${name} } from "${specifier}"`,
1227
+ sortText: `4${name}`,
1228
+ additionalTextEdits: [importEdit(analyzer, analysis, file, name, specifier, typePosition)]
1229
+ });
1230
+ }
1231
+ }
1232
+ return items;
1233
+ }
1234
+ function serviceItems(analysis, taken) {
1235
+ const services = analysis.types.aliases.get("Services");
1236
+ if (!services || services.kind !== "object") return [];
1237
+ const at = serviceInsertion(analysis.program.body.statements);
1238
+ const items = [];
1239
+ for (const name of services.properties.keys()) {
1240
+ if (taken.has(name)) continue;
1241
+ const line = `const ${name} = game:GetService("${name}")`;
1242
+ items.push({
1243
+ label: name,
1244
+ kind: import_vscode_languageserver4.CompletionItemKind.Module,
1245
+ labelDetails: { description: "service" },
1246
+ detail: line,
1247
+ sortText: `5${name}`,
1248
+ additionalTextEdits: [{ range: { start: at.position, end: at.position }, newText: `${line}
1249
+ ${at.gap}` }]
1250
+ });
1251
+ }
1252
+ return items;
1253
+ }
1254
+ function importEdit(analyzer, analysis, file, name, specifier, typePosition) {
1255
+ const statements = analysis.program.body.statements;
1256
+ const imports = statements.filter((s) => s.type === "ImportStatement");
1257
+ const existing = imports.find((s) => !s.namespaceImport && (typePosition || !s.isTypeOnly) && samePathOrUndefined(analyzer.resolveModulePath(analysis.uri, s.source.value), file));
1258
+ if (existing) {
1259
+ const last = existing.specifiers[existing.specifiers.length - 1];
1260
+ if (last) {
1261
+ const at2 = endOf(last);
1262
+ return { range: { start: at2, end: at2 }, newText: `, ${name}` };
1263
+ }
1264
+ if (existing.defaultImport) {
1265
+ const at2 = endOf(existing.defaultImport);
1266
+ return { range: { start: at2, end: at2 }, newText: `, { ${name} }` };
1267
+ }
1268
+ }
1269
+ const line = `import { ${name} } from "${specifier}"`;
1270
+ const lastImport = imports[imports.length - 1];
1271
+ if (lastImport) {
1272
+ const at2 = { line: lastImport.line.end, character: 0 };
1273
+ return { range: { start: at2, end: at2 }, newText: `${line}
1274
+ ` };
1275
+ }
1276
+ const first = statements[0];
1277
+ const at = { line: first ? first.line.start - 1 : 0, character: 0 };
1278
+ return { range: { start: at, end: at }, newText: first ? `${line}
1279
+
1280
+ ` : `${line}
1281
+ ` };
1282
+ }
1283
+ function serviceInsertion(statements) {
1284
+ let last;
1285
+ for (const statement of statements) {
1286
+ if (statement.type !== "ImportStatement" && !isServiceDeclaration(statement)) break;
1287
+ last = statement;
1288
+ }
1289
+ if (last) return { position: { line: last.line.end, character: 0 }, gap: "" };
1290
+ const first = statements[0];
1291
+ return first ? { position: { line: first.line.start - 1, character: 0 }, gap: "\n" } : { position: { line: 0, character: 0 }, gap: "" };
1292
+ }
1293
+ function isServiceDeclaration(statement) {
1294
+ if (statement.type !== "VariableDeclaration") return false;
1295
+ const init = statement.init[0];
1296
+ return init?.type === "MethodCallExpression" && init.method.name === "GetService" && init.object.type === "Identifier" && init.object.name === "game";
1297
+ }
1298
+ function endOf(node) {
1299
+ return { line: node.line.end - 1, character: node.column.end - 1 };
1300
+ }
1301
+ function samePathOrUndefined(a, b) {
1302
+ return a !== void 0 && samePath(a, b);
1303
+ }
1304
+ function specifierFor(from, target, config) {
1305
+ const withoutExtension = (path) => {
1306
+ const bare = path.replace(/\\/g, "/").replace(/\.luaut$/, "");
1307
+ return bare.endsWith("/index") ? bare.slice(0, -"/index".length) : bare;
1308
+ };
1309
+ let relativePath = withoutExtension((0, import_node_path3.relative)((0, import_node_path3.dirname)(from), target));
1310
+ if (!relativePath.startsWith(".")) relativePath = `./${relativePath}`;
1311
+ if (!relativePath.startsWith("../") || !config) return relativePath;
1312
+ for (const [pattern, targets] of Object.entries(config.paths)) {
1313
+ const star = pattern.indexOf("*");
1314
+ if (star < 0) continue;
1315
+ for (const targetPattern of targets) {
1316
+ const cut = targetPattern.indexOf("*");
1317
+ if (cut < 0) continue;
1318
+ const head = (0, import_node_path3.resolve)(config.baseUrl, targetPattern.slice(0, cut));
1319
+ const rest = (0, import_node_path3.relative)(head, target);
1320
+ if (rest.startsWith("..") || (0, import_node_path3.resolve)(head, rest) !== (0, import_node_path3.resolve)(target)) continue;
1321
+ return `${pattern.slice(0, star)}${withoutExtension(rest)}${pattern.slice(star + 1)}`;
1322
+ }
1323
+ }
1324
+ return relativePath;
1325
+ }
1326
+ var FILE_LIMIT = 2e3;
1327
+ var LISTING_TTL = 3e3;
1328
+ var listings = /* @__PURE__ */ new Map();
1329
+ function projectFiles(root) {
1330
+ const cached = listings.get(root);
1331
+ if (cached && Date.now() - cached.at < LISTING_TTL) return cached.files;
1332
+ const files = [];
1333
+ const walk2 = (directory, depth) => {
1334
+ if (files.length >= FILE_LIMIT || depth > 12) return;
1335
+ let entries;
1336
+ try {
1337
+ entries = (0, import_node_fs3.readdirSync)(directory, { withFileTypes: true });
1338
+ } catch {
1339
+ return;
1340
+ }
1341
+ for (const entry of entries) {
1342
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1343
+ const path = (0, import_node_path3.join)(directory, entry.name);
1344
+ if (entry.isDirectory()) walk2(path, depth + 1);
1345
+ else if (entry.name.endsWith(".luaut") && !entry.name.endsWith(".d.luaut")) files.push(path);
1346
+ if (files.length >= FILE_LIMIT) return;
1347
+ }
1348
+ };
1349
+ walk2(root, 0);
1350
+ listings.set(root, { at: Date.now(), files });
1351
+ return files;
1352
+ }
1353
+
1354
+ // src/features/completion.ts
1130
1355
  var PLACEHOLDER = "__luautCompletion__";
1131
1356
  var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
1132
1357
  function completion(analyzer, document, position) {
@@ -1159,28 +1384,58 @@ function completion(analyzer, document, position) {
1159
1384
  first ??= { analysis, path };
1160
1385
  }
1161
1386
  if (operator || !first) return [];
1387
+ const keys = objectKeyItems(first.analysis, first.path, source.slice(end));
1388
+ if (keys) return keys;
1162
1389
  if (inTypePosition(first.path)) {
1163
1390
  const named = [...first.analysis.types.aliases].map(([name, type]) => ({
1164
1391
  label: name,
1165
- kind: (0, import_luaut_parser5.isClassType)(type) ? import_vscode_languageserver4.CompletionItemKind.Class : import_vscode_languageserver4.CompletionItemKind.Interface,
1166
- detail: (0, import_luaut_parser5.isClassType)(type) ? "class" : "type"
1392
+ kind: (0, import_luaut_parser6.isClassType)(type) ? import_vscode_languageserver5.CompletionItemKind.Class : import_vscode_languageserver5.CompletionItemKind.Interface,
1393
+ detail: (0, import_luaut_parser6.isClassType)(type) ? "class" : "type"
1167
1394
  }));
1168
1395
  const primitives = PRIMITIVES2.map((name) => ({
1169
1396
  label: name,
1170
- kind: import_vscode_languageserver4.CompletionItemKind.Keyword,
1397
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1171
1398
  detail: "type"
1172
1399
  }));
1173
- return [...named, ...primitives];
1174
- }
1175
- return valueItems(first.analysis, at);
1400
+ const keywords = TYPE_KEYWORDS.map((name) => ({
1401
+ label: name,
1402
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1403
+ sortText: `3${name}`
1404
+ }));
1405
+ const typeNames = new Set(first.analysis.types.aliases.keys());
1406
+ const imported = importItems(analyzer, analyzer.get(document), true, typeNames);
1407
+ return [...named, ...primitives, ...keywords, ...imported];
1408
+ }
1409
+ const taken = /* @__PURE__ */ new Set();
1410
+ for (const binding of first.analysis.scopes.bindings.values()) taken.add(binding.name);
1411
+ const current = analyzer.get(document);
1412
+ return [
1413
+ ...valueItems(first.analysis, at),
1414
+ ...contextKeywords(source.slice(0, start)),
1415
+ ...importItems(analyzer, current, false, taken),
1416
+ ...serviceItems(current, taken)
1417
+ ];
1176
1418
  }
1177
1419
  function stringCompletion(analyzer, document, position) {
1178
- const analysis = analyzer.get(document);
1179
- const path = pathAt(analysis.program, position, false);
1180
- const literal = [...path].reverse().find((n) => n.type === "StringLiteral");
1181
- if (!literal) return void 0;
1420
+ let analysis = analyzer.get(document);
1421
+ let literal = stringAt(analysis, position);
1422
+ if (!literal) {
1423
+ const repaired = repairedStrings(document, position);
1424
+ for (const text of repaired) {
1425
+ const candidate = analyzer.analyze(document.uri, -1, text);
1426
+ literal = stringAt(candidate, position);
1427
+ if (literal) {
1428
+ analysis = candidate;
1429
+ break;
1430
+ }
1431
+ }
1432
+ if (!literal) return repaired.length ? [] : void 0;
1433
+ }
1182
1434
  const expected = analysis.types.expectedTypeOf.get(literal);
1183
- const values = stringLiterals(expected, analysis.types.aliases);
1435
+ const values = [.../* @__PURE__ */ new Set([
1436
+ ...stringLiterals(expected, analysis.types.aliases),
1437
+ ...indexKeys(analysis, position, literal)
1438
+ ])];
1184
1439
  if (!values.length) return [];
1185
1440
  const line = literal.line.start - 1;
1186
1441
  const range = literal.line.start === literal.line.end ? {
@@ -1189,10 +1444,81 @@ function stringCompletion(analyzer, document, position) {
1189
1444
  } : void 0;
1190
1445
  return values.map((value) => ({
1191
1446
  label: value,
1192
- kind: import_vscode_languageserver4.CompletionItemKind.Constant,
1447
+ kind: import_vscode_languageserver5.CompletionItemKind.Constant,
1193
1448
  ...range ? { textEdit: { range, newText: value } } : {}
1194
1449
  }));
1195
1450
  }
1451
+ function stringAt(analysis, position) {
1452
+ const path = pathAt(analysis.program, position, false);
1453
+ return [...path].reverse().find((n) => n.type === "StringLiteral" || n.type === "TypeLiteralString");
1454
+ }
1455
+ function objectKeyItems(analysis, path, after) {
1456
+ const index = path.findLastIndex(
1457
+ (n) => n.type === "Identifier" && n.name === PLACEHOLDER
1458
+ );
1459
+ const literal = index > 0 ? path[index - 1] : void 0;
1460
+ if (literal?.type !== "TableExpression") return void 0;
1461
+ const fields = literal.fields;
1462
+ const atKey = fields.some((f) => f.type === "TableFieldShorthand" && f.name?.name === PLACEHOLDER);
1463
+ if (!atKey) return void 0;
1464
+ let expected = analysis.types.expectedTypeOf.get(literal);
1465
+ for (let up = index - 2; expected === void 0 && up >= 0; up--) {
1466
+ const outer = path[up];
1467
+ if (outer.type !== "AsConstExpression" && outer.type !== "ParenthesizedExpression") break;
1468
+ expected = analysis.types.expectedTypeOf.get(outer);
1469
+ }
1470
+ const members = membersOf(expected, analysis.types.aliases);
1471
+ if (!members.length) return void 0;
1472
+ const written = /* @__PURE__ */ new Set();
1473
+ for (const field of fields) {
1474
+ if (field.type === "TableFieldNamed") written.add(field.key?.name ?? field.key?.value ?? "");
1475
+ else if (field.type === "TableFieldShorthand" && field.name?.name !== PLACEHOLDER) written.add(field.name.name);
1476
+ }
1477
+ const colon = /^\s*:/.test(after);
1478
+ return members.filter((member) => !written.has(member.name)).map((member) => ({
1479
+ label: member.name,
1480
+ kind: import_vscode_languageserver5.CompletionItemKind.Field,
1481
+ detail: `${member.property.optional ? "?" : ""}: ${(0, import_luaut_parser6.formatType)(member.property.type)}`,
1482
+ insertText: colon ? member.name : `${member.name}: `
1483
+ }));
1484
+ }
1485
+ function indexKeys(analysis, position, literal) {
1486
+ const path = pathAt(analysis.program, position, false);
1487
+ const at = path.indexOf(literal);
1488
+ const parent = at > 0 ? path[at - 1] : void 0;
1489
+ if (!parent) return [];
1490
+ let indexed;
1491
+ if (parent.type === "IndexedAccessTypeNode" && parent.indexType === literal) {
1492
+ indexed = analysis.types.typeOfTypeNode.get(parent.objectType);
1493
+ } else if (parent.type === "IndexExpression" && parent.index === literal) {
1494
+ indexed = withoutNil(analysis.types.typeOf.get(parent.object));
1495
+ }
1496
+ return membersOf(indexed, analysis.types.aliases).map((member) => member.name);
1497
+ }
1498
+ function repairedStrings(document, position) {
1499
+ const source = document.getText();
1500
+ const offset = document.offsetAt(position);
1501
+ const lineStart = offset - position.character;
1502
+ const lineEndIndex = source.indexOf("\n", offset);
1503
+ const lineEnd = lineEndIndex < 0 ? source.length : lineEndIndex;
1504
+ const before = source.slice(lineStart, offset);
1505
+ let quote;
1506
+ for (let i = 0; i < before.length; i++) {
1507
+ const ch = before[i];
1508
+ if (quote) {
1509
+ if (ch === "\\") i++;
1510
+ else if (ch === quote) quote = void 0;
1511
+ } else if (ch === '"' || ch === "'") {
1512
+ quote = ch;
1513
+ }
1514
+ }
1515
+ if (!quote) return [];
1516
+ let rest = source.slice(offset, lineEnd).replace(/\r$/, "");
1517
+ if (!rest.includes(quote)) rest += quote;
1518
+ const line = before + rest;
1519
+ const endings = ["", " then end", " do end", ")", ") then end", "]"];
1520
+ return endings.map((ending) => source.slice(0, lineStart) + line + ending + source.slice(lineEnd));
1521
+ }
1196
1522
  function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
1197
1523
  if (!type || seen.has(type)) return [];
1198
1524
  seen.add(type);
@@ -1224,27 +1550,29 @@ function memberOperator(source, wordStart) {
1224
1550
  }
1225
1551
  function memberItems(analysis, access) {
1226
1552
  const object = access.object;
1227
- const type = analysis.types.typeOf.get(object);
1553
+ const type = withoutNil(analysis.types.typeOf.get(object));
1228
1554
  const colon = access.type === "MethodCallExpression";
1229
- if (isStringLike(type)) {
1230
- if (!colon) return [];
1231
- const id = analysis.scopes.globalsByName.get("string");
1232
- const library = id === void 0 ? void 0 : analysis.types.bindingType.get(id);
1233
- 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));
1234
- }
1555
+ if (isMethodOnly(type) && !colon) return [];
1235
1556
  return membersOf(type, analysis.types.aliases).filter((member) => colon ? member.isMethod : true).map((member) => memberItem(member.name, member.property.type, member.property.readonly));
1236
1557
  }
1237
- function isStringLike(type) {
1558
+ function withoutNil(type) {
1559
+ if (type?.kind !== "union") return type;
1560
+ const kept = type.types.filter((t) => !(t.kind === "primitive" && t.name === "nil"));
1561
+ return kept.length === 1 ? kept[0] : { ...type, types: kept };
1562
+ }
1563
+ function isMethodOnly(type) {
1238
1564
  if (!type) return false;
1239
1565
  switch (type.kind) {
1566
+ case "array":
1567
+ case "tuple":
1568
+ case "templateLiteral":
1569
+ return true;
1240
1570
  case "primitive":
1241
1571
  return type.name === "string";
1242
1572
  case "literal":
1243
1573
  return typeof type.value === "string";
1244
- case "templateLiteral":
1245
- return true;
1246
1574
  case "union":
1247
- return type.types.length > 0 && type.types.every(isStringLike);
1575
+ return type.types.length > 0 && type.types.every(isMethodOnly);
1248
1576
  default:
1249
1577
  return false;
1250
1578
  }
@@ -1262,13 +1590,13 @@ function valueItems(analysis, at) {
1262
1590
  items.push({
1263
1591
  label: binding.name,
1264
1592
  kind: kindOf(type, binding.kind),
1265
- detail: type ? (0, import_luaut_parser5.formatType)(type) : void 0,
1593
+ detail: type ? (0, import_luaut_parser6.formatType)(type) : void 0,
1266
1594
  // Locals before globals, and globals before library names.
1267
1595
  sortText: `${binding.isBuiltin ? 2 : binding.kind === "global" ? 1 : 0}${binding.name}`
1268
1596
  });
1269
1597
  }
1270
1598
  for (const keyword2 of KEYWORDS) {
1271
- items.push({ label: keyword2, kind: import_vscode_languageserver4.CompletionItemKind.Keyword, sortText: `3${keyword2}` });
1599
+ items.push({ label: keyword2, kind: import_vscode_languageserver5.CompletionItemKind.Keyword, sortText: `3${keyword2}` });
1272
1600
  }
1273
1601
  return items;
1274
1602
  }
@@ -1277,22 +1605,22 @@ function memberItem(name, type, readonly) {
1277
1605
  if (signatures.length) {
1278
1606
  return {
1279
1607
  label: name,
1280
- kind: import_vscode_languageserver4.CompletionItemKind.Method,
1608
+ kind: import_vscode_languageserver5.CompletionItemKind.Method,
1281
1609
  detail: signatureLabel(signatures[0]).label,
1282
1610
  insertText: `${name}($0)`,
1283
- insertTextFormat: import_vscode_languageserver4.InsertTextFormat.Snippet
1611
+ insertTextFormat: import_vscode_languageserver5.InsertTextFormat.Snippet
1284
1612
  };
1285
1613
  }
1286
1614
  return {
1287
1615
  label: name,
1288
- kind: import_vscode_languageserver4.CompletionItemKind.Field,
1289
- detail: `${readonly ? "readonly " : ""}${(0, import_luaut_parser5.formatType)(type)}`
1616
+ kind: import_vscode_languageserver5.CompletionItemKind.Field,
1617
+ detail: `${readonly ? "readonly " : ""}${(0, import_luaut_parser6.formatType)(type)}`
1290
1618
  };
1291
1619
  }
1292
1620
  function kindOf(type, bindingKind) {
1293
- if (type && signaturesOf(type).length) return import_vscode_languageserver4.CompletionItemKind.Function;
1294
- if (bindingKind === "param" || bindingKind === "self") return import_vscode_languageserver4.CompletionItemKind.Variable;
1295
- return import_vscode_languageserver4.CompletionItemKind.Variable;
1621
+ if (type && signaturesOf(type).length) return import_vscode_languageserver5.CompletionItemKind.Function;
1622
+ if (bindingKind === "param" || bindingKind === "self") return import_vscode_languageserver5.CompletionItemKind.Variable;
1623
+ return import_vscode_languageserver5.CompletionItemKind.Variable;
1296
1624
  }
1297
1625
  function inTypePosition(path) {
1298
1626
  return path.some(
@@ -1310,6 +1638,17 @@ var PRIMITIVES2 = [
1310
1638
  "thread",
1311
1639
  "buffer"
1312
1640
  ];
1641
+ var TYPE_KEYWORDS = ["keyof", "typeof", "infer", "extends"];
1642
+ function contextKeywords(before) {
1643
+ const keyword2 = (name) => ({
1644
+ label: name,
1645
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1646
+ sortText: `3${name}`
1647
+ });
1648
+ if (/<\s*[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*\s+$/.test(before)) return [keyword2("extends")];
1649
+ if (/[)\]}"'`\w][^\S\n]+$/.test(before)) return [keyword2("as"), keyword2("satisfies")];
1650
+ return [];
1651
+ }
1313
1652
  var KEYWORDS = [
1314
1653
  "const",
1315
1654
  "let",
@@ -1415,8 +1754,8 @@ function activeArgument(call, position) {
1415
1754
  }
1416
1755
 
1417
1756
  // src/features/symbols.ts
1418
- var import_vscode_languageserver5 = require("vscode-languageserver");
1419
- var import_luaut_parser6 = require("luaut-parser");
1757
+ var import_vscode_languageserver6 = require("vscode-languageserver");
1758
+ var import_luaut_parser7 = require("luaut-parser");
1420
1759
  function documentSymbols(analysis) {
1421
1760
  const out = [];
1422
1761
  walk(analysis.program, (node) => {
@@ -1424,7 +1763,7 @@ function documentSymbols(analysis) {
1424
1763
  case "FunctionDeclaration":
1425
1764
  case "FunctionDeclarationStatement": {
1426
1765
  const name = functionName(node);
1427
- if (name) out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Function, node, detailOf(analysis, node)));
1766
+ if (name) out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Function, node, detailOf(analysis, node)));
1428
1767
  break;
1429
1768
  }
1430
1769
  case "TypeAliasStatement":
@@ -1433,20 +1772,20 @@ function documentSymbols(analysis) {
1433
1772
  const name = typeof named === "string" ? named : named?.name;
1434
1773
  if (name) {
1435
1774
  const alias = analysis.types.aliases.get(name);
1436
- out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Interface, node, alias ? (0, import_luaut_parser6.formatType)(alias) : void 0));
1775
+ out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Interface, node, alias ? (0, import_luaut_parser7.formatType)(alias) : void 0));
1437
1776
  }
1438
1777
  break;
1439
1778
  }
1440
1779
  case "DeclareClassStatement": {
1441
1780
  const name = node.name.name;
1442
1781
  const superclass = node.superclass?.base;
1443
- out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Class, node, superclass && `extends ${superclass}`));
1782
+ out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Class, node, superclass && `extends ${superclass}`));
1444
1783
  break;
1445
1784
  }
1446
1785
  case "VariableDeclaration": {
1447
1786
  for (const target of node.names ?? []) {
1448
1787
  const name = target.name;
1449
- if (name) out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Variable, target));
1788
+ if (name) out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Variable, target));
1450
1789
  }
1451
1790
  break;
1452
1791
  }
@@ -1470,7 +1809,7 @@ function detailOf(analysis, node) {
1470
1809
  if (name && typeof name === "object") {
1471
1810
  const binding = bindingOfNode(analysis, name);
1472
1811
  const type = binding && analysis.types.bindingType.get(binding.id);
1473
- if (type) return (0, import_luaut_parser6.formatType)(type);
1812
+ if (type) return (0, import_luaut_parser7.formatType)(type);
1474
1813
  }
1475
1814
  return void 0;
1476
1815
  }
@@ -1480,7 +1819,7 @@ function symbol(name, kind, node, detail) {
1480
1819
  }
1481
1820
 
1482
1821
  // src/features/semanticTokens.ts
1483
- var import_luaut_parser7 = require("luaut-parser");
1822
+ var import_luaut_parser8 = require("luaut-parser");
1484
1823
  var TOKEN_TYPES = [
1485
1824
  "namespace",
1486
1825
  "type",
@@ -1524,7 +1863,7 @@ function semanticTokens(analysis) {
1524
1863
  };
1525
1864
  let tokens = [];
1526
1865
  try {
1527
- tokens = (0, import_luaut_parser7.tokenize)(analysis.source);
1866
+ tokens = (0, import_luaut_parser8.tokenize)(analysis.source);
1528
1867
  } catch {
1529
1868
  }
1530
1869
  const identifiers = tokens.filter((t) => t.type === "Identifier");
@@ -1568,7 +1907,7 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1568
1907
  if (!baseToken) return;
1569
1908
  if (!namespace && typeParameterInScope2(ancestors, base)) {
1570
1909
  add(baseToken, base.length, "typeParameter");
1571
- } else if ((0, import_luaut_parser7.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser7.unknownType)) {
1910
+ } else if ((0, import_luaut_parser8.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser8.unknownType)) {
1572
1911
  add(baseToken, base.length, "class");
1573
1912
  } else {
1574
1913
  add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);
@@ -1801,7 +2140,7 @@ function createServer(connection, options = {}) {
1801
2140
  severity: import_node.DiagnosticSeverity.Information,
1802
2141
  source: "luaut",
1803
2142
  code: "no-config",
1804
- 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 }'
2143
+ 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.'
1805
2144
  }];
1806
2145
  };
1807
2146
  documents.onDidOpen(publishAll);