luaut-language-server 2.1.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,37 +814,64 @@ 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--) {
853
+ if (UNNAMED.has(path[i].type)) return null;
822
854
  const text = describe(analysis, path, i);
823
855
  if (text) return { contents: { kind: "markdown", value: code(text) }, range: toRange(path[i]) };
824
856
  }
825
857
  return null;
826
858
  }
859
+ var UNNAMED = /* @__PURE__ */ new Set([
860
+ "BinaryExpression",
861
+ "UnaryExpression",
862
+ "CallExpression",
863
+ "MethodCallExpression",
864
+ "MemberExpression",
865
+ "IndexExpression",
866
+ "ParenthesizedExpression",
867
+ "IfElseExpression",
868
+ "TableExpression",
869
+ "ArrayExpression",
870
+ "TypeAssertionExpression",
871
+ "SatisfiesExpression",
872
+ "AsConstExpression",
873
+ "InterpolatedStringExpression"
874
+ ]);
827
875
  var PRIMITIVES = /* @__PURE__ */ new Set(["any", "unknown", "never", "nil", "boolean", "number", "string", "thread", "buffer"]);
828
876
  function describe(analysis, path, index) {
829
877
  const { types } = analysis;
@@ -845,6 +893,27 @@ function describe(analysis, path, index) {
845
893
  const type2 = property?.type ?? types.typeOf.get(field.value);
846
894
  return type2 && `(property) ${name}: ${pretty(type2)}`;
847
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
+ }
848
917
  case "ImportSpecifier": {
849
918
  const alias = types.aliases.get(name);
850
919
  const binding2 = bindingOfNode(analysis, identifier2);
@@ -890,7 +959,7 @@ function describe(analysis, path, index) {
890
959
  case "MappedTypeNode":
891
960
  if (parent.parameterId === node) {
892
961
  const keys = typeOfNode(parent.constraint);
893
- 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)}` : ""}`;
894
963
  }
895
964
  break;
896
965
  }
@@ -899,7 +968,7 @@ function describe(analysis, path, index) {
899
968
  const binding = bindingOfNode(analysis, identifier2);
900
969
  if (binding) {
901
970
  const type2 = types.bindingType.get(binding.id);
902
- if (type2) return `${keyword(binding)} ${binding.name}: ${pretty(type2)}`;
971
+ if (type2) return bindingText(binding, type2);
903
972
  }
904
973
  if (parent?.type === "MemberExpression" || parent?.type === "MethodCallExpression") {
905
974
  const type2 = types.typeOf.get(parent);
@@ -907,13 +976,18 @@ function describe(analysis, path, index) {
907
976
  }
908
977
  return void 0;
909
978
  }
910
- // Declarations: `const x`, a parameter, `const function f`.
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
+ }
984
+ // Declarations: `const x`, a parameter.
911
985
  case "IdentifierPattern":
912
986
  case "FunctionParameter":
913
987
  case "TypedIdentifier": {
914
988
  const binding = bindingOfNode(analysis, node);
915
989
  const type2 = binding && types.bindingType.get(binding.id);
916
- return type2 ? `${keyword(binding)} ${binding.name}: ${pretty(type2)}` : void 0;
990
+ return type2 ? bindingText(binding, type2) : void 0;
917
991
  }
918
992
  // A type written by name: `number`, `Shape`, `Partial<User>`, or a type
919
993
  // parameter in scope.
@@ -927,7 +1001,7 @@ function describe(analysis, path, index) {
927
1001
  if (!node.typeArguments.length) {
928
1002
  const qualified = node.namespace ? `${node.namespace}.${base}` : base;
929
1003
  const alias = types.aliases.get(qualified);
930
- 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);
931
1005
  if (alias) return `type ${qualified} = ${pretty(alias)}`;
932
1006
  }
933
1007
  const type2 = typeOfNode(node);
@@ -955,17 +1029,17 @@ function declareText(analysis, statement) {
955
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);
956
1030
  const others = total - 1;
957
1031
  const overloads = others > 0 ? ` (+${others} overload${others > 1 ? "s" : ""})` : "";
958
- return `declare function ${name}${(0, import_luaut_parser4.formatType)(own)}${overloads}`;
1032
+ return `declare function ${name}${(0, import_luaut_parser5.formatType)(own)}${overloads}`;
959
1033
  }
960
1034
  function classText(analysis, name) {
961
1035
  const type = analysis.types.aliases.get(name);
962
- if (!type || !(0, import_luaut_parser4.isClassType)(type)) return void 0;
1036
+ if (!type || !(0, import_luaut_parser5.isClassType)(type)) return void 0;
963
1037
  const superclass = type.class.superclass;
964
1038
  const inherited = superclass ? analysis.types.aliases.get(superclass) : void 0;
965
1039
  const own = [...type.properties].filter(([key, property]) => inherited?.kind !== "object" || inherited.properties.get(key) !== property);
966
1040
  const head = `declare class ${name}${superclass ? ` extends ${superclass}` : ""}`;
967
1041
  if (!own.length) return `${head} {}`;
968
- const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser4.formatType)(property.type)},`);
1042
+ const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser5.formatType)(property.type)},`);
969
1043
  return `${head} {
970
1044
  ${lines.join("\n")}
971
1045
  }`;
@@ -977,7 +1051,7 @@ function typeParameterSignature(analysis, parameter) {
977
1051
  const p = parameter;
978
1052
  if (p.infer) return `infer ${p.name}`;
979
1053
  const constraint = p.constraint ? analysis.types.typeOfTypeNode.get(p.constraint) : void 0;
980
- 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)}` : ""}`;
981
1055
  }
982
1056
  function typeParameterInScope(path, index, name) {
983
1057
  for (let i = index - 1; i >= 0; i--) {
@@ -1002,7 +1076,7 @@ function referenceText(analysis, reference) {
1002
1076
  if (!args.length) return name;
1003
1077
  const resolved = args.map((a) => {
1004
1078
  const t = analysis.types.typeOfTypeNode.get(a);
1005
- return t ? (0, import_luaut_parser4.formatType)(t) : "?";
1079
+ return t ? (0, import_luaut_parser5.formatType)(t) : "?";
1006
1080
  });
1007
1081
  return `${name}<${resolved.join(", ")}>`;
1008
1082
  }
@@ -1011,28 +1085,37 @@ function fieldWithKey(table, key) {
1011
1085
  return fields.find((f) => f.type === "TableFieldNamed" && f.key === key);
1012
1086
  }
1013
1087
  function pretty(type) {
1014
- const flat = (0, import_luaut_parser4.formatType)(type);
1088
+ const flat = (0, import_luaut_parser5.formatType)(type);
1015
1089
  if (flat.length <= 80) return flat;
1016
1090
  if (type.kind === "object") {
1017
1091
  const lines = [];
1018
- 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)},`);
1019
1093
  for (const [name, property] of type.properties) {
1020
1094
  const readonly = property.readonly ? "readonly " : "";
1021
- 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)},`);
1022
1096
  }
1023
1097
  return `{
1024
1098
  ${lines.join("\n")}
1025
1099
  }`;
1026
1100
  }
1027
1101
  if (type.kind === "intersection" && type.types.every((t) => t.kind === "function")) {
1028
- return type.types.map(import_luaut_parser4.formatType).join("\n& ");
1102
+ return type.types.map(import_luaut_parser5.formatType).join("\n& ");
1029
1103
  }
1030
1104
  return flat;
1031
1105
  }
1106
+ function bindingText(binding, type) {
1107
+ if (binding.declaredBy === "function" && type.kind === "function") {
1108
+ return `function ${binding.name}${(0, import_luaut_parser5.formatType)(type)}`;
1109
+ }
1110
+ return `${keyword(binding)} ${binding.name}: ${pretty(type)}`;
1111
+ }
1032
1112
  function keyword(binding) {
1033
1113
  if (binding.kind === "param" || binding.kind === "self") return "(parameter)";
1034
1114
  if (binding.kind === "global") return "(global)";
1035
1115
  if (binding.kind.startsWith("for-")) return "(loop variable)";
1116
+ if (binding.declaredBy === "import" || binding.declaredBy === "namespace") return "(import)";
1117
+ if (binding.declaredBy === "type") return "(type import)";
1118
+ if (binding.declaredBy === "function") return "function";
1036
1119
  return binding.isConst ? "const" : "let";
1037
1120
  }
1038
1121
  function code(text) {
@@ -1099,8 +1182,162 @@ function isIdentifier(name) {
1099
1182
  }
1100
1183
 
1101
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");
1102
1191
  var import_vscode_languageserver4 = require("vscode-languageserver");
1103
- 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
1104
1341
  var PLACEHOLDER = "__luautCompletion__";
1105
1342
  var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
1106
1343
  function completion(analyzer, document, position) {
@@ -1133,28 +1370,58 @@ function completion(analyzer, document, position) {
1133
1370
  first ??= { analysis, path };
1134
1371
  }
1135
1372
  if (operator || !first) return [];
1373
+ const keys = objectKeyItems(first.analysis, first.path, source.slice(end));
1374
+ if (keys) return keys;
1136
1375
  if (inTypePosition(first.path)) {
1137
1376
  const named = [...first.analysis.types.aliases].map(([name, type]) => ({
1138
1377
  label: name,
1139
- kind: (0, import_luaut_parser5.isClassType)(type) ? import_vscode_languageserver4.CompletionItemKind.Class : import_vscode_languageserver4.CompletionItemKind.Interface,
1140
- detail: (0, import_luaut_parser5.isClassType)(type) ? "class" : "type"
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"
1141
1380
  }));
1142
1381
  const primitives = PRIMITIVES2.map((name) => ({
1143
1382
  label: name,
1144
- kind: import_vscode_languageserver4.CompletionItemKind.Keyword,
1383
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1145
1384
  detail: "type"
1146
1385
  }));
1147
- return [...named, ...primitives];
1148
- }
1149
- 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
+ ];
1150
1404
  }
1151
1405
  function stringCompletion(analyzer, document, position) {
1152
- const analysis = analyzer.get(document);
1153
- const path = pathAt(analysis.program, position, false);
1154
- const literal = [...path].reverse().find((n) => n.type === "StringLiteral");
1155
- 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
+ }
1156
1420
  const expected = analysis.types.expectedTypeOf.get(literal);
1157
- 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
+ ])];
1158
1425
  if (!values.length) return [];
1159
1426
  const line = literal.line.start - 1;
1160
1427
  const range = literal.line.start === literal.line.end ? {
@@ -1163,10 +1430,81 @@ function stringCompletion(analyzer, document, position) {
1163
1430
  } : void 0;
1164
1431
  return values.map((value) => ({
1165
1432
  label: value,
1166
- kind: import_vscode_languageserver4.CompletionItemKind.Constant,
1433
+ kind: import_vscode_languageserver5.CompletionItemKind.Constant,
1167
1434
  ...range ? { textEdit: { range, newText: value } } : {}
1168
1435
  }));
1169
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
+ }
1170
1508
  function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
1171
1509
  if (!type || seen.has(type)) return [];
1172
1510
  seen.add(type);
@@ -1198,7 +1536,7 @@ function memberOperator(source, wordStart) {
1198
1536
  }
1199
1537
  function memberItems(analysis, access) {
1200
1538
  const object = access.object;
1201
- const type = analysis.types.typeOf.get(object);
1539
+ const type = withoutNil(analysis.types.typeOf.get(object));
1202
1540
  const colon = access.type === "MethodCallExpression";
1203
1541
  if (isStringLike(type)) {
1204
1542
  if (!colon) return [];
@@ -1208,6 +1546,11 @@ function memberItems(analysis, access) {
1208
1546
  }
1209
1547
  return membersOf(type, analysis.types.aliases).filter((member) => colon ? member.isMethod : true).map((member) => memberItem(member.name, member.property.type, member.property.readonly));
1210
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
+ }
1211
1554
  function isStringLike(type) {
1212
1555
  if (!type) return false;
1213
1556
  switch (type.kind) {
@@ -1228,6 +1571,7 @@ function valueItems(analysis, at) {
1228
1571
  const seen = /* @__PURE__ */ new Set();
1229
1572
  for (const binding of analysis.scopes.bindings.values()) {
1230
1573
  if (binding.name === PLACEHOLDER || seen.has(binding.name)) continue;
1574
+ if (binding.declaredBy === "type") continue;
1231
1575
  const declaration = binding.declarationNode;
1232
1576
  if (declaration && declaration.line.start - 1 > at.line) continue;
1233
1577
  seen.add(binding.name);
@@ -1235,13 +1579,13 @@ function valueItems(analysis, at) {
1235
1579
  items.push({
1236
1580
  label: binding.name,
1237
1581
  kind: kindOf(type, binding.kind),
1238
- detail: type ? (0, import_luaut_parser5.formatType)(type) : void 0,
1582
+ detail: type ? (0, import_luaut_parser6.formatType)(type) : void 0,
1239
1583
  // Locals before globals, and globals before library names.
1240
1584
  sortText: `${binding.isBuiltin ? 2 : binding.kind === "global" ? 1 : 0}${binding.name}`
1241
1585
  });
1242
1586
  }
1243
1587
  for (const keyword2 of KEYWORDS) {
1244
- 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}` });
1245
1589
  }
1246
1590
  return items;
1247
1591
  }
@@ -1250,22 +1594,22 @@ function memberItem(name, type, readonly) {
1250
1594
  if (signatures.length) {
1251
1595
  return {
1252
1596
  label: name,
1253
- kind: import_vscode_languageserver4.CompletionItemKind.Method,
1597
+ kind: import_vscode_languageserver5.CompletionItemKind.Method,
1254
1598
  detail: signatureLabel(signatures[0]).label,
1255
1599
  insertText: `${name}($0)`,
1256
- insertTextFormat: import_vscode_languageserver4.InsertTextFormat.Snippet
1600
+ insertTextFormat: import_vscode_languageserver5.InsertTextFormat.Snippet
1257
1601
  };
1258
1602
  }
1259
1603
  return {
1260
1604
  label: name,
1261
- kind: import_vscode_languageserver4.CompletionItemKind.Field,
1262
- 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)}`
1263
1607
  };
1264
1608
  }
1265
1609
  function kindOf(type, bindingKind) {
1266
- if (type && signaturesOf(type).length) return import_vscode_languageserver4.CompletionItemKind.Function;
1267
- if (bindingKind === "param" || bindingKind === "self") return import_vscode_languageserver4.CompletionItemKind.Variable;
1268
- 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;
1269
1613
  }
1270
1614
  function inTypePosition(path) {
1271
1615
  return path.some(
@@ -1283,6 +1627,17 @@ var PRIMITIVES2 = [
1283
1627
  "thread",
1284
1628
  "buffer"
1285
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
+ }
1286
1641
  var KEYWORDS = [
1287
1642
  "const",
1288
1643
  "let",
@@ -1388,8 +1743,8 @@ function activeArgument(call, position) {
1388
1743
  }
1389
1744
 
1390
1745
  // src/features/symbols.ts
1391
- var import_vscode_languageserver5 = require("vscode-languageserver");
1392
- var import_luaut_parser6 = require("luaut-parser");
1746
+ var import_vscode_languageserver6 = require("vscode-languageserver");
1747
+ var import_luaut_parser7 = require("luaut-parser");
1393
1748
  function documentSymbols(analysis) {
1394
1749
  const out = [];
1395
1750
  walk(analysis.program, (node) => {
@@ -1397,7 +1752,7 @@ function documentSymbols(analysis) {
1397
1752
  case "FunctionDeclaration":
1398
1753
  case "FunctionDeclarationStatement": {
1399
1754
  const name = functionName(node);
1400
- 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)));
1401
1756
  break;
1402
1757
  }
1403
1758
  case "TypeAliasStatement":
@@ -1406,20 +1761,20 @@ function documentSymbols(analysis) {
1406
1761
  const name = typeof named === "string" ? named : named?.name;
1407
1762
  if (name) {
1408
1763
  const alias = analysis.types.aliases.get(name);
1409
- 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));
1410
1765
  }
1411
1766
  break;
1412
1767
  }
1413
1768
  case "DeclareClassStatement": {
1414
1769
  const name = node.name.name;
1415
1770
  const superclass = node.superclass?.base;
1416
- 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}`));
1417
1772
  break;
1418
1773
  }
1419
1774
  case "VariableDeclaration": {
1420
1775
  for (const target of node.names ?? []) {
1421
1776
  const name = target.name;
1422
- 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));
1423
1778
  }
1424
1779
  break;
1425
1780
  }
@@ -1443,7 +1798,7 @@ function detailOf(analysis, node) {
1443
1798
  if (name && typeof name === "object") {
1444
1799
  const binding = bindingOfNode(analysis, name);
1445
1800
  const type = binding && analysis.types.bindingType.get(binding.id);
1446
- if (type) return (0, import_luaut_parser6.formatType)(type);
1801
+ if (type) return (0, import_luaut_parser7.formatType)(type);
1447
1802
  }
1448
1803
  return void 0;
1449
1804
  }
@@ -1453,7 +1808,7 @@ function symbol(name, kind, node, detail) {
1453
1808
  }
1454
1809
 
1455
1810
  // src/features/semanticTokens.ts
1456
- var import_luaut_parser7 = require("luaut-parser");
1811
+ var import_luaut_parser8 = require("luaut-parser");
1457
1812
  var TOKEN_TYPES = [
1458
1813
  "namespace",
1459
1814
  "type",
@@ -1497,7 +1852,7 @@ function semanticTokens(analysis) {
1497
1852
  };
1498
1853
  let tokens = [];
1499
1854
  try {
1500
- tokens = (0, import_luaut_parser7.tokenize)(analysis.source);
1855
+ tokens = (0, import_luaut_parser8.tokenize)(analysis.source);
1501
1856
  } catch {
1502
1857
  }
1503
1858
  const identifiers = tokens.filter((t) => t.type === "Identifier");
@@ -1541,7 +1896,7 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1541
1896
  if (!baseToken) return;
1542
1897
  if (!namespace && typeParameterInScope2(ancestors, base)) {
1543
1898
  add(baseToken, base.length, "typeParameter");
1544
- } else if ((0, import_luaut_parser7.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser7.unknownType)) {
1899
+ } else if ((0, import_luaut_parser8.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser8.unknownType)) {
1545
1900
  add(baseToken, base.length, "class");
1546
1901
  } else {
1547
1902
  add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);
@@ -1596,10 +1951,14 @@ function identifier(analysis, node, parent, add) {
1596
1951
  break;
1597
1952
  case "ImportSpecifier": {
1598
1953
  const binding2 = bindingOfNode(analysis, node);
1954
+ if (binding2?.declaredBy === "type") return as("type", ["declaration"]);
1599
1955
  const value = binding2 && analysis.types.bindingType.get(binding2.id);
1600
1956
  if (analysis.types.aliases.has(name) && (!value || value.kind === "any")) return as("type", ["declaration"]);
1601
1957
  break;
1602
1958
  }
1959
+ case "ImportStatement":
1960
+ if (parent.namespaceImport === node) return as("namespace", ["declaration"]);
1961
+ break;
1603
1962
  case "ExportSpecifier":
1604
1963
  if (!bindingOfNode(analysis, node) && analysis.types.aliases.has(name)) return as("type");
1605
1964
  break;
@@ -1615,6 +1974,7 @@ function identifier(analysis, node, parent, add) {
1615
1974
  function valueKind(analysis, binding) {
1616
1975
  if (!binding) return "variable";
1617
1976
  if (binding.kind === "param" || binding.kind === "self") return "parameter";
1977
+ if (binding.declaredBy === "namespace") return "namespace";
1618
1978
  return isFunction(analysis.types.bindingType.get(binding.id)) ? "function" : "variable";
1619
1979
  }
1620
1980
  function modifiersOf(binding, isDeclaration) {