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/cli.cjs CHANGED
@@ -13,6 +13,19 @@ var import_luaut_parser2 = require("luaut-parser");
13
13
 
14
14
  // src/features/members.ts
15
15
  var import_luaut_parser = require("luaut-parser");
16
+ function literalKeys(key, aliases) {
17
+ if (!key) return [];
18
+ const resolved = key.kind === "genericRef" ? aliases.get(key.name) : key;
19
+ if (!resolved) return [];
20
+ const parts = resolved.kind === "union" ? resolved.types : [resolved];
21
+ const out = [];
22
+ for (const part of parts) {
23
+ const member = part.kind === "genericRef" ? aliases.get(part.name) ?? part : part;
24
+ if (member.kind !== "literal" || typeof member.value !== "string") return [];
25
+ out.push(member.value);
26
+ }
27
+ return out;
28
+ }
16
29
  function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
17
30
  if (!type || seen.has(type)) return [];
18
31
  seen.add(type);
@@ -22,6 +35,11 @@ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
22
35
  for (const [name, property] of type.properties) {
23
36
  out.push({ name, property, isMethod: takesSelf(property.type) });
24
37
  }
38
+ for (const name of literalKeys(type.indexer?.key, aliases)) {
39
+ if (type.properties.has(name)) continue;
40
+ const property = { type: type.indexer.value, optional: true };
41
+ out.push({ name, property, isMethod: takesSelf(property.type) });
42
+ }
25
43
  return out;
26
44
  }
27
45
  case "intersection": {
@@ -341,11 +359,13 @@ var Analyzer = class {
341
359
  const script = path ? context.sourceMap?.scriptFor(path) : void 0;
342
360
  const libs = script ? [...context.libs, script] : context.libs;
343
361
  const globals = script ? [...context.globals, "script"] : context.globals;
344
- const { program, errors } = (0, import_luaut_parser2.parseWithRecovery)(source);
345
- const scopes = (0, import_luaut_parser2.analyzeScopes)(program, { builtinGlobals: [...globals] });
362
+ const { program, errors, directives } = (0, import_luaut_parser2.parseWithRecovery)(source);
363
+ const reportUndeclared = context.libs.length > 0;
364
+ const scopes = (0, import_luaut_parser2.analyzeScopes)(program, { builtinGlobals: [...globals], reportUndeclared });
346
365
  const dependencies = new Map(context.reads);
347
366
  const types = (0, import_luaut_parser2.analyzeTypes)(program, scopes, {
348
367
  libs,
368
+ reportUnknownTypes: reportUndeclared,
349
369
  resolveModule: (specifier) => {
350
370
  if (!path) return void 0;
351
371
  const candidates = this.candidatesFor(path, specifier);
@@ -359,7 +379,7 @@ var Analyzer = class {
359
379
  return exports2;
360
380
  }
361
381
  });
362
- return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
382
+ return { uri, version, source, program, parseErrors: errors, directives, scopes, types, dependencies, project: context.project };
363
383
  }
364
384
  exportsOf(path, importing) {
365
385
  const key = pathKey(path);
@@ -454,7 +474,7 @@ function collect(container, out) {
454
474
  }
455
475
  }
456
476
  function isSpanlessNode(v) {
457
- return !!v && typeof v === "object" && typeof v.type === "string";
477
+ return !!v && typeof v === "object" && !Array.isArray(v);
458
478
  }
459
479
  function pathAt(root, pos, inclusive = false) {
460
480
  let best;
@@ -714,6 +734,7 @@ function patternNamed(target, name) {
714
734
 
715
735
  // src/features/diagnostics.ts
716
736
  var import_vscode_languageserver2 = require("vscode-languageserver");
737
+ var import_luaut_parser4 = require("luaut-parser");
717
738
  function diagnostics(analysis) {
718
739
  const out = [];
719
740
  for (const error of analysis.parseErrors) {
@@ -727,37 +748,64 @@ function diagnostics(analysis) {
727
748
  message: error.message.replace(/\s*\(\d+:\d+\)$/, "")
728
749
  });
729
750
  }
730
- for (const d of analysis.scopes.diagnostics) {
731
- out.push({
751
+ const semantic = [
752
+ ...analysis.scopes.diagnostics.map((d) => ({
732
753
  range: toRange(d.node),
733
754
  severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
734
755
  source: "luaut",
735
756
  code: d.kind,
736
757
  message: d.message
737
- });
738
- }
739
- for (const d of analysis.types.diagnostics) {
740
- out.push({
758
+ })),
759
+ ...analysis.types.diagnostics.map((d) => ({
741
760
  range: toRange(d.node),
742
761
  severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
743
762
  source: "luaut",
744
763
  code: "type",
745
764
  message: d.message
765
+ }))
766
+ ];
767
+ const { kept, unusedExpectErrors } = (0, import_luaut_parser4.applyDirectives)(analysis.directives, semantic, (d) => d.range.start.line + 1);
768
+ out.push(...kept);
769
+ for (const directive of unusedExpectErrors) {
770
+ const start = toPosition(directive.line, directive.column);
771
+ out.push({
772
+ range: { start, end: { line: start.line, character: start.character + "--@luaut-expect-error".length } },
773
+ severity: import_vscode_languageserver2.DiagnosticSeverity.Error,
774
+ source: "luaut",
775
+ code: "directive",
776
+ message: import_luaut_parser4.UNUSED_EXPECT_ERROR
746
777
  });
747
778
  }
748
779
  return out;
749
780
  }
750
781
 
751
782
  // src/features/hover.ts
752
- var import_luaut_parser4 = require("luaut-parser");
783
+ var import_luaut_parser5 = require("luaut-parser");
753
784
  function hover(analysis, position) {
754
785
  const path = pathAt(analysis.program, position, true);
755
786
  for (let i = path.length - 1; i >= 0; i--) {
787
+ if (UNNAMED.has(path[i].type)) return null;
756
788
  const text = describe(analysis, path, i);
757
789
  if (text) return { contents: { kind: "markdown", value: code(text) }, range: toRange(path[i]) };
758
790
  }
759
791
  return null;
760
792
  }
793
+ var UNNAMED = /* @__PURE__ */ new Set([
794
+ "BinaryExpression",
795
+ "UnaryExpression",
796
+ "CallExpression",
797
+ "MethodCallExpression",
798
+ "MemberExpression",
799
+ "IndexExpression",
800
+ "ParenthesizedExpression",
801
+ "IfElseExpression",
802
+ "TableExpression",
803
+ "ArrayExpression",
804
+ "TypeAssertionExpression",
805
+ "SatisfiesExpression",
806
+ "AsConstExpression",
807
+ "InterpolatedStringExpression"
808
+ ]);
761
809
  var PRIMITIVES = /* @__PURE__ */ new Set(["any", "unknown", "never", "nil", "boolean", "number", "string", "thread", "buffer"]);
762
810
  function describe(analysis, path, index) {
763
811
  const { types } = analysis;
@@ -779,6 +827,27 @@ function describe(analysis, path, index) {
779
827
  const type2 = property?.type ?? types.typeOf.get(field.value);
780
828
  return type2 && `(property) ${name}: ${pretty(type2)}`;
781
829
  }
830
+ // `const { name } = t`: a shorthand key *is* the binding it
831
+ // declares, and has the same span, so the cursor can land on
832
+ // either. A renamed key (`{ name: other }`) names the property
833
+ // the value is read from.
834
+ case "ObjectPatternProperty": {
835
+ if (parent.key !== node || parent.computed) break;
836
+ const value = parent.value;
837
+ if (parent.shorthand) return describe(analysis, [...path.slice(0, index), value], index);
838
+ const binding2 = value.type === "IdentifierPattern" ? bindingOfNode(analysis, value) : void 0;
839
+ const type2 = binding2 && types.bindingType.get(binding2.id);
840
+ return type2 && `(property) ${name}: ${pretty(type2)}`;
841
+ }
842
+ // One line of an overload set reads as its own signature.
843
+ // The line the body is on reads as the whole set, which is
844
+ // what the binding says and what the default path gives.
845
+ case "FunctionSignature": {
846
+ if (parent.name !== node) break;
847
+ const own = types.typeOfTypeNode.get(parent);
848
+ if (own) return `function ${name}${pretty(own)}`;
849
+ break;
850
+ }
782
851
  case "ImportSpecifier": {
783
852
  const alias = types.aliases.get(name);
784
853
  const binding2 = bindingOfNode(analysis, identifier2);
@@ -824,7 +893,7 @@ function describe(analysis, path, index) {
824
893
  case "MappedTypeNode":
825
894
  if (parent.parameterId === node) {
826
895
  const keys = typeOfNode(parent.constraint);
827
- return `(type parameter) ${name}${keys ? ` in ${(0, import_luaut_parser4.formatType)(keys)}` : ""}`;
896
+ return `(type parameter) ${name}${keys ? ` in ${(0, import_luaut_parser5.formatType)(keys)}` : ""}`;
828
897
  }
829
898
  break;
830
899
  }
@@ -833,7 +902,7 @@ function describe(analysis, path, index) {
833
902
  const binding = bindingOfNode(analysis, identifier2);
834
903
  if (binding) {
835
904
  const type2 = types.bindingType.get(binding.id);
836
- if (type2) return `${keyword(binding)} ${binding.name}: ${pretty(type2)}`;
905
+ if (type2) return bindingText(binding, type2);
837
906
  }
838
907
  if (parent?.type === "MemberExpression" || parent?.type === "MethodCallExpression") {
839
908
  const type2 = types.typeOf.get(parent);
@@ -841,13 +910,18 @@ function describe(analysis, path, index) {
841
910
  }
842
911
  return void 0;
843
912
  }
844
- // Declarations: `const x`, a parameter, `const function f`.
913
+ // `...` what this function's extra arguments are.
914
+ case "VarargExpression": {
915
+ const type2 = types.typeOf.get(node);
916
+ return type2 ? `(vararg) ...: ${pretty(type2)}` : void 0;
917
+ }
918
+ // Declarations: `const x`, a parameter.
845
919
  case "IdentifierPattern":
846
920
  case "FunctionParameter":
847
921
  case "TypedIdentifier": {
848
922
  const binding = bindingOfNode(analysis, node);
849
923
  const type2 = binding && types.bindingType.get(binding.id);
850
- return type2 ? `${keyword(binding)} ${binding.name}: ${pretty(type2)}` : void 0;
924
+ return type2 ? bindingText(binding, type2) : void 0;
851
925
  }
852
926
  // A type written by name: `number`, `Shape`, `Partial<User>`, or a type
853
927
  // parameter in scope.
@@ -861,7 +935,7 @@ function describe(analysis, path, index) {
861
935
  if (!node.typeArguments.length) {
862
936
  const qualified = node.namespace ? `${node.namespace}.${base}` : base;
863
937
  const alias = types.aliases.get(qualified);
864
- if (alias && (0, import_luaut_parser4.isClassType)(alias)) return classText(analysis, qualified);
938
+ if (alias && (0, import_luaut_parser5.isClassType)(alias)) return classText(analysis, qualified);
865
939
  if (alias) return `type ${qualified} = ${pretty(alias)}`;
866
940
  }
867
941
  const type2 = typeOfNode(node);
@@ -889,17 +963,17 @@ function declareText(analysis, statement) {
889
963
  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);
890
964
  const others = total - 1;
891
965
  const overloads = others > 0 ? ` (+${others} overload${others > 1 ? "s" : ""})` : "";
892
- return `declare function ${name}${(0, import_luaut_parser4.formatType)(own)}${overloads}`;
966
+ return `declare function ${name}${(0, import_luaut_parser5.formatType)(own)}${overloads}`;
893
967
  }
894
968
  function classText(analysis, name) {
895
969
  const type = analysis.types.aliases.get(name);
896
- if (!type || !(0, import_luaut_parser4.isClassType)(type)) return void 0;
970
+ if (!type || !(0, import_luaut_parser5.isClassType)(type)) return void 0;
897
971
  const superclass = type.class.superclass;
898
972
  const inherited = superclass ? analysis.types.aliases.get(superclass) : void 0;
899
973
  const own = [...type.properties].filter(([key, property]) => inherited?.kind !== "object" || inherited.properties.get(key) !== property);
900
974
  const head = `declare class ${name}${superclass ? ` extends ${superclass}` : ""}`;
901
975
  if (!own.length) return `${head} {}`;
902
- const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser4.formatType)(property.type)},`);
976
+ const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser5.formatType)(property.type)},`);
903
977
  return `${head} {
904
978
  ${lines.join("\n")}
905
979
  }`;
@@ -911,7 +985,7 @@ function typeParameterSignature(analysis, parameter) {
911
985
  const p = parameter;
912
986
  if (p.infer) return `infer ${p.name}`;
913
987
  const constraint = p.constraint ? analysis.types.typeOfTypeNode.get(p.constraint) : void 0;
914
- return `${p.isConst ? "const " : ""}${p.name}${constraint ? ` extends ${(0, import_luaut_parser4.formatType)(constraint)}` : ""}`;
988
+ return `${p.isConst ? "const " : ""}${p.name}${constraint ? ` extends ${(0, import_luaut_parser5.formatType)(constraint)}` : ""}`;
915
989
  }
916
990
  function typeParameterInScope(path, index, name) {
917
991
  for (let i = index - 1; i >= 0; i--) {
@@ -936,7 +1010,7 @@ function referenceText(analysis, reference) {
936
1010
  if (!args.length) return name;
937
1011
  const resolved = args.map((a) => {
938
1012
  const t = analysis.types.typeOfTypeNode.get(a);
939
- return t ? (0, import_luaut_parser4.formatType)(t) : "?";
1013
+ return t ? (0, import_luaut_parser5.formatType)(t) : "?";
940
1014
  });
941
1015
  return `${name}<${resolved.join(", ")}>`;
942
1016
  }
@@ -945,28 +1019,37 @@ function fieldWithKey(table, key) {
945
1019
  return fields.find((f) => f.type === "TableFieldNamed" && f.key === key);
946
1020
  }
947
1021
  function pretty(type) {
948
- const flat = (0, import_luaut_parser4.formatType)(type);
1022
+ const flat = (0, import_luaut_parser5.formatType)(type);
949
1023
  if (flat.length <= 80) return flat;
950
1024
  if (type.kind === "object") {
951
1025
  const lines = [];
952
- if (type.indexer) lines.push(` [${(0, import_luaut_parser4.formatType)(type.indexer.key)}]: ${(0, import_luaut_parser4.formatType)(type.indexer.value)},`);
1026
+ if (type.indexer) lines.push(` [${(0, import_luaut_parser5.formatType)(type.indexer.key)}]: ${(0, import_luaut_parser5.formatType)(type.indexer.value)},`);
953
1027
  for (const [name, property] of type.properties) {
954
1028
  const readonly = property.readonly ? "readonly " : "";
955
- lines.push(` ${readonly}${name}${property.optional ? "?" : ""}: ${(0, import_luaut_parser4.formatType)(property.type)},`);
1029
+ lines.push(` ${readonly}${name}${property.optional ? "?" : ""}: ${(0, import_luaut_parser5.formatType)(property.type)},`);
956
1030
  }
957
1031
  return `{
958
1032
  ${lines.join("\n")}
959
1033
  }`;
960
1034
  }
961
1035
  if (type.kind === "intersection" && type.types.every((t) => t.kind === "function")) {
962
- return type.types.map(import_luaut_parser4.formatType).join("\n& ");
1036
+ return type.types.map(import_luaut_parser5.formatType).join("\n& ");
963
1037
  }
964
1038
  return flat;
965
1039
  }
1040
+ function bindingText(binding, type) {
1041
+ if (binding.declaredBy === "function" && type.kind === "function") {
1042
+ return `function ${binding.name}${(0, import_luaut_parser5.formatType)(type)}`;
1043
+ }
1044
+ return `${keyword(binding)} ${binding.name}: ${pretty(type)}`;
1045
+ }
966
1046
  function keyword(binding) {
967
1047
  if (binding.kind === "param" || binding.kind === "self") return "(parameter)";
968
1048
  if (binding.kind === "global") return "(global)";
969
1049
  if (binding.kind.startsWith("for-")) return "(loop variable)";
1050
+ if (binding.declaredBy === "import" || binding.declaredBy === "namespace") return "(import)";
1051
+ if (binding.declaredBy === "type") return "(type import)";
1052
+ if (binding.declaredBy === "function") return "function";
970
1053
  return binding.isConst ? "const" : "let";
971
1054
  }
972
1055
  function code(text) {
@@ -1033,8 +1116,162 @@ function isIdentifier(name) {
1033
1116
  }
1034
1117
 
1035
1118
  // src/features/completion.ts
1119
+ var import_vscode_languageserver5 = require("vscode-languageserver");
1120
+ var import_luaut_parser6 = require("luaut-parser");
1121
+
1122
+ // src/features/autoImport.ts
1123
+ var import_node_fs3 = require("fs");
1124
+ var import_node_path3 = require("path");
1036
1125
  var import_vscode_languageserver4 = require("vscode-languageserver");
1037
- var import_luaut_parser5 = require("luaut-parser");
1126
+ function importItems(analyzer, analysis, typePosition, taken) {
1127
+ const from = pathOfUri(analysis.uri);
1128
+ if (!from) return [];
1129
+ const config = analysis.project.config;
1130
+ const items = [];
1131
+ const offered = /* @__PURE__ */ new Set();
1132
+ for (const file of projectFiles(config?.directory ?? (0, import_node_path3.dirname)(from))) {
1133
+ if (samePath(file, from)) continue;
1134
+ const exports2 = analyzer.exportsAt(file);
1135
+ if (!exports2 || exports2.partial) continue;
1136
+ const names = typePosition ? [...exports2.types.keys()] : [...exports2.values.keys()];
1137
+ const specifier = specifierFor(from, file, config);
1138
+ for (const name of names) {
1139
+ if (taken.has(name) || offered.has(name)) continue;
1140
+ offered.add(name);
1141
+ const type = typePosition ? exports2.types.get(name)?.type : exports2.values.get(name);
1142
+ items.push({
1143
+ label: name,
1144
+ kind: typePosition ? import_vscode_languageserver4.CompletionItemKind.Interface : type && signaturesOf(type).length ? import_vscode_languageserver4.CompletionItemKind.Function : import_vscode_languageserver4.CompletionItemKind.Variable,
1145
+ labelDetails: { description: specifier },
1146
+ detail: `import { ${name} } from "${specifier}"`,
1147
+ sortText: `4${name}`,
1148
+ additionalTextEdits: [importEdit(analyzer, analysis, file, name, specifier, typePosition)]
1149
+ });
1150
+ }
1151
+ }
1152
+ return items;
1153
+ }
1154
+ function serviceItems(analysis, taken) {
1155
+ const services = analysis.types.aliases.get("Services");
1156
+ if (!services || services.kind !== "object") return [];
1157
+ const at = serviceInsertion(analysis.program.body.statements);
1158
+ const items = [];
1159
+ for (const name of services.properties.keys()) {
1160
+ if (taken.has(name)) continue;
1161
+ const line = `const ${name} = game:GetService("${name}")`;
1162
+ items.push({
1163
+ label: name,
1164
+ kind: import_vscode_languageserver4.CompletionItemKind.Module,
1165
+ labelDetails: { description: "service" },
1166
+ detail: line,
1167
+ sortText: `5${name}`,
1168
+ additionalTextEdits: [{ range: { start: at.position, end: at.position }, newText: `${line}
1169
+ ${at.gap}` }]
1170
+ });
1171
+ }
1172
+ return items;
1173
+ }
1174
+ function importEdit(analyzer, analysis, file, name, specifier, typePosition) {
1175
+ const statements = analysis.program.body.statements;
1176
+ const imports = statements.filter((s) => s.type === "ImportStatement");
1177
+ const existing = imports.find((s) => !s.namespaceImport && (typePosition || !s.isTypeOnly) && samePathOrUndefined(analyzer.resolveModulePath(analysis.uri, s.source.value), file));
1178
+ if (existing) {
1179
+ const last = existing.specifiers[existing.specifiers.length - 1];
1180
+ if (last) {
1181
+ const at2 = endOf(last);
1182
+ return { range: { start: at2, end: at2 }, newText: `, ${name}` };
1183
+ }
1184
+ if (existing.defaultImport) {
1185
+ const at2 = endOf(existing.defaultImport);
1186
+ return { range: { start: at2, end: at2 }, newText: `, { ${name} }` };
1187
+ }
1188
+ }
1189
+ const line = `import { ${name} } from "${specifier}"`;
1190
+ const lastImport = imports[imports.length - 1];
1191
+ if (lastImport) {
1192
+ const at2 = { line: lastImport.line.end, character: 0 };
1193
+ return { range: { start: at2, end: at2 }, newText: `${line}
1194
+ ` };
1195
+ }
1196
+ const first = statements[0];
1197
+ const at = { line: first ? first.line.start - 1 : 0, character: 0 };
1198
+ return { range: { start: at, end: at }, newText: first ? `${line}
1199
+
1200
+ ` : `${line}
1201
+ ` };
1202
+ }
1203
+ function serviceInsertion(statements) {
1204
+ let last;
1205
+ for (const statement of statements) {
1206
+ if (statement.type !== "ImportStatement" && !isServiceDeclaration(statement)) break;
1207
+ last = statement;
1208
+ }
1209
+ if (last) return { position: { line: last.line.end, character: 0 }, gap: "" };
1210
+ const first = statements[0];
1211
+ return first ? { position: { line: first.line.start - 1, character: 0 }, gap: "\n" } : { position: { line: 0, character: 0 }, gap: "" };
1212
+ }
1213
+ function isServiceDeclaration(statement) {
1214
+ if (statement.type !== "VariableDeclaration") return false;
1215
+ const init = statement.init[0];
1216
+ return init?.type === "MethodCallExpression" && init.method.name === "GetService" && init.object.type === "Identifier" && init.object.name === "game";
1217
+ }
1218
+ function endOf(node) {
1219
+ return { line: node.line.end - 1, character: node.column.end - 1 };
1220
+ }
1221
+ function samePathOrUndefined(a, b) {
1222
+ return a !== void 0 && samePath(a, b);
1223
+ }
1224
+ function specifierFor(from, target, config) {
1225
+ const withoutExtension = (path) => {
1226
+ const bare = path.replace(/\\/g, "/").replace(/\.luaut$/, "");
1227
+ return bare.endsWith("/index") ? bare.slice(0, -"/index".length) : bare;
1228
+ };
1229
+ let relativePath = withoutExtension((0, import_node_path3.relative)((0, import_node_path3.dirname)(from), target));
1230
+ if (!relativePath.startsWith(".")) relativePath = `./${relativePath}`;
1231
+ if (!relativePath.startsWith("../") || !config) return relativePath;
1232
+ for (const [pattern, targets] of Object.entries(config.paths)) {
1233
+ const star = pattern.indexOf("*");
1234
+ if (star < 0) continue;
1235
+ for (const targetPattern of targets) {
1236
+ const cut = targetPattern.indexOf("*");
1237
+ if (cut < 0) continue;
1238
+ const head = (0, import_node_path3.resolve)(config.baseUrl, targetPattern.slice(0, cut));
1239
+ const rest = (0, import_node_path3.relative)(head, target);
1240
+ if (rest.startsWith("..") || (0, import_node_path3.resolve)(head, rest) !== (0, import_node_path3.resolve)(target)) continue;
1241
+ return `${pattern.slice(0, star)}${withoutExtension(rest)}${pattern.slice(star + 1)}`;
1242
+ }
1243
+ }
1244
+ return relativePath;
1245
+ }
1246
+ var FILE_LIMIT = 2e3;
1247
+ var LISTING_TTL = 3e3;
1248
+ var listings = /* @__PURE__ */ new Map();
1249
+ function projectFiles(root) {
1250
+ const cached = listings.get(root);
1251
+ if (cached && Date.now() - cached.at < LISTING_TTL) return cached.files;
1252
+ const files = [];
1253
+ const walk2 = (directory, depth) => {
1254
+ if (files.length >= FILE_LIMIT || depth > 12) return;
1255
+ let entries;
1256
+ try {
1257
+ entries = (0, import_node_fs3.readdirSync)(directory, { withFileTypes: true });
1258
+ } catch {
1259
+ return;
1260
+ }
1261
+ for (const entry of entries) {
1262
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1263
+ const path = (0, import_node_path3.join)(directory, entry.name);
1264
+ if (entry.isDirectory()) walk2(path, depth + 1);
1265
+ else if (entry.name.endsWith(".luaut") && !entry.name.endsWith(".d.luaut")) files.push(path);
1266
+ if (files.length >= FILE_LIMIT) return;
1267
+ }
1268
+ };
1269
+ walk2(root, 0);
1270
+ listings.set(root, { at: Date.now(), files });
1271
+ return files;
1272
+ }
1273
+
1274
+ // src/features/completion.ts
1038
1275
  var PLACEHOLDER = "__luautCompletion__";
1039
1276
  var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
1040
1277
  function completion(analyzer, document, position) {
@@ -1067,28 +1304,58 @@ function completion(analyzer, document, position) {
1067
1304
  first ??= { analysis, path };
1068
1305
  }
1069
1306
  if (operator || !first) return [];
1307
+ const keys = objectKeyItems(first.analysis, first.path, source.slice(end));
1308
+ if (keys) return keys;
1070
1309
  if (inTypePosition(first.path)) {
1071
1310
  const named = [...first.analysis.types.aliases].map(([name, type]) => ({
1072
1311
  label: name,
1073
- kind: (0, import_luaut_parser5.isClassType)(type) ? import_vscode_languageserver4.CompletionItemKind.Class : import_vscode_languageserver4.CompletionItemKind.Interface,
1074
- detail: (0, import_luaut_parser5.isClassType)(type) ? "class" : "type"
1312
+ kind: (0, import_luaut_parser6.isClassType)(type) ? import_vscode_languageserver5.CompletionItemKind.Class : import_vscode_languageserver5.CompletionItemKind.Interface,
1313
+ detail: (0, import_luaut_parser6.isClassType)(type) ? "class" : "type"
1075
1314
  }));
1076
1315
  const primitives = PRIMITIVES2.map((name) => ({
1077
1316
  label: name,
1078
- kind: import_vscode_languageserver4.CompletionItemKind.Keyword,
1317
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1079
1318
  detail: "type"
1080
1319
  }));
1081
- return [...named, ...primitives];
1082
- }
1083
- return valueItems(first.analysis, at);
1320
+ const keywords = TYPE_KEYWORDS.map((name) => ({
1321
+ label: name,
1322
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1323
+ sortText: `3${name}`
1324
+ }));
1325
+ const typeNames = new Set(first.analysis.types.aliases.keys());
1326
+ const imported = importItems(analyzer, analyzer.get(document), true, typeNames);
1327
+ return [...named, ...primitives, ...keywords, ...imported];
1328
+ }
1329
+ const taken = /* @__PURE__ */ new Set();
1330
+ for (const binding of first.analysis.scopes.bindings.values()) taken.add(binding.name);
1331
+ const current = analyzer.get(document);
1332
+ return [
1333
+ ...valueItems(first.analysis, at),
1334
+ ...contextKeywords(source.slice(0, start)),
1335
+ ...importItems(analyzer, current, false, taken),
1336
+ ...serviceItems(current, taken)
1337
+ ];
1084
1338
  }
1085
1339
  function stringCompletion(analyzer, document, position) {
1086
- const analysis = analyzer.get(document);
1087
- const path = pathAt(analysis.program, position, false);
1088
- const literal = [...path].reverse().find((n) => n.type === "StringLiteral");
1089
- if (!literal) return void 0;
1340
+ let analysis = analyzer.get(document);
1341
+ let literal = stringAt(analysis, position);
1342
+ if (!literal) {
1343
+ const repaired = repairedStrings(document, position);
1344
+ for (const text of repaired) {
1345
+ const candidate = analyzer.analyze(document.uri, -1, text);
1346
+ literal = stringAt(candidate, position);
1347
+ if (literal) {
1348
+ analysis = candidate;
1349
+ break;
1350
+ }
1351
+ }
1352
+ if (!literal) return repaired.length ? [] : void 0;
1353
+ }
1090
1354
  const expected = analysis.types.expectedTypeOf.get(literal);
1091
- const values = stringLiterals(expected, analysis.types.aliases);
1355
+ const values = [.../* @__PURE__ */ new Set([
1356
+ ...stringLiterals(expected, analysis.types.aliases),
1357
+ ...indexKeys(analysis, position, literal)
1358
+ ])];
1092
1359
  if (!values.length) return [];
1093
1360
  const line = literal.line.start - 1;
1094
1361
  const range = literal.line.start === literal.line.end ? {
@@ -1097,10 +1364,81 @@ function stringCompletion(analyzer, document, position) {
1097
1364
  } : void 0;
1098
1365
  return values.map((value) => ({
1099
1366
  label: value,
1100
- kind: import_vscode_languageserver4.CompletionItemKind.Constant,
1367
+ kind: import_vscode_languageserver5.CompletionItemKind.Constant,
1101
1368
  ...range ? { textEdit: { range, newText: value } } : {}
1102
1369
  }));
1103
1370
  }
1371
+ function stringAt(analysis, position) {
1372
+ const path = pathAt(analysis.program, position, false);
1373
+ return [...path].reverse().find((n) => n.type === "StringLiteral" || n.type === "TypeLiteralString");
1374
+ }
1375
+ function objectKeyItems(analysis, path, after) {
1376
+ const index = path.findLastIndex(
1377
+ (n) => n.type === "Identifier" && n.name === PLACEHOLDER
1378
+ );
1379
+ const literal = index > 0 ? path[index - 1] : void 0;
1380
+ if (literal?.type !== "TableExpression") return void 0;
1381
+ const fields = literal.fields;
1382
+ const atKey = fields.some((f) => f.type === "TableFieldShorthand" && f.name?.name === PLACEHOLDER);
1383
+ if (!atKey) return void 0;
1384
+ let expected = analysis.types.expectedTypeOf.get(literal);
1385
+ for (let up = index - 2; expected === void 0 && up >= 0; up--) {
1386
+ const outer = path[up];
1387
+ if (outer.type !== "AsConstExpression" && outer.type !== "ParenthesizedExpression") break;
1388
+ expected = analysis.types.expectedTypeOf.get(outer);
1389
+ }
1390
+ const members = membersOf(expected, analysis.types.aliases);
1391
+ if (!members.length) return void 0;
1392
+ const written = /* @__PURE__ */ new Set();
1393
+ for (const field of fields) {
1394
+ if (field.type === "TableFieldNamed") written.add(field.key?.name ?? field.key?.value ?? "");
1395
+ else if (field.type === "TableFieldShorthand" && field.name?.name !== PLACEHOLDER) written.add(field.name.name);
1396
+ }
1397
+ const colon = /^\s*:/.test(after);
1398
+ return members.filter((member) => !written.has(member.name)).map((member) => ({
1399
+ label: member.name,
1400
+ kind: import_vscode_languageserver5.CompletionItemKind.Field,
1401
+ detail: `${member.property.optional ? "?" : ""}: ${(0, import_luaut_parser6.formatType)(member.property.type)}`,
1402
+ insertText: colon ? member.name : `${member.name}: `
1403
+ }));
1404
+ }
1405
+ function indexKeys(analysis, position, literal) {
1406
+ const path = pathAt(analysis.program, position, false);
1407
+ const at = path.indexOf(literal);
1408
+ const parent = at > 0 ? path[at - 1] : void 0;
1409
+ if (!parent) return [];
1410
+ let indexed;
1411
+ if (parent.type === "IndexedAccessTypeNode" && parent.indexType === literal) {
1412
+ indexed = analysis.types.typeOfTypeNode.get(parent.objectType);
1413
+ } else if (parent.type === "IndexExpression" && parent.index === literal) {
1414
+ indexed = withoutNil(analysis.types.typeOf.get(parent.object));
1415
+ }
1416
+ return membersOf(indexed, analysis.types.aliases).map((member) => member.name);
1417
+ }
1418
+ function repairedStrings(document, position) {
1419
+ const source = document.getText();
1420
+ const offset = document.offsetAt(position);
1421
+ const lineStart = offset - position.character;
1422
+ const lineEndIndex = source.indexOf("\n", offset);
1423
+ const lineEnd = lineEndIndex < 0 ? source.length : lineEndIndex;
1424
+ const before = source.slice(lineStart, offset);
1425
+ let quote;
1426
+ for (let i = 0; i < before.length; i++) {
1427
+ const ch = before[i];
1428
+ if (quote) {
1429
+ if (ch === "\\") i++;
1430
+ else if (ch === quote) quote = void 0;
1431
+ } else if (ch === '"' || ch === "'") {
1432
+ quote = ch;
1433
+ }
1434
+ }
1435
+ if (!quote) return [];
1436
+ let rest = source.slice(offset, lineEnd).replace(/\r$/, "");
1437
+ if (!rest.includes(quote)) rest += quote;
1438
+ const line = before + rest;
1439
+ const endings = ["", " then end", " do end", ")", ") then end", "]"];
1440
+ return endings.map((ending) => source.slice(0, lineStart) + line + ending + source.slice(lineEnd));
1441
+ }
1104
1442
  function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
1105
1443
  if (!type || seen.has(type)) return [];
1106
1444
  seen.add(type);
@@ -1132,7 +1470,7 @@ function memberOperator(source, wordStart) {
1132
1470
  }
1133
1471
  function memberItems(analysis, access) {
1134
1472
  const object = access.object;
1135
- const type = analysis.types.typeOf.get(object);
1473
+ const type = withoutNil(analysis.types.typeOf.get(object));
1136
1474
  const colon = access.type === "MethodCallExpression";
1137
1475
  if (isStringLike(type)) {
1138
1476
  if (!colon) return [];
@@ -1142,6 +1480,11 @@ function memberItems(analysis, access) {
1142
1480
  }
1143
1481
  return membersOf(type, analysis.types.aliases).filter((member) => colon ? member.isMethod : true).map((member) => memberItem(member.name, member.property.type, member.property.readonly));
1144
1482
  }
1483
+ function withoutNil(type) {
1484
+ if (type?.kind !== "union") return type;
1485
+ const kept = type.types.filter((t) => !(t.kind === "primitive" && t.name === "nil"));
1486
+ return kept.length === 1 ? kept[0] : { ...type, types: kept };
1487
+ }
1145
1488
  function isStringLike(type) {
1146
1489
  if (!type) return false;
1147
1490
  switch (type.kind) {
@@ -1162,6 +1505,7 @@ function valueItems(analysis, at) {
1162
1505
  const seen = /* @__PURE__ */ new Set();
1163
1506
  for (const binding of analysis.scopes.bindings.values()) {
1164
1507
  if (binding.name === PLACEHOLDER || seen.has(binding.name)) continue;
1508
+ if (binding.declaredBy === "type") continue;
1165
1509
  const declaration = binding.declarationNode;
1166
1510
  if (declaration && declaration.line.start - 1 > at.line) continue;
1167
1511
  seen.add(binding.name);
@@ -1169,13 +1513,13 @@ function valueItems(analysis, at) {
1169
1513
  items.push({
1170
1514
  label: binding.name,
1171
1515
  kind: kindOf(type, binding.kind),
1172
- detail: type ? (0, import_luaut_parser5.formatType)(type) : void 0,
1516
+ detail: type ? (0, import_luaut_parser6.formatType)(type) : void 0,
1173
1517
  // Locals before globals, and globals before library names.
1174
1518
  sortText: `${binding.isBuiltin ? 2 : binding.kind === "global" ? 1 : 0}${binding.name}`
1175
1519
  });
1176
1520
  }
1177
1521
  for (const keyword2 of KEYWORDS) {
1178
- items.push({ label: keyword2, kind: import_vscode_languageserver4.CompletionItemKind.Keyword, sortText: `3${keyword2}` });
1522
+ items.push({ label: keyword2, kind: import_vscode_languageserver5.CompletionItemKind.Keyword, sortText: `3${keyword2}` });
1179
1523
  }
1180
1524
  return items;
1181
1525
  }
@@ -1184,22 +1528,22 @@ function memberItem(name, type, readonly) {
1184
1528
  if (signatures.length) {
1185
1529
  return {
1186
1530
  label: name,
1187
- kind: import_vscode_languageserver4.CompletionItemKind.Method,
1531
+ kind: import_vscode_languageserver5.CompletionItemKind.Method,
1188
1532
  detail: signatureLabel(signatures[0]).label,
1189
1533
  insertText: `${name}($0)`,
1190
- insertTextFormat: import_vscode_languageserver4.InsertTextFormat.Snippet
1534
+ insertTextFormat: import_vscode_languageserver5.InsertTextFormat.Snippet
1191
1535
  };
1192
1536
  }
1193
1537
  return {
1194
1538
  label: name,
1195
- kind: import_vscode_languageserver4.CompletionItemKind.Field,
1196
- detail: `${readonly ? "readonly " : ""}${(0, import_luaut_parser5.formatType)(type)}`
1539
+ kind: import_vscode_languageserver5.CompletionItemKind.Field,
1540
+ detail: `${readonly ? "readonly " : ""}${(0, import_luaut_parser6.formatType)(type)}`
1197
1541
  };
1198
1542
  }
1199
1543
  function kindOf(type, bindingKind) {
1200
- if (type && signaturesOf(type).length) return import_vscode_languageserver4.CompletionItemKind.Function;
1201
- if (bindingKind === "param" || bindingKind === "self") return import_vscode_languageserver4.CompletionItemKind.Variable;
1202
- return import_vscode_languageserver4.CompletionItemKind.Variable;
1544
+ if (type && signaturesOf(type).length) return import_vscode_languageserver5.CompletionItemKind.Function;
1545
+ if (bindingKind === "param" || bindingKind === "self") return import_vscode_languageserver5.CompletionItemKind.Variable;
1546
+ return import_vscode_languageserver5.CompletionItemKind.Variable;
1203
1547
  }
1204
1548
  function inTypePosition(path) {
1205
1549
  return path.some(
@@ -1217,6 +1561,17 @@ var PRIMITIVES2 = [
1217
1561
  "thread",
1218
1562
  "buffer"
1219
1563
  ];
1564
+ var TYPE_KEYWORDS = ["keyof", "typeof", "infer", "extends"];
1565
+ function contextKeywords(before) {
1566
+ const keyword2 = (name) => ({
1567
+ label: name,
1568
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1569
+ sortText: `3${name}`
1570
+ });
1571
+ if (/<\s*[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*\s+$/.test(before)) return [keyword2("extends")];
1572
+ if (/[)\]}"'`\w][^\S\n]+$/.test(before)) return [keyword2("as"), keyword2("satisfies")];
1573
+ return [];
1574
+ }
1220
1575
  var KEYWORDS = [
1221
1576
  "const",
1222
1577
  "let",
@@ -1322,8 +1677,8 @@ function activeArgument(call, position) {
1322
1677
  }
1323
1678
 
1324
1679
  // src/features/symbols.ts
1325
- var import_vscode_languageserver5 = require("vscode-languageserver");
1326
- var import_luaut_parser6 = require("luaut-parser");
1680
+ var import_vscode_languageserver6 = require("vscode-languageserver");
1681
+ var import_luaut_parser7 = require("luaut-parser");
1327
1682
  function documentSymbols(analysis) {
1328
1683
  const out = [];
1329
1684
  walk(analysis.program, (node) => {
@@ -1331,7 +1686,7 @@ function documentSymbols(analysis) {
1331
1686
  case "FunctionDeclaration":
1332
1687
  case "FunctionDeclarationStatement": {
1333
1688
  const name = functionName(node);
1334
- if (name) out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Function, node, detailOf(analysis, node)));
1689
+ if (name) out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Function, node, detailOf(analysis, node)));
1335
1690
  break;
1336
1691
  }
1337
1692
  case "TypeAliasStatement":
@@ -1340,20 +1695,20 @@ function documentSymbols(analysis) {
1340
1695
  const name = typeof named === "string" ? named : named?.name;
1341
1696
  if (name) {
1342
1697
  const alias = analysis.types.aliases.get(name);
1343
- out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Interface, node, alias ? (0, import_luaut_parser6.formatType)(alias) : void 0));
1698
+ out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Interface, node, alias ? (0, import_luaut_parser7.formatType)(alias) : void 0));
1344
1699
  }
1345
1700
  break;
1346
1701
  }
1347
1702
  case "DeclareClassStatement": {
1348
1703
  const name = node.name.name;
1349
1704
  const superclass = node.superclass?.base;
1350
- out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Class, node, superclass && `extends ${superclass}`));
1705
+ out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Class, node, superclass && `extends ${superclass}`));
1351
1706
  break;
1352
1707
  }
1353
1708
  case "VariableDeclaration": {
1354
1709
  for (const target of node.names ?? []) {
1355
1710
  const name = target.name;
1356
- if (name) out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Variable, target));
1711
+ if (name) out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Variable, target));
1357
1712
  }
1358
1713
  break;
1359
1714
  }
@@ -1377,7 +1732,7 @@ function detailOf(analysis, node) {
1377
1732
  if (name && typeof name === "object") {
1378
1733
  const binding = bindingOfNode(analysis, name);
1379
1734
  const type = binding && analysis.types.bindingType.get(binding.id);
1380
- if (type) return (0, import_luaut_parser6.formatType)(type);
1735
+ if (type) return (0, import_luaut_parser7.formatType)(type);
1381
1736
  }
1382
1737
  return void 0;
1383
1738
  }
@@ -1387,7 +1742,7 @@ function symbol(name, kind, node, detail) {
1387
1742
  }
1388
1743
 
1389
1744
  // src/features/semanticTokens.ts
1390
- var import_luaut_parser7 = require("luaut-parser");
1745
+ var import_luaut_parser8 = require("luaut-parser");
1391
1746
  var TOKEN_TYPES = [
1392
1747
  "namespace",
1393
1748
  "type",
@@ -1431,7 +1786,7 @@ function semanticTokens(analysis) {
1431
1786
  };
1432
1787
  let tokens = [];
1433
1788
  try {
1434
- tokens = (0, import_luaut_parser7.tokenize)(analysis.source);
1789
+ tokens = (0, import_luaut_parser8.tokenize)(analysis.source);
1435
1790
  } catch {
1436
1791
  }
1437
1792
  const identifiers = tokens.filter((t) => t.type === "Identifier");
@@ -1475,7 +1830,7 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1475
1830
  if (!baseToken) return;
1476
1831
  if (!namespace && typeParameterInScope2(ancestors, base)) {
1477
1832
  add(baseToken, base.length, "typeParameter");
1478
- } else if ((0, import_luaut_parser7.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser7.unknownType)) {
1833
+ } else if ((0, import_luaut_parser8.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser8.unknownType)) {
1479
1834
  add(baseToken, base.length, "class");
1480
1835
  } else {
1481
1836
  add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);
@@ -1530,10 +1885,14 @@ function identifier(analysis, node, parent, add) {
1530
1885
  break;
1531
1886
  case "ImportSpecifier": {
1532
1887
  const binding2 = bindingOfNode(analysis, node);
1888
+ if (binding2?.declaredBy === "type") return as("type", ["declaration"]);
1533
1889
  const value = binding2 && analysis.types.bindingType.get(binding2.id);
1534
1890
  if (analysis.types.aliases.has(name) && (!value || value.kind === "any")) return as("type", ["declaration"]);
1535
1891
  break;
1536
1892
  }
1893
+ case "ImportStatement":
1894
+ if (parent.namespaceImport === node) return as("namespace", ["declaration"]);
1895
+ break;
1537
1896
  case "ExportSpecifier":
1538
1897
  if (!bindingOfNode(analysis, node) && analysis.types.aliases.has(name)) return as("type");
1539
1898
  break;
@@ -1549,6 +1908,7 @@ function identifier(analysis, node, parent, add) {
1549
1908
  function valueKind(analysis, binding) {
1550
1909
  if (!binding) return "variable";
1551
1910
  if (binding.kind === "param" || binding.kind === "self") return "parameter";
1911
+ if (binding.declaredBy === "namespace") return "namespace";
1552
1912
  return isFunction(analysis.types.bindingType.get(binding.id)) ? "function" : "variable";
1553
1913
  }
1554
1914
  function modifiersOf(binding, isDeclaration) {