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/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,29 +748,39 @@ 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--) {
@@ -796,6 +827,27 @@ function describe(analysis, path, index) {
796
827
  const type2 = property?.type ?? types.typeOf.get(field.value);
797
828
  return type2 && `(property) ${name}: ${pretty(type2)}`;
798
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
+ }
799
851
  case "ImportSpecifier": {
800
852
  const alias = types.aliases.get(name);
801
853
  const binding2 = bindingOfNode(analysis, identifier2);
@@ -841,7 +893,7 @@ function describe(analysis, path, index) {
841
893
  case "MappedTypeNode":
842
894
  if (parent.parameterId === node) {
843
895
  const keys = typeOfNode(parent.constraint);
844
- 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)}` : ""}`;
845
897
  }
846
898
  break;
847
899
  }
@@ -858,6 +910,11 @@ function describe(analysis, path, index) {
858
910
  }
859
911
  return void 0;
860
912
  }
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
+ }
861
918
  // Declarations: `const x`, a parameter.
862
919
  case "IdentifierPattern":
863
920
  case "FunctionParameter":
@@ -878,7 +935,7 @@ function describe(analysis, path, index) {
878
935
  if (!node.typeArguments.length) {
879
936
  const qualified = node.namespace ? `${node.namespace}.${base}` : base;
880
937
  const alias = types.aliases.get(qualified);
881
- 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);
882
939
  if (alias) return `type ${qualified} = ${pretty(alias)}`;
883
940
  }
884
941
  const type2 = typeOfNode(node);
@@ -906,17 +963,17 @@ function declareText(analysis, statement) {
906
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);
907
964
  const others = total - 1;
908
965
  const overloads = others > 0 ? ` (+${others} overload${others > 1 ? "s" : ""})` : "";
909
- return `declare function ${name}${(0, import_luaut_parser4.formatType)(own)}${overloads}`;
966
+ return `declare function ${name}${(0, import_luaut_parser5.formatType)(own)}${overloads}`;
910
967
  }
911
968
  function classText(analysis, name) {
912
969
  const type = analysis.types.aliases.get(name);
913
- if (!type || !(0, import_luaut_parser4.isClassType)(type)) return void 0;
970
+ if (!type || !(0, import_luaut_parser5.isClassType)(type)) return void 0;
914
971
  const superclass = type.class.superclass;
915
972
  const inherited = superclass ? analysis.types.aliases.get(superclass) : void 0;
916
973
  const own = [...type.properties].filter(([key, property]) => inherited?.kind !== "object" || inherited.properties.get(key) !== property);
917
974
  const head = `declare class ${name}${superclass ? ` extends ${superclass}` : ""}`;
918
975
  if (!own.length) return `${head} {}`;
919
- const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser4.formatType)(property.type)},`);
976
+ const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${(0, import_luaut_parser5.formatType)(property.type)},`);
920
977
  return `${head} {
921
978
  ${lines.join("\n")}
922
979
  }`;
@@ -928,7 +985,7 @@ function typeParameterSignature(analysis, parameter) {
928
985
  const p = parameter;
929
986
  if (p.infer) return `infer ${p.name}`;
930
987
  const constraint = p.constraint ? analysis.types.typeOfTypeNode.get(p.constraint) : void 0;
931
- return `${p.isConst ? "const " : ""}${p.name}${constraint ? ` extends ${(0, import_luaut_parser4.formatType)(constraint)}` : ""}`;
988
+ return `${p.isConst ? "const " : ""}${p.name}${constraint ? ` extends ${(0, import_luaut_parser5.formatType)(constraint)}` : ""}`;
932
989
  }
933
990
  function typeParameterInScope(path, index, name) {
934
991
  for (let i = index - 1; i >= 0; i--) {
@@ -953,7 +1010,7 @@ function referenceText(analysis, reference) {
953
1010
  if (!args.length) return name;
954
1011
  const resolved = args.map((a) => {
955
1012
  const t = analysis.types.typeOfTypeNode.get(a);
956
- return t ? (0, import_luaut_parser4.formatType)(t) : "?";
1013
+ return t ? (0, import_luaut_parser5.formatType)(t) : "?";
957
1014
  });
958
1015
  return `${name}<${resolved.join(", ")}>`;
959
1016
  }
@@ -962,27 +1019,27 @@ function fieldWithKey(table, key) {
962
1019
  return fields.find((f) => f.type === "TableFieldNamed" && f.key === key);
963
1020
  }
964
1021
  function pretty(type) {
965
- const flat = (0, import_luaut_parser4.formatType)(type);
1022
+ const flat = (0, import_luaut_parser5.formatType)(type);
966
1023
  if (flat.length <= 80) return flat;
967
1024
  if (type.kind === "object") {
968
1025
  const lines = [];
969
- 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)},`);
970
1027
  for (const [name, property] of type.properties) {
971
1028
  const readonly = property.readonly ? "readonly " : "";
972
- 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)},`);
973
1030
  }
974
1031
  return `{
975
1032
  ${lines.join("\n")}
976
1033
  }`;
977
1034
  }
978
1035
  if (type.kind === "intersection" && type.types.every((t) => t.kind === "function")) {
979
- return type.types.map(import_luaut_parser4.formatType).join("\n& ");
1036
+ return type.types.map(import_luaut_parser5.formatType).join("\n& ");
980
1037
  }
981
1038
  return flat;
982
1039
  }
983
1040
  function bindingText(binding, type) {
984
1041
  if (binding.declaredBy === "function" && type.kind === "function") {
985
- return `function ${binding.name}${(0, import_luaut_parser4.formatType)(type)}`;
1042
+ return `function ${binding.name}${(0, import_luaut_parser5.formatType)(type)}`;
986
1043
  }
987
1044
  return `${keyword(binding)} ${binding.name}: ${pretty(type)}`;
988
1045
  }
@@ -1059,8 +1116,162 @@ function isIdentifier(name) {
1059
1116
  }
1060
1117
 
1061
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");
1062
1125
  var import_vscode_languageserver4 = require("vscode-languageserver");
1063
- 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
1064
1275
  var PLACEHOLDER = "__luautCompletion__";
1065
1276
  var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
1066
1277
  function completion(analyzer, document, position) {
@@ -1093,28 +1304,58 @@ function completion(analyzer, document, position) {
1093
1304
  first ??= { analysis, path };
1094
1305
  }
1095
1306
  if (operator || !first) return [];
1307
+ const keys = objectKeyItems(first.analysis, first.path, source.slice(end));
1308
+ if (keys) return keys;
1096
1309
  if (inTypePosition(first.path)) {
1097
1310
  const named = [...first.analysis.types.aliases].map(([name, type]) => ({
1098
1311
  label: name,
1099
- kind: (0, import_luaut_parser5.isClassType)(type) ? import_vscode_languageserver4.CompletionItemKind.Class : import_vscode_languageserver4.CompletionItemKind.Interface,
1100
- detail: (0, import_luaut_parser5.isClassType)(type) ? "class" : "type"
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"
1101
1314
  }));
1102
1315
  const primitives = PRIMITIVES2.map((name) => ({
1103
1316
  label: name,
1104
- kind: import_vscode_languageserver4.CompletionItemKind.Keyword,
1317
+ kind: import_vscode_languageserver5.CompletionItemKind.Keyword,
1105
1318
  detail: "type"
1106
1319
  }));
1107
- return [...named, ...primitives];
1108
- }
1109
- 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
+ ];
1110
1338
  }
1111
1339
  function stringCompletion(analyzer, document, position) {
1112
- const analysis = analyzer.get(document);
1113
- const path = pathAt(analysis.program, position, false);
1114
- const literal = [...path].reverse().find((n) => n.type === "StringLiteral");
1115
- if (!literal) return void 0;
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
+ }
1116
1354
  const expected = analysis.types.expectedTypeOf.get(literal);
1117
- 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
+ ])];
1118
1359
  if (!values.length) return [];
1119
1360
  const line = literal.line.start - 1;
1120
1361
  const range = literal.line.start === literal.line.end ? {
@@ -1123,10 +1364,81 @@ function stringCompletion(analyzer, document, position) {
1123
1364
  } : void 0;
1124
1365
  return values.map((value) => ({
1125
1366
  label: value,
1126
- kind: import_vscode_languageserver4.CompletionItemKind.Constant,
1367
+ kind: import_vscode_languageserver5.CompletionItemKind.Constant,
1127
1368
  ...range ? { textEdit: { range, newText: value } } : {}
1128
1369
  }));
1129
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
+ }
1130
1442
  function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
1131
1443
  if (!type || seen.has(type)) return [];
1132
1444
  seen.add(type);
@@ -1158,7 +1470,7 @@ function memberOperator(source, wordStart) {
1158
1470
  }
1159
1471
  function memberItems(analysis, access) {
1160
1472
  const object = access.object;
1161
- const type = analysis.types.typeOf.get(object);
1473
+ const type = withoutNil(analysis.types.typeOf.get(object));
1162
1474
  const colon = access.type === "MethodCallExpression";
1163
1475
  if (isStringLike(type)) {
1164
1476
  if (!colon) return [];
@@ -1168,6 +1480,11 @@ function memberItems(analysis, access) {
1168
1480
  }
1169
1481
  return membersOf(type, analysis.types.aliases).filter((member) => colon ? member.isMethod : true).map((member) => memberItem(member.name, member.property.type, member.property.readonly));
1170
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
+ }
1171
1488
  function isStringLike(type) {
1172
1489
  if (!type) return false;
1173
1490
  switch (type.kind) {
@@ -1196,13 +1513,13 @@ function valueItems(analysis, at) {
1196
1513
  items.push({
1197
1514
  label: binding.name,
1198
1515
  kind: kindOf(type, binding.kind),
1199
- detail: type ? (0, import_luaut_parser5.formatType)(type) : void 0,
1516
+ detail: type ? (0, import_luaut_parser6.formatType)(type) : void 0,
1200
1517
  // Locals before globals, and globals before library names.
1201
1518
  sortText: `${binding.isBuiltin ? 2 : binding.kind === "global" ? 1 : 0}${binding.name}`
1202
1519
  });
1203
1520
  }
1204
1521
  for (const keyword2 of KEYWORDS) {
1205
- 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}` });
1206
1523
  }
1207
1524
  return items;
1208
1525
  }
@@ -1211,22 +1528,22 @@ function memberItem(name, type, readonly) {
1211
1528
  if (signatures.length) {
1212
1529
  return {
1213
1530
  label: name,
1214
- kind: import_vscode_languageserver4.CompletionItemKind.Method,
1531
+ kind: import_vscode_languageserver5.CompletionItemKind.Method,
1215
1532
  detail: signatureLabel(signatures[0]).label,
1216
1533
  insertText: `${name}($0)`,
1217
- insertTextFormat: import_vscode_languageserver4.InsertTextFormat.Snippet
1534
+ insertTextFormat: import_vscode_languageserver5.InsertTextFormat.Snippet
1218
1535
  };
1219
1536
  }
1220
1537
  return {
1221
1538
  label: name,
1222
- kind: import_vscode_languageserver4.CompletionItemKind.Field,
1223
- 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)}`
1224
1541
  };
1225
1542
  }
1226
1543
  function kindOf(type, bindingKind) {
1227
- if (type && signaturesOf(type).length) return import_vscode_languageserver4.CompletionItemKind.Function;
1228
- if (bindingKind === "param" || bindingKind === "self") return import_vscode_languageserver4.CompletionItemKind.Variable;
1229
- return import_vscode_languageserver4.CompletionItemKind.Variable;
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;
1230
1547
  }
1231
1548
  function inTypePosition(path) {
1232
1549
  return path.some(
@@ -1244,6 +1561,17 @@ var PRIMITIVES2 = [
1244
1561
  "thread",
1245
1562
  "buffer"
1246
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
+ }
1247
1575
  var KEYWORDS = [
1248
1576
  "const",
1249
1577
  "let",
@@ -1349,8 +1677,8 @@ function activeArgument(call, position) {
1349
1677
  }
1350
1678
 
1351
1679
  // src/features/symbols.ts
1352
- var import_vscode_languageserver5 = require("vscode-languageserver");
1353
- var import_luaut_parser6 = require("luaut-parser");
1680
+ var import_vscode_languageserver6 = require("vscode-languageserver");
1681
+ var import_luaut_parser7 = require("luaut-parser");
1354
1682
  function documentSymbols(analysis) {
1355
1683
  const out = [];
1356
1684
  walk(analysis.program, (node) => {
@@ -1358,7 +1686,7 @@ function documentSymbols(analysis) {
1358
1686
  case "FunctionDeclaration":
1359
1687
  case "FunctionDeclarationStatement": {
1360
1688
  const name = functionName(node);
1361
- 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)));
1362
1690
  break;
1363
1691
  }
1364
1692
  case "TypeAliasStatement":
@@ -1367,20 +1695,20 @@ function documentSymbols(analysis) {
1367
1695
  const name = typeof named === "string" ? named : named?.name;
1368
1696
  if (name) {
1369
1697
  const alias = analysis.types.aliases.get(name);
1370
- out.push(symbol(name, import_vscode_languageserver5.SymbolKind.Interface, node, alias ? (0, import_luaut_parser6.formatType)(alias) : void 0));
1698
+ out.push(symbol(name, import_vscode_languageserver6.SymbolKind.Interface, node, alias ? (0, import_luaut_parser7.formatType)(alias) : void 0));
1371
1699
  }
1372
1700
  break;
1373
1701
  }
1374
1702
  case "DeclareClassStatement": {
1375
1703
  const name = node.name.name;
1376
1704
  const superclass = node.superclass?.base;
1377
- 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}`));
1378
1706
  break;
1379
1707
  }
1380
1708
  case "VariableDeclaration": {
1381
1709
  for (const target of node.names ?? []) {
1382
1710
  const name = target.name;
1383
- 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));
1384
1712
  }
1385
1713
  break;
1386
1714
  }
@@ -1404,7 +1732,7 @@ function detailOf(analysis, node) {
1404
1732
  if (name && typeof name === "object") {
1405
1733
  const binding = bindingOfNode(analysis, name);
1406
1734
  const type = binding && analysis.types.bindingType.get(binding.id);
1407
- if (type) return (0, import_luaut_parser6.formatType)(type);
1735
+ if (type) return (0, import_luaut_parser7.formatType)(type);
1408
1736
  }
1409
1737
  return void 0;
1410
1738
  }
@@ -1414,7 +1742,7 @@ function symbol(name, kind, node, detail) {
1414
1742
  }
1415
1743
 
1416
1744
  // src/features/semanticTokens.ts
1417
- var import_luaut_parser7 = require("luaut-parser");
1745
+ var import_luaut_parser8 = require("luaut-parser");
1418
1746
  var TOKEN_TYPES = [
1419
1747
  "namespace",
1420
1748
  "type",
@@ -1458,7 +1786,7 @@ function semanticTokens(analysis) {
1458
1786
  };
1459
1787
  let tokens = [];
1460
1788
  try {
1461
- tokens = (0, import_luaut_parser7.tokenize)(analysis.source);
1789
+ tokens = (0, import_luaut_parser8.tokenize)(analysis.source);
1462
1790
  } catch {
1463
1791
  }
1464
1792
  const identifiers = tokens.filter((t) => t.type === "Identifier");
@@ -1502,7 +1830,7 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1502
1830
  if (!baseToken) return;
1503
1831
  if (!namespace && typeParameterInScope2(ancestors, base)) {
1504
1832
  add(baseToken, base.length, "typeParameter");
1505
- } else if ((0, import_luaut_parser7.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser7.unknownType)) {
1833
+ } else if ((0, import_luaut_parser8.isClassType)(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? import_luaut_parser8.unknownType)) {
1506
1834
  add(baseToken, base.length, "class");
1507
1835
  } else {
1508
1836
  add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);