luaut-language-server 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -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": {
@@ -396,11 +414,13 @@ var Analyzer = class {
396
414
  const script = path ? context.sourceMap?.scriptFor(path) : void 0;
397
415
  const libs = script ? [...context.libs, script] : context.libs;
398
416
  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] });
417
+ const { program, errors, directives } = (0, import_luaut_parser2.parseWithRecovery)(source);
418
+ const reportUndeclared = context.libs.length > 0;
419
+ const scopes = (0, import_luaut_parser2.analyzeScopes)(program, { builtinGlobals: [...globals], reportUndeclared });
401
420
  const dependencies = new Map(context.reads);
402
421
  const types = (0, import_luaut_parser2.analyzeTypes)(program, scopes, {
403
422
  libs,
423
+ reportUnknownTypes: reportUndeclared,
404
424
  resolveModule: (specifier) => {
405
425
  if (!path) return void 0;
406
426
  const candidates = this.candidatesFor(path, specifier);
@@ -414,7 +434,7 @@ var Analyzer = class {
414
434
  return exports2;
415
435
  }
416
436
  });
417
- return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
437
+ return { uri, version, source, program, parseErrors: errors, directives, scopes, types, dependencies, project: context.project };
418
438
  }
419
439
  exportsOf(path, importing) {
420
440
  const key = pathKey(path);
@@ -509,7 +529,7 @@ function collect(container, out) {
509
529
  }
510
530
  }
511
531
  function isSpanlessNode(v) {
512
- return !!v && typeof v === "object" && typeof v.type === "string";
532
+ return !!v && typeof v === "object" && !Array.isArray(v);
513
533
  }
514
534
  function pathAt(root, pos, inclusive = false) {
515
535
  let best;
@@ -780,6 +800,7 @@ function patternNamed(target, name) {
780
800
 
781
801
  // src/features/diagnostics.ts
782
802
  var import_vscode_languageserver2 = require("vscode-languageserver");
803
+ var import_luaut_parser4 = require("luaut-parser");
783
804
  function diagnostics(analysis) {
784
805
  const out = [];
785
806
  for (const error of analysis.parseErrors) {
@@ -793,29 +814,39 @@ function diagnostics(analysis) {
793
814
  message: error.message.replace(/\s*\(\d+:\d+\)$/, "")
794
815
  });
795
816
  }
796
- for (const d of analysis.scopes.diagnostics) {
797
- out.push({
817
+ const semantic = [
818
+ ...analysis.scopes.diagnostics.map((d) => ({
798
819
  range: toRange(d.node),
799
820
  severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
800
821
  source: "luaut",
801
822
  code: d.kind,
802
823
  message: d.message
803
- });
804
- }
805
- for (const d of analysis.types.diagnostics) {
806
- out.push({
824
+ })),
825
+ ...analysis.types.diagnostics.map((d) => ({
807
826
  range: toRange(d.node),
808
827
  severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
809
828
  source: "luaut",
810
829
  code: "type",
811
830
  message: d.message
831
+ }))
832
+ ];
833
+ const { kept, unusedExpectErrors } = (0, import_luaut_parser4.applyDirectives)(analysis.directives, semantic, (d) => d.range.start.line + 1);
834
+ out.push(...kept);
835
+ for (const directive of unusedExpectErrors) {
836
+ const start = toPosition(directive.line, directive.column);
837
+ out.push({
838
+ range: { start, end: { line: start.line, character: start.character + "--@luaut-expect-error".length } },
839
+ severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
840
+ source: "luaut",
841
+ code: "directive",
842
+ message: import_luaut_parser4.UNUSED_EXPECT_ERROR
812
843
  });
813
844
  }
814
845
  return out;
815
846
  }
816
847
 
817
848
  // src/features/hover.ts
818
- var import_luaut_parser4 = require("luaut-parser");
849
+ var import_luaut_parser5 = require("luaut-parser");
819
850
  function hover(analysis, position) {
820
851
  const path = pathAt(analysis.program, position, true);
821
852
  for (let i = path.length - 1; i >= 0; i--) {
@@ -862,6 +893,27 @@ function describe(analysis, path, index) {
862
893
  const type2 = property?.type ?? types.typeOf.get(field.value);
863
894
  return type2 && `(property) ${name}: ${pretty(type2)}`;
864
895
  }
896
+ // `const { name } = t`: a shorthand key *is* the binding it
897
+ // declares, and has the same span, so the cursor can land on
898
+ // either. A renamed key (`{ name: other }`) names the property
899
+ // the value is read from.
900
+ case "ObjectPatternProperty": {
901
+ if (parent.key !== node || parent.computed) break;
902
+ const value = parent.value;
903
+ if (parent.shorthand) return describe(analysis, [...path.slice(0, index), value], index);
904
+ const binding2 = value.type === "IdentifierPattern" ? bindingOfNode(analysis, value) : void 0;
905
+ const type2 = binding2 && types.bindingType.get(binding2.id);
906
+ return type2 && `(property) ${name}: ${pretty(type2)}`;
907
+ }
908
+ // One line of an overload set reads as its own signature.
909
+ // The line the body is on reads as the whole set, which is
910
+ // what the binding says and what the default path gives.
911
+ case "FunctionSignature": {
912
+ if (parent.name !== node) break;
913
+ const own = types.typeOfTypeNode.get(parent);
914
+ if (own) return `function ${name}${pretty(own)}`;
915
+ break;
916
+ }
865
917
  case "ImportSpecifier": {
866
918
  const alias = types.aliases.get(name);
867
919
  const binding2 = bindingOfNode(analysis, identifier2);
@@ -907,7 +959,7 @@ function describe(analysis, path, index) {
907
959
  case "MappedTypeNode":
908
960
  if (parent.parameterId === node) {
909
961
  const keys = typeOfNode(parent.constraint);
910
- return `(type parameter) ${name}${keys ? ` in ${(0, import_luaut_parser4.formatType)(keys)}` : ""}`;
962
+ return `(type parameter) ${name}${keys ? ` in ${(0, import_luaut_parser5.formatType)(keys)}` : ""}`;
911
963
  }
912
964
  break;
913
965
  }
@@ -924,6 +976,11 @@ function describe(analysis, path, index) {
924
976
  }
925
977
  return void 0;
926
978
  }
979
+ // `...` — what this function's extra arguments are.
980
+ case "VarargExpression": {
981
+ const type2 = types.typeOf.get(node);
982
+ return type2 ? `(vararg) ...: ${pretty(type2)}` : void 0;
983
+ }
927
984
  // Declarations: `const x`, a parameter.
928
985
  case "IdentifierPattern":
929
986
  case "FunctionParameter":
@@ -944,7 +1001,7 @@ function describe(analysis, path, index) {
944
1001
  if (!node.typeArguments.length) {
945
1002
  const qualified = node.namespace ? `${node.namespace}.${base}` : base;
946
1003
  const alias = types.aliases.get(qualified);
947
- if (alias && (0, import_luaut_parser4.isClassType)(alias)) return classText(analysis, qualified);
1004
+ if (alias && (0, import_luaut_parser5.isClassType)(alias)) return classText(analysis, qualified);
948
1005
  if (alias) return `type ${qualified} = ${pretty(alias)}`;
949
1006
  }
950
1007
  const type2 = typeOfNode(node);
@@ -972,17 +1029,17 @@ function declareText(analysis, statement) {
972
1029
  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
1030
  const others = total - 1;
974
1031
  const overloads = others > 0 ? ` (+${others} overload${others > 1 ? "s" : ""})` : "";
975
- return `declare function ${name}${(0, import_luaut_parser4.formatType)(own)}${overloads}`;
1032
+ return `declare function ${name}${(0, import_luaut_parser5.formatType)(own)}${overloads}`;
976
1033
  }
977
1034
  function classText(analysis, name) {
978
1035
  const type = analysis.types.aliases.get(name);
979
- if (!type || !(0, import_luaut_parser4.isClassType)(type)) return void 0;
1036
+ if (!type || !(0, import_luaut_parser5.isClassType)(type)) return void 0;
980
1037
  const superclass = type.class.superclass;
981
1038
  const inherited = superclass ? analysis.types.aliases.get(superclass) : void 0;
982
1039
  const own = [...type.properties].filter(([key, property]) => inherited?.kind !== "object" || inherited.properties.get(key) !== property);
983
1040
  const head = `declare class ${name}${superclass ? ` extends ${superclass}` : ""}`;
984
1041
  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)},`);
1042
+ const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser5.formatType)(property.type)},`);
986
1043
  return `${head} {
987
1044
  ${lines.join("\n")}
988
1045
  }`;
@@ -994,7 +1051,7 @@ function typeParameterSignature(analysis, parameter) {
994
1051
  const p = parameter;
995
1052
  if (p.infer) return `infer ${p.name}`;
996
1053
  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)}` : ""}`;
1054
+ return `${p.isConst ? "const " : ""}${p.name}${constraint ? ` extends ${(0, import_luaut_parser5.formatType)(constraint)}` : ""}`;
998
1055
  }
999
1056
  function typeParameterInScope(path, index, name) {
1000
1057
  for (let i = index - 1; i >= 0; i--) {
@@ -1019,7 +1076,7 @@ function referenceText(analysis, reference) {
1019
1076
  if (!args.length) return name;
1020
1077
  const resolved = args.map((a) => {
1021
1078
  const t = analysis.types.typeOfTypeNode.get(a);
1022
- return t ? (0, import_luaut_parser4.formatType)(t) : "?";
1079
+ return t ? (0, import_luaut_parser5.formatType)(t) : "?";
1023
1080
  });
1024
1081
  return `${name}<${resolved.join(", ")}>`;
1025
1082
  }
@@ -1028,27 +1085,27 @@ function fieldWithKey(table, key) {
1028
1085
  return fields.find((f) => f.type === "TableFieldNamed" && f.key === key);
1029
1086
  }
1030
1087
  function pretty(type) {
1031
- const flat = (0, import_luaut_parser4.formatType)(type);
1088
+ const flat = (0, import_luaut_parser5.formatType)(type);
1032
1089
  if (flat.length <= 80) return flat;
1033
1090
  if (type.kind === "object") {
1034
1091
  const lines = [];
1035
- if (type.indexer) lines.push(` [${(0, import_luaut_parser4.formatType)(type.indexer.key)}]: ${(0, import_luaut_parser4.formatType)(type.indexer.value)},`);
1092
+ if (type.indexer) lines.push(` [${(0, import_luaut_parser5.formatType)(type.indexer.key)}]: ${(0, import_luaut_parser5.formatType)(type.indexer.value)},`);
1036
1093
  for (const [name, property] of type.properties) {
1037
1094
  const readonly = property.readonly ? "readonly " : "";
1038
- lines.push(` ${readonly}${name}${property.optional ? "?" : ""}: ${(0, import_luaut_parser4.formatType)(property.type)},`);
1095
+ lines.push(` ${readonly}${name}${property.optional ? "?" : ""}: ${(0, import_luaut_parser5.formatType)(property.type)},`);
1039
1096
  }
1040
1097
  return `{
1041
1098
  ${lines.join("\n")}
1042
1099
  }`;
1043
1100
  }
1044
1101
  if (type.kind === "intersection" && type.types.every((t) => t.kind === "function")) {
1045
- return type.types.map(import_luaut_parser4.formatType).join("\n& ");
1102
+ return type.types.map(import_luaut_parser5.formatType).join("\n& ");
1046
1103
  }
1047
1104
  return flat;
1048
1105
  }
1049
1106
  function bindingText(binding, type) {
1050
1107
  if (binding.declaredBy === "function" && type.kind === "function") {
1051
- return `function ${binding.name}${(0, import_luaut_parser4.formatType)(type)}`;
1108
+ return `function ${binding.name}${(0, import_luaut_parser5.formatType)(type)}`;
1052
1109
  }
1053
1110
  return `${keyword(binding)} ${binding.name}: ${pretty(type)}`;
1054
1111
  }
@@ -1125,8 +1182,162 @@ function isIdentifier(name) {
1125
1182
  }
1126
1183
 
1127
1184
  // src/features/completion.ts
1185
+ var import_vscode_languageserver5 = require("vscode-languageserver");
1186
+ var import_luaut_parser6 = require("luaut-parser");
1187
+
1188
+ // src/features/autoImport.ts
1189
+ var import_node_fs3 = require("fs");
1190
+ var import_node_path3 = require("path");
1128
1191
  var import_vscode_languageserver4 = require("vscode-languageserver");
1129
- var import_luaut_parser5 = require("luaut-parser");
1192
+ function importItems(analyzer, analysis, typePosition, taken) {
1193
+ const from = pathOfUri(analysis.uri);
1194
+ if (!from) return [];
1195
+ const config = analysis.project.config;
1196
+ const items = [];
1197
+ const offered = /* @__PURE__ */ new Set();
1198
+ for (const file of projectFiles(config?.directory ?? (0, import_node_path3.dirname)(from))) {
1199
+ if (samePath(file, from)) continue;
1200
+ const exports2 = analyzer.exportsAt(file);
1201
+ if (!exports2 || exports2.partial) continue;
1202
+ const names = typePosition ? [...exports2.types.keys()] : [...exports2.values.keys()];
1203
+ const specifier = specifierFor(from, file, config);
1204
+ for (const name of names) {
1205
+ if (taken.has(name) || offered.has(name)) continue;
1206
+ offered.add(name);
1207
+ const type = typePosition ? exports2.types.get(name)?.type : exports2.values.get(name);
1208
+ items.push({
1209
+ label: name,
1210
+ kind: typePosition ? import_vscode_languageserver4.CompletionItemKind.Interface : type && signaturesOf(type).length ? import_vscode_languageserver4.CompletionItemKind.Function : import_vscode_languageserver4.CompletionItemKind.Variable,
1211
+ labelDetails: { description: specifier },
1212
+ detail: `import { ${name} } from "${specifier}"`,
1213
+ sortText: `4${name}`,
1214
+ additionalTextEdits: [importEdit(analyzer, analysis, file, name, specifier, typePosition)]
1215
+ });
1216
+ }
1217
+ }
1218
+ return items;
1219
+ }
1220
+ function serviceItems(analysis, taken) {
1221
+ const services = analysis.types.aliases.get("Services");
1222
+ if (!services || services.kind !== "object") return [];
1223
+ const at = serviceInsertion(analysis.program.body.statements);
1224
+ const items = [];
1225
+ for (const name of services.properties.keys()) {
1226
+ if (taken.has(name)) continue;
1227
+ const line = `const ${name} = game:GetService("${name}")`;
1228
+ items.push({
1229
+ label: name,
1230
+ kind: import_vscode_languageserver4.CompletionItemKind.Module,
1231
+ labelDetails: { description: "service" },
1232
+ detail: line,
1233
+ sortText: `5${name}`,
1234
+ additionalTextEdits: [{ range: { start: at.position, end: at.position }, newText: `${line}
1235
+ ${at.gap}` }]
1236
+ });
1237
+ }
1238
+ return items;
1239
+ }
1240
+ function importEdit(analyzer, analysis, file, name, specifier, typePosition) {
1241
+ const statements = analysis.program.body.statements;
1242
+ const imports = statements.filter((s) => s.type === "ImportStatement");
1243
+ const existing = imports.find((s) => !s.namespaceImport && (typePosition || !s.isTypeOnly) && samePathOrUndefined(analyzer.resolveModulePath(analysis.uri, s.source.value), file));
1244
+ if (existing) {
1245
+ const last = existing.specifiers[existing.specifiers.length - 1];
1246
+ if (last) {
1247
+ const at2 = endOf(last);
1248
+ return { range: { start: at2, end: at2 }, newText: `, ${name}` };
1249
+ }
1250
+ if (existing.defaultImport) {
1251
+ const at2 = endOf(existing.defaultImport);
1252
+ return { range: { start: at2, end: at2 }, newText: `, { ${name} }` };
1253
+ }
1254
+ }
1255
+ const line = `import { ${name} } from "${specifier}"`;
1256
+ const lastImport = imports[imports.length - 1];
1257
+ if (lastImport) {
1258
+ const at2 = { line: lastImport.line.end, character: 0 };
1259
+ return { range: { start: at2, end: at2 }, newText: `${line}
1260
+ ` };
1261
+ }
1262
+ const first = statements[0];
1263
+ const at = { line: first ? first.line.start - 1 : 0, character: 0 };
1264
+ return { range: { start: at, end: at }, newText: first ? `${line}
1265
+
1266
+ ` : `${line}
1267
+ ` };
1268
+ }
1269
+ function serviceInsertion(statements) {
1270
+ let last;
1271
+ for (const statement of statements) {
1272
+ if (statement.type !== "ImportStatement" && !isServiceDeclaration(statement)) break;
1273
+ last = statement;
1274
+ }
1275
+ if (last) return { position: { line: last.line.end, character: 0 }, gap: "" };
1276
+ const first = statements[0];
1277
+ return first ? { position: { line: first.line.start - 1, character: 0 }, gap: "\n" } : { position: { line: 0, character: 0 }, gap: "" };
1278
+ }
1279
+ function isServiceDeclaration(statement) {
1280
+ if (statement.type !== "VariableDeclaration") return false;
1281
+ const init = statement.init[0];
1282
+ return init?.type === "MethodCallExpression" && init.method.name === "GetService" && init.object.type === "Identifier" && init.object.name === "game";
1283
+ }
1284
+ function endOf(node) {
1285
+ return { line: node.line.end - 1, character: node.column.end - 1 };
1286
+ }
1287
+ function samePathOrUndefined(a, b) {
1288
+ return a !== void 0 && samePath(a, b);
1289
+ }
1290
+ function specifierFor(from, target, config) {
1291
+ const withoutExtension = (path) => {
1292
+ const bare = path.replace(/\\/g, "/").replace(/\.luaut$/, "");
1293
+ return bare.endsWith("/index") ? bare.slice(0, -"/index".length) : bare;
1294
+ };
1295
+ let relativePath = withoutExtension((0, import_node_path3.relative)((0, import_node_path3.dirname)(from), target));
1296
+ if (!relativePath.startsWith(".")) relativePath = `./${relativePath}`;
1297
+ if (!relativePath.startsWith("../") || !config) return relativePath;
1298
+ for (const [pattern, targets] of Object.entries(config.paths)) {
1299
+ const star = pattern.indexOf("*");
1300
+ if (star < 0) continue;
1301
+ for (const targetPattern of targets) {
1302
+ const cut = targetPattern.indexOf("*");
1303
+ if (cut < 0) continue;
1304
+ const head = (0, import_node_path3.resolve)(config.baseUrl, targetPattern.slice(0, cut));
1305
+ const rest = (0, import_node_path3.relative)(head, target);
1306
+ if (rest.startsWith("..") || (0, import_node_path3.resolve)(head, rest) !== (0, import_node_path3.resolve)(target)) continue;
1307
+ return `${pattern.slice(0, star)}${withoutExtension(rest)}${pattern.slice(star + 1)}`;
1308
+ }
1309
+ }
1310
+ return relativePath;
1311
+ }
1312
+ var FILE_LIMIT = 2e3;
1313
+ var LISTING_TTL = 3e3;
1314
+ var listings = /* @__PURE__ */ new Map();
1315
+ function projectFiles(root) {
1316
+ const cached = listings.get(root);
1317
+ if (cached && Date.now() - cached.at < LISTING_TTL) return cached.files;
1318
+ const files = [];
1319
+ const walk2 = (directory, depth) => {
1320
+ if (files.length >= FILE_LIMIT || depth > 12) return;
1321
+ let entries;
1322
+ try {
1323
+ entries = (0, import_node_fs3.readdirSync)(directory, { withFileTypes: true });
1324
+ } catch {
1325
+ return;
1326
+ }
1327
+ for (const entry of entries) {
1328
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1329
+ const path = (0, import_node_path3.join)(directory, entry.name);
1330
+ if (entry.isDirectory()) walk2(path, depth + 1);
1331
+ else if (entry.name.endsWith(".luaut") && !entry.name.endsWith(".d.luaut")) files.push(path);
1332
+ if (files.length >= FILE_LIMIT) return;
1333
+ }
1334
+ };
1335
+ walk2(root, 0);
1336
+ listings.set(root, { at: Date.now(), files });
1337
+ return files;
1338
+ }
1339
+
1340
+ // src/features/completion.ts
1130
1341
  var PLACEHOLDER = "__luautCompletion__";
1131
1342
  var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
1132
1343
  function completion(analyzer, document, position) {
@@ -1159,28 +1370,58 @@ function completion(analyzer, document, position) {
1159
1370
  first ??= { analysis, path };
1160
1371
  }
1161
1372
  if (operator || !first) return [];
1373
+ const keys = objectKeyItems(first.analysis, first.path, source.slice(end));
1374
+ if (keys) return keys;
1162
1375
  if (inTypePosition(first.path)) {
1163
1376
  const named = [...first.analysis.types.aliases].map(([name, type]) => ({
1164
1377
  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"
1378
+ kind: (0, import_luaut_parser6.isClassType)(type) ? import_vscode_languageserver5.CompletionItemKind.Class : import_vscode_languageserver5.CompletionItemKind.Interface,
1379
+ detail: (0, import_luaut_parser6.isClassType)(type) ? "class" : "type"
1167
1380
  }));
1168
1381
  const primitives = PRIMITIVES2.map((name) => ({
1169
1382
  label: name,
1170
- kind: import_vscode_languageserver4.CompletionItemKind.Keyword,
1383
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1171
1384
  detail: "type"
1172
1385
  }));
1173
- return [...named, ...primitives];
1174
- }
1175
- return valueItems(first.analysis, at);
1386
+ const keywords = TYPE_KEYWORDS.map((name) => ({
1387
+ label: name,
1388
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1389
+ sortText: `3${name}`
1390
+ }));
1391
+ const typeNames = new Set(first.analysis.types.aliases.keys());
1392
+ const imported = importItems(analyzer, analyzer.get(document), true, typeNames);
1393
+ return [...named, ...primitives, ...keywords, ...imported];
1394
+ }
1395
+ const taken = /* @__PURE__ */ new Set();
1396
+ for (const binding of first.analysis.scopes.bindings.values()) taken.add(binding.name);
1397
+ const current = analyzer.get(document);
1398
+ return [
1399
+ ...valueItems(first.analysis, at),
1400
+ ...contextKeywords(source.slice(0, start)),
1401
+ ...importItems(analyzer, current, false, taken),
1402
+ ...serviceItems(current, taken)
1403
+ ];
1176
1404
  }
1177
1405
  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;
1406
+ let analysis = analyzer.get(document);
1407
+ let literal = stringAt(analysis, position);
1408
+ if (!literal) {
1409
+ const repaired = repairedStrings(document, position);
1410
+ for (const text of repaired) {
1411
+ const candidate = analyzer.analyze(document.uri, -1, text);
1412
+ literal = stringAt(candidate, position);
1413
+ if (literal) {
1414
+ analysis = candidate;
1415
+ break;
1416
+ }
1417
+ }
1418
+ if (!literal) return repaired.length ? [] : void 0;
1419
+ }
1182
1420
  const expected = analysis.types.expectedTypeOf.get(literal);
1183
- const values = stringLiterals(expected, analysis.types.aliases);
1421
+ const values = [.../* @__PURE__ */ new Set([
1422
+ ...stringLiterals(expected, analysis.types.aliases),
1423
+ ...indexKeys(analysis, position, literal)
1424
+ ])];
1184
1425
  if (!values.length) return [];
1185
1426
  const line = literal.line.start - 1;
1186
1427
  const range = literal.line.start === literal.line.end ? {
@@ -1189,10 +1430,81 @@ function stringCompletion(analyzer, document, position) {
1189
1430
  } : void 0;
1190
1431
  return values.map((value) => ({
1191
1432
  label: value,
1192
- kind: import_vscode_languageserver4.CompletionItemKind.Constant,
1433
+ kind: import_vscode_languageserver5.CompletionItemKind.Constant,
1193
1434
  ...range ? { textEdit: { range, newText: value } } : {}
1194
1435
  }));
1195
1436
  }
1437
+ function stringAt(analysis, position) {
1438
+ const path = pathAt(analysis.program, position, false);
1439
+ return [...path].reverse().find((n) => n.type === "StringLiteral" || n.type === "TypeLiteralString");
1440
+ }
1441
+ function objectKeyItems(analysis, path, after) {
1442
+ const index = path.findLastIndex(
1443
+ (n) => n.type === "Identifier" && n.name === PLACEHOLDER
1444
+ );
1445
+ const literal = index > 0 ? path[index - 1] : void 0;
1446
+ if (literal?.type !== "TableExpression") return void 0;
1447
+ const fields = literal.fields;
1448
+ const atKey = fields.some((f) => f.type === "TableFieldShorthand" && f.name?.name === PLACEHOLDER);
1449
+ if (!atKey) return void 0;
1450
+ let expected = analysis.types.expectedTypeOf.get(literal);
1451
+ for (let up = index - 2; expected === void 0 && up >= 0; up--) {
1452
+ const outer = path[up];
1453
+ if (outer.type !== "AsConstExpression" && outer.type !== "ParenthesizedExpression") break;
1454
+ expected = analysis.types.expectedTypeOf.get(outer);
1455
+ }
1456
+ const members = membersOf(expected, analysis.types.aliases);
1457
+ if (!members.length) return void 0;
1458
+ const written = /* @__PURE__ */ new Set();
1459
+ for (const field of fields) {
1460
+ if (field.type === "TableFieldNamed") written.add(field.key?.name ?? field.key?.value ?? "");
1461
+ else if (field.type === "TableFieldShorthand" && field.name?.name !== PLACEHOLDER) written.add(field.name.name);
1462
+ }
1463
+ const colon = /^\s*:/.test(after);
1464
+ return members.filter((member) => !written.has(member.name)).map((member) => ({
1465
+ label: member.name,
1466
+ kind: import_vscode_languageserver5.CompletionItemKind.Field,
1467
+ detail: `${member.property.optional ? "?" : ""}: ${(0, import_luaut_parser6.formatType)(member.property.type)}`,
1468
+ insertText: colon ? member.name : `${member.name}: `
1469
+ }));
1470
+ }
1471
+ function indexKeys(analysis, position, literal) {
1472
+ const path = pathAt(analysis.program, position, false);
1473
+ const at = path.indexOf(literal);
1474
+ const parent = at > 0 ? path[at - 1] : void 0;
1475
+ if (!parent) return [];
1476
+ let indexed;
1477
+ if (parent.type === "IndexedAccessTypeNode" && parent.indexType === literal) {
1478
+ indexed = analysis.types.typeOfTypeNode.get(parent.objectType);
1479
+ } else if (parent.type === "IndexExpression" && parent.index === literal) {
1480
+ indexed = withoutNil(analysis.types.typeOf.get(parent.object));
1481
+ }
1482
+ return membersOf(indexed, analysis.types.aliases).map((member) => member.name);
1483
+ }
1484
+ function repairedStrings(document, position) {
1485
+ const source = document.getText();
1486
+ const offset = document.offsetAt(position);
1487
+ const lineStart = offset - position.character;
1488
+ const lineEndIndex = source.indexOf("\n", offset);
1489
+ const lineEnd = lineEndIndex < 0 ? source.length : lineEndIndex;
1490
+ const before = source.slice(lineStart, offset);
1491
+ let quote;
1492
+ for (let i = 0; i < before.length; i++) {
1493
+ const ch = before[i];
1494
+ if (quote) {
1495
+ if (ch === "\\") i++;
1496
+ else if (ch === quote) quote = void 0;
1497
+ } else if (ch === '"' || ch === "'") {
1498
+ quote = ch;
1499
+ }
1500
+ }
1501
+ if (!quote) return [];
1502
+ let rest = source.slice(offset, lineEnd).replace(/\r$/, "");
1503
+ if (!rest.includes(quote)) rest += quote;
1504
+ const line = before + rest;
1505
+ const endings = ["", " then end", " do end", ")", ") then end", "]"];
1506
+ return endings.map((ending) => source.slice(0, lineStart) + line + ending + source.slice(lineEnd));
1507
+ }
1196
1508
  function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
1197
1509
  if (!type || seen.has(type)) return [];
1198
1510
  seen.add(type);
@@ -1224,7 +1536,7 @@ function memberOperator(source, wordStart) {
1224
1536
  }
1225
1537
  function memberItems(analysis, access) {
1226
1538
  const object = access.object;
1227
- const type = analysis.types.typeOf.get(object);
1539
+ const type = withoutNil(analysis.types.typeOf.get(object));
1228
1540
  const colon = access.type === "MethodCallExpression";
1229
1541
  if (isStringLike(type)) {
1230
1542
  if (!colon) return [];
@@ -1234,6 +1546,11 @@ function memberItems(analysis, access) {
1234
1546
  }
1235
1547
  return membersOf(type, analysis.types.aliases).filter((member) => colon ? member.isMethod : true).map((member) => memberItem(member.name, member.property.type, member.property.readonly));
1236
1548
  }
1549
+ function withoutNil(type) {
1550
+ if (type?.kind !== "union") return type;
1551
+ const kept = type.types.filter((t) => !(t.kind === "primitive" && t.name === "nil"));
1552
+ return kept.length === 1 ? kept[0] : { ...type, types: kept };
1553
+ }
1237
1554
  function isStringLike(type) {
1238
1555
  if (!type) return false;
1239
1556
  switch (type.kind) {
@@ -1262,13 +1579,13 @@ function valueItems(analysis, at) {
1262
1579
  items.push({
1263
1580
  label: binding.name,
1264
1581
  kind: kindOf(type, binding.kind),
1265
- detail: type ? (0, import_luaut_parser5.formatType)(type) : void 0,
1582
+ detail: type ? (0, import_luaut_parser6.formatType)(type) : void 0,
1266
1583
  // Locals before globals, and globals before library names.
1267
1584
  sortText: `${binding.isBuiltin ? 2 : binding.kind === "global" ? 1 : 0}${binding.name}`
1268
1585
  });
1269
1586
  }
1270
1587
  for (const keyword2 of KEYWORDS) {
1271
- items.push({ label: keyword2, kind: import_vscode_languageserver4.CompletionItemKind.Keyword, sortText: `3${keyword2}` });
1588
+ items.push({ label: keyword2, kind: import_vscode_languageserver5.CompletionItemKind.Keyword, sortText: `3${keyword2}` });
1272
1589
  }
1273
1590
  return items;
1274
1591
  }
@@ -1277,22 +1594,22 @@ function memberItem(name, type, readonly) {
1277
1594
  if (signatures.length) {
1278
1595
  return {
1279
1596
  label: name,
1280
- kind: import_vscode_languageserver4.CompletionItemKind.Method,
1597
+ kind: import_vscode_languageserver5.CompletionItemKind.Method,
1281
1598
  detail: signatureLabel(signatures[0]).label,
1282
1599
  insertText: `${name}($0)`,
1283
- insertTextFormat: import_vscode_languageserver4.InsertTextFormat.Snippet
1600
+ insertTextFormat: import_vscode_languageserver5.InsertTextFormat.Snippet
1284
1601
  };
1285
1602
  }
1286
1603
  return {
1287
1604
  label: name,
1288
- kind: import_vscode_languageserver4.CompletionItemKind.Field,
1289
- detail: `${readonly ? "readonly " : ""}${(0, import_luaut_parser5.formatType)(type)}`
1605
+ kind: import_vscode_languageserver5.CompletionItemKind.Field,
1606
+ detail: `${readonly ? "readonly " : ""}${(0, import_luaut_parser6.formatType)(type)}`
1290
1607
  };
1291
1608
  }
1292
1609
  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;
1610
+ if (type && signaturesOf(type).length) return import_vscode_languageserver5.CompletionItemKind.Function;
1611
+ if (bindingKind === "param" || bindingKind === "self") return import_vscode_languageserver5.CompletionItemKind.Variable;
1612
+ return import_vscode_languageserver5.CompletionItemKind.Variable;
1296
1613
  }
1297
1614
  function inTypePosition(path) {
1298
1615
  return path.some(
@@ -1310,6 +1627,17 @@ var PRIMITIVES2 = [
1310
1627
  "thread",
1311
1628
  "buffer"
1312
1629
  ];
1630
+ var TYPE_KEYWORDS = ["keyof", "typeof", "infer", "extends"];
1631
+ function contextKeywords(before) {
1632
+ const keyword2 = (name) => ({
1633
+ label: name,
1634
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1635
+ sortText: `3${name}`
1636
+ });
1637
+ if (/<\s*[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*\s+$/.test(before)) return [keyword2("extends")];
1638
+ if (/[)\]}"'`\w][^\S\n]+$/.test(before)) return [keyword2("as"), keyword2("satisfies")];
1639
+ return [];
1640
+ }
1313
1641
  var KEYWORDS = [
1314
1642
  "const",
1315
1643
  "let",
@@ -1415,8 +1743,8 @@ function activeArgument(call, position) {
1415
1743
  }
1416
1744
 
1417
1745
  // src/features/symbols.ts
1418
- var import_vscode_languageserver5 = require("vscode-languageserver");
1419
- var import_luaut_parser6 = require("luaut-parser");
1746
+ var import_vscode_languageserver6 = require("vscode-languageserver");
1747
+ var import_luaut_parser7 = require("luaut-parser");
1420
1748
  function documentSymbols(analysis) {
1421
1749
  const out = [];
1422
1750
  walk(analysis.program, (node) => {
@@ -1424,7 +1752,7 @@ function documentSymbols(analysis) {
1424
1752
  case "FunctionDeclaration":
1425
1753
  case "FunctionDeclarationStatement": {
1426
1754
  const name = functionName(node);
1427
- if (name) out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Function, node, detailOf(analysis, node)));
1755
+ if (name) out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Function, node, detailOf(analysis, node)));
1428
1756
  break;
1429
1757
  }
1430
1758
  case "TypeAliasStatement":
@@ -1433,20 +1761,20 @@ function documentSymbols(analysis) {
1433
1761
  const name = typeof named === "string" ? named : named?.name;
1434
1762
  if (name) {
1435
1763
  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));
1764
+ out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Interface, node, alias ? (0, import_luaut_parser7.formatType)(alias) : void 0));
1437
1765
  }
1438
1766
  break;
1439
1767
  }
1440
1768
  case "DeclareClassStatement": {
1441
1769
  const name = node.name.name;
1442
1770
  const superclass = node.superclass?.base;
1443
- out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Class, node, superclass && `extends ${superclass}`));
1771
+ out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Class, node, superclass && `extends ${superclass}`));
1444
1772
  break;
1445
1773
  }
1446
1774
  case "VariableDeclaration": {
1447
1775
  for (const target of node.names ?? []) {
1448
1776
  const name = target.name;
1449
- if (name) out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Variable, target));
1777
+ if (name) out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Variable, target));
1450
1778
  }
1451
1779
  break;
1452
1780
  }
@@ -1470,7 +1798,7 @@ function detailOf(analysis, node) {
1470
1798
  if (name && typeof name === "object") {
1471
1799
  const binding = bindingOfNode(analysis, name);
1472
1800
  const type = binding && analysis.types.bindingType.get(binding.id);
1473
- if (type) return (0, import_luaut_parser6.formatType)(type);
1801
+ if (type) return (0, import_luaut_parser7.formatType)(type);
1474
1802
  }
1475
1803
  return void 0;
1476
1804
  }
@@ -1480,7 +1808,7 @@ function symbol(name, kind, node, detail) {
1480
1808
  }
1481
1809
 
1482
1810
  // src/features/semanticTokens.ts
1483
- var import_luaut_parser7 = require("luaut-parser");
1811
+ var import_luaut_parser8 = require("luaut-parser");
1484
1812
  var TOKEN_TYPES = [
1485
1813
  "namespace",
1486
1814
  "type",
@@ -1524,7 +1852,7 @@ function semanticTokens(analysis) {
1524
1852
  };
1525
1853
  let tokens = [];
1526
1854
  try {
1527
- tokens = (0, import_luaut_parser7.tokenize)(analysis.source);
1855
+ tokens = (0, import_luaut_parser8.tokenize)(analysis.source);
1528
1856
  } catch {
1529
1857
  }
1530
1858
  const identifiers = tokens.filter((t) => t.type === "Identifier");
@@ -1568,7 +1896,7 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1568
1896
  if (!baseToken) return;
1569
1897
  if (!namespace && typeParameterInScope2(ancestors, base)) {
1570
1898
  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)) {
1899
+ } else if ((0, import_luaut_parser8.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser8.unknownType)) {
1572
1900
  add(baseToken, base.length, "class");
1573
1901
  } else {
1574
1902
  add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);