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/README.md CHANGED
@@ -16,13 +16,13 @@ luaut-language-server --stdio
16
16
 
17
17
  | request | notes |
18
18
  |---|---|
19
- | `publishDiagnostics` | syntax, scope (redeclare, assign-to-`const`) and type errors, on open and on every keystroke |
19
+ | `publishDiagnostics` | syntax, scope (redeclare, assign-to-`const`) and type errors, on open and on every keystroke. A name nothing declares is an error ("Cannot find name 'x'") whenever type libraries are loaded. `--@luaut-nocheck`, `--@luaut-ignore` and `--@luaut-expect-error` silence scope and type errors |
20
20
  | `hover` | the type as luaut writes it — the **narrowed** type at a reference, so a guarded `v` reads `string`, not `string \| nil`. Also every name in a type or definitions file: `declare` names (with their overload count), classes (`declare class Part extends BasePart { ...what it adds }`), alias names, object-type properties, type parameters, `infer` names, and any type annotation, which reads as what it resolves to |
21
21
  | `semanticTokens` | colours from the parser, not from patterns — see [Highlighting](#highlighting) |
22
22
  | `definition` | the binding's declaration — and from an `import`, the export in the other module |
23
23
  | `references`, `documentHighlight` | every use of the binding |
24
24
  | `rename`, `prepareRename` | refuses names that are not identifiers, and builtins from the definitions files |
25
- | `completion` | members after `.` / `:` (never the globals there), names in scope, type names in a type position; inside an `import`, module paths and the exported names |
25
+ | `completion` | members after `.` / `:` (never the globals there), names in scope, type names in a type position; inside an `import`, module paths and the exported names. Inside an object literal written against a type — an annotation, `satisfies`, an argument — the keys that type names, minus the ones already there. A name another file of the project exports is offered too, and picking it adds `import { name } from "./path"` at the top (or joins the import of that file already there). With the Roblox types, each service is offered, and picking one adds `const Players = game:GetService("Players")` under the imports and the services already declared |
26
26
  | `signatureHelp` | every overload, with the active parameter — `:` calls count `self` for you |
27
27
  | `documentSymbol` | functions, type aliases, top-level bindings |
28
28
 
@@ -1,5 +1,18 @@
1
1
  // src/features/members.ts
2
2
  import { formatType } from "luaut-parser";
3
+ function literalKeys(key, aliases) {
4
+ if (!key) return [];
5
+ const resolved = key.kind === "genericRef" ? aliases.get(key.name) : key;
6
+ if (!resolved) return [];
7
+ const parts = resolved.kind === "union" ? resolved.types : [resolved];
8
+ const out = [];
9
+ for (const part of parts) {
10
+ const member = part.kind === "genericRef" ? aliases.get(part.name) ?? part : part;
11
+ if (member.kind !== "literal" || typeof member.value !== "string") return [];
12
+ out.push(member.value);
13
+ }
14
+ return out;
15
+ }
3
16
  function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
4
17
  if (!type || seen.has(type)) return [];
5
18
  seen.add(type);
@@ -9,6 +22,11 @@ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
9
22
  for (const [name, property] of type.properties) {
10
23
  out.push({ name, property, isMethod: takesSelf(property.type) });
11
24
  }
25
+ for (const name of literalKeys(type.indexer?.key, aliases)) {
26
+ if (type.properties.has(name)) continue;
27
+ const property = { type: type.indexer.value, optional: true };
28
+ out.push({ name, property, isMethod: takesSelf(property.type) });
29
+ }
12
30
  return out;
13
31
  }
14
32
  case "intersection": {
@@ -343,11 +361,13 @@ var Analyzer = class {
343
361
  const script = path ? context.sourceMap?.scriptFor(path) : void 0;
344
362
  const libs = script ? [...context.libs, script] : context.libs;
345
363
  const globals = script ? [...context.globals, "script"] : context.globals;
346
- const { program, errors } = parseWithRecovery(source);
347
- const scopes = analyzeScopes(program, { builtinGlobals: [...globals] });
364
+ const { program, errors, directives } = parseWithRecovery(source);
365
+ const reportUndeclared = context.libs.length > 0;
366
+ const scopes = analyzeScopes(program, { builtinGlobals: [...globals], reportUndeclared });
348
367
  const dependencies = new Map(context.reads);
349
368
  const types = analyzeTypes(program, scopes, {
350
369
  libs,
370
+ reportUnknownTypes: reportUndeclared,
351
371
  resolveModule: (specifier) => {
352
372
  if (!path) return void 0;
353
373
  const candidates = this.candidatesFor(path, specifier);
@@ -361,7 +381,7 @@ var Analyzer = class {
361
381
  return exports;
362
382
  }
363
383
  });
364
- return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
384
+ return { uri, version, source, program, parseErrors: errors, directives, scopes, types, dependencies, project: context.project };
365
385
  }
366
386
  exportsOf(path, importing) {
367
387
  const key = pathKey(path);
@@ -450,7 +470,7 @@ function collect(container, out) {
450
470
  }
451
471
  }
452
472
  function isSpanlessNode(v) {
453
- return !!v && typeof v === "object" && typeof v.type === "string";
473
+ return !!v && typeof v === "object" && !Array.isArray(v);
454
474
  }
455
475
  function pathAt(root, pos, inclusive = false) {
456
476
  let best;
@@ -729,6 +749,7 @@ function patternNamed(target, name) {
729
749
 
730
750
  // src/features/diagnostics.ts
731
751
  import { DiagnosticSeverity } from "vscode-languageserver";
752
+ import { applyDirectives, UNUSED_EXPECT_ERROR } from "luaut-parser";
732
753
  function diagnostics(analysis) {
733
754
  const out = [];
734
755
  for (const error of analysis.parseErrors) {
@@ -742,22 +763,32 @@ function diagnostics(analysis) {
742
763
  message: error.message.replace(/\s*\(\d+:\d+\)$/, "")
743
764
  });
744
765
  }
745
- for (const d of analysis.scopes.diagnostics) {
746
- out.push({
766
+ const semantic = [
767
+ ...analysis.scopes.diagnostics.map((d) => ({
747
768
  range: toRange(d.node),
748
769
  severity: DiagnosticSeverity.Error,
749
770
  source: "luaut",
750
771
  code: d.kind,
751
772
  message: d.message
752
- });
753
- }
754
- for (const d of analysis.types.diagnostics) {
755
- out.push({
773
+ })),
774
+ ...analysis.types.diagnostics.map((d) => ({
756
775
  range: toRange(d.node),
757
776
  severity: DiagnosticSeverity.Error,
758
777
  source: "luaut",
759
778
  code: "type",
760
779
  message: d.message
780
+ }))
781
+ ];
782
+ const { kept, unusedExpectErrors } = applyDirectives(analysis.directives, semantic, (d) => d.range.start.line + 1);
783
+ out.push(...kept);
784
+ for (const directive of unusedExpectErrors) {
785
+ const start = toPosition(directive.line, directive.column);
786
+ out.push({
787
+ range: { start, end: { line: start.line, character: start.character + "--@luaut-expect-error".length } },
788
+ severity: DiagnosticSeverity.Error,
789
+ source: "luaut",
790
+ code: "directive",
791
+ message: UNUSED_EXPECT_ERROR
761
792
  });
762
793
  }
763
794
  return out;
@@ -814,6 +845,27 @@ function describe(analysis, path, index) {
814
845
  const type2 = property?.type ?? types.typeOf.get(field.value);
815
846
  return type2 && `(property) ${name}: ${pretty(type2)}`;
816
847
  }
848
+ // `const { name } = t`: a shorthand key *is* the binding it
849
+ // declares, and has the same span, so the cursor can land on
850
+ // either. A renamed key (`{ name: other }`) names the property
851
+ // the value is read from.
852
+ case "ObjectPatternProperty": {
853
+ if (parent.key !== node || parent.computed) break;
854
+ const value = parent.value;
855
+ if (parent.shorthand) return describe(analysis, [...path.slice(0, index), value], index);
856
+ const binding2 = value.type === "IdentifierPattern" ? bindingOfNode(analysis, value) : void 0;
857
+ const type2 = binding2 && types.bindingType.get(binding2.id);
858
+ return type2 && `(property) ${name}: ${pretty(type2)}`;
859
+ }
860
+ // One line of an overload set reads as its own signature.
861
+ // The line the body is on reads as the whole set, which is
862
+ // what the binding says and what the default path gives.
863
+ case "FunctionSignature": {
864
+ if (parent.name !== node) break;
865
+ const own = types.typeOfTypeNode.get(parent);
866
+ if (own) return `function ${name}${pretty(own)}`;
867
+ break;
868
+ }
817
869
  case "ImportSpecifier": {
818
870
  const alias = types.aliases.get(name);
819
871
  const binding2 = bindingOfNode(analysis, identifier2);
@@ -876,6 +928,11 @@ function describe(analysis, path, index) {
876
928
  }
877
929
  return void 0;
878
930
  }
931
+ // `...` — what this function's extra arguments are.
932
+ case "VarargExpression": {
933
+ const type2 = types.typeOf.get(node);
934
+ return type2 ? `(vararg) ...: ${pretty(type2)}` : void 0;
935
+ }
879
936
  // Declarations: `const x`, a parameter.
880
937
  case "IdentifierPattern":
881
938
  case "FunctionParameter":
@@ -1080,10 +1137,164 @@ function isIdentifier(name) {
1080
1137
 
1081
1138
  // src/features/completion.ts
1082
1139
  import {
1083
- CompletionItemKind as CompletionItemKind2,
1140
+ CompletionItemKind as CompletionItemKind3,
1084
1141
  InsertTextFormat
1085
1142
  } from "vscode-languageserver";
1086
1143
  import { formatType as formatType4, isClassType as isClassType2 } from "luaut-parser";
1144
+
1145
+ // src/features/autoImport.ts
1146
+ import { readdirSync as readdirSync2 } from "fs";
1147
+ import { dirname as dirname3, join, relative, resolve as resolve3 } from "path";
1148
+ import { CompletionItemKind as CompletionItemKind2 } from "vscode-languageserver";
1149
+ function importItems(analyzer, analysis, typePosition, taken) {
1150
+ const from = pathOfUri(analysis.uri);
1151
+ if (!from) return [];
1152
+ const config = analysis.project.config;
1153
+ const items = [];
1154
+ const offered = /* @__PURE__ */ new Set();
1155
+ for (const file of projectFiles(config?.directory ?? dirname3(from))) {
1156
+ if (samePath(file, from)) continue;
1157
+ const exports = analyzer.exportsAt(file);
1158
+ if (!exports || exports.partial) continue;
1159
+ const names = typePosition ? [...exports.types.keys()] : [...exports.values.keys()];
1160
+ const specifier = specifierFor(from, file, config);
1161
+ for (const name of names) {
1162
+ if (taken.has(name) || offered.has(name)) continue;
1163
+ offered.add(name);
1164
+ const type = typePosition ? exports.types.get(name)?.type : exports.values.get(name);
1165
+ items.push({
1166
+ label: name,
1167
+ kind: typePosition ? CompletionItemKind2.Interface : type && signaturesOf(type).length ? CompletionItemKind2.Function : CompletionItemKind2.Variable,
1168
+ labelDetails: { description: specifier },
1169
+ detail: `import { ${name} } from "${specifier}"`,
1170
+ sortText: `4${name}`,
1171
+ additionalTextEdits: [importEdit(analyzer, analysis, file, name, specifier, typePosition)]
1172
+ });
1173
+ }
1174
+ }
1175
+ return items;
1176
+ }
1177
+ function serviceItems(analysis, taken) {
1178
+ const services = analysis.types.aliases.get("Services");
1179
+ if (!services || services.kind !== "object") return [];
1180
+ const at = serviceInsertion(analysis.program.body.statements);
1181
+ const items = [];
1182
+ for (const name of services.properties.keys()) {
1183
+ if (taken.has(name)) continue;
1184
+ const line = `const ${name} = game:GetService("${name}")`;
1185
+ items.push({
1186
+ label: name,
1187
+ kind: CompletionItemKind2.Module,
1188
+ labelDetails: { description: "service" },
1189
+ detail: line,
1190
+ sortText: `5${name}`,
1191
+ additionalTextEdits: [{ range: { start: at.position, end: at.position }, newText: `${line}
1192
+ ${at.gap}` }]
1193
+ });
1194
+ }
1195
+ return items;
1196
+ }
1197
+ function importEdit(analyzer, analysis, file, name, specifier, typePosition) {
1198
+ const statements = analysis.program.body.statements;
1199
+ const imports = statements.filter((s) => s.type === "ImportStatement");
1200
+ const existing = imports.find((s) => !s.namespaceImport && (typePosition || !s.isTypeOnly) && samePathOrUndefined(analyzer.resolveModulePath(analysis.uri, s.source.value), file));
1201
+ if (existing) {
1202
+ const last = existing.specifiers[existing.specifiers.length - 1];
1203
+ if (last) {
1204
+ const at2 = endOf(last);
1205
+ return { range: { start: at2, end: at2 }, newText: `, ${name}` };
1206
+ }
1207
+ if (existing.defaultImport) {
1208
+ const at2 = endOf(existing.defaultImport);
1209
+ return { range: { start: at2, end: at2 }, newText: `, { ${name} }` };
1210
+ }
1211
+ }
1212
+ const line = `import { ${name} } from "${specifier}"`;
1213
+ const lastImport = imports[imports.length - 1];
1214
+ if (lastImport) {
1215
+ const at2 = { line: lastImport.line.end, character: 0 };
1216
+ return { range: { start: at2, end: at2 }, newText: `${line}
1217
+ ` };
1218
+ }
1219
+ const first = statements[0];
1220
+ const at = { line: first ? first.line.start - 1 : 0, character: 0 };
1221
+ return { range: { start: at, end: at }, newText: first ? `${line}
1222
+
1223
+ ` : `${line}
1224
+ ` };
1225
+ }
1226
+ function serviceInsertion(statements) {
1227
+ let last;
1228
+ for (const statement of statements) {
1229
+ if (statement.type !== "ImportStatement" && !isServiceDeclaration(statement)) break;
1230
+ last = statement;
1231
+ }
1232
+ if (last) return { position: { line: last.line.end, character: 0 }, gap: "" };
1233
+ const first = statements[0];
1234
+ return first ? { position: { line: first.line.start - 1, character: 0 }, gap: "\n" } : { position: { line: 0, character: 0 }, gap: "" };
1235
+ }
1236
+ function isServiceDeclaration(statement) {
1237
+ if (statement.type !== "VariableDeclaration") return false;
1238
+ const init = statement.init[0];
1239
+ return init?.type === "MethodCallExpression" && init.method.name === "GetService" && init.object.type === "Identifier" && init.object.name === "game";
1240
+ }
1241
+ function endOf(node) {
1242
+ return { line: node.line.end - 1, character: node.column.end - 1 };
1243
+ }
1244
+ function samePathOrUndefined(a, b) {
1245
+ return a !== void 0 && samePath(a, b);
1246
+ }
1247
+ function specifierFor(from, target, config) {
1248
+ const withoutExtension = (path) => {
1249
+ const bare = path.replace(/\\/g, "/").replace(/\.luaut$/, "");
1250
+ return bare.endsWith("/index") ? bare.slice(0, -"/index".length) : bare;
1251
+ };
1252
+ let relativePath = withoutExtension(relative(dirname3(from), target));
1253
+ if (!relativePath.startsWith(".")) relativePath = `./${relativePath}`;
1254
+ if (!relativePath.startsWith("../") || !config) return relativePath;
1255
+ for (const [pattern, targets] of Object.entries(config.paths)) {
1256
+ const star = pattern.indexOf("*");
1257
+ if (star < 0) continue;
1258
+ for (const targetPattern of targets) {
1259
+ const cut = targetPattern.indexOf("*");
1260
+ if (cut < 0) continue;
1261
+ const head = resolve3(config.baseUrl, targetPattern.slice(0, cut));
1262
+ const rest = relative(head, target);
1263
+ if (rest.startsWith("..") || resolve3(head, rest) !== resolve3(target)) continue;
1264
+ return `${pattern.slice(0, star)}${withoutExtension(rest)}${pattern.slice(star + 1)}`;
1265
+ }
1266
+ }
1267
+ return relativePath;
1268
+ }
1269
+ var FILE_LIMIT = 2e3;
1270
+ var LISTING_TTL = 3e3;
1271
+ var listings = /* @__PURE__ */ new Map();
1272
+ function projectFiles(root) {
1273
+ const cached = listings.get(root);
1274
+ if (cached && Date.now() - cached.at < LISTING_TTL) return cached.files;
1275
+ const files = [];
1276
+ const walk2 = (directory, depth) => {
1277
+ if (files.length >= FILE_LIMIT || depth > 12) return;
1278
+ let entries;
1279
+ try {
1280
+ entries = readdirSync2(directory, { withFileTypes: true });
1281
+ } catch {
1282
+ return;
1283
+ }
1284
+ for (const entry of entries) {
1285
+ if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
1286
+ const path = join(directory, entry.name);
1287
+ if (entry.isDirectory()) walk2(path, depth + 1);
1288
+ else if (entry.name.endsWith(".luaut") && !entry.name.endsWith(".d.luaut")) files.push(path);
1289
+ if (files.length >= FILE_LIMIT) return;
1290
+ }
1291
+ };
1292
+ walk2(root, 0);
1293
+ listings.set(root, { at: Date.now(), files });
1294
+ return files;
1295
+ }
1296
+
1297
+ // src/features/completion.ts
1087
1298
  var PLACEHOLDER = "__luautCompletion__";
1088
1299
  var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
1089
1300
  function completion(analyzer, document, position) {
@@ -1116,28 +1327,58 @@ function completion(analyzer, document, position) {
1116
1327
  first ??= { analysis, path };
1117
1328
  }
1118
1329
  if (operator || !first) return [];
1330
+ const keys = objectKeyItems(first.analysis, first.path, source.slice(end));
1331
+ if (keys) return keys;
1119
1332
  if (inTypePosition(first.path)) {
1120
1333
  const named = [...first.analysis.types.aliases].map(([name, type]) => ({
1121
1334
  label: name,
1122
- kind: isClassType2(type) ? CompletionItemKind2.Class : CompletionItemKind2.Interface,
1335
+ kind: isClassType2(type) ? CompletionItemKind3.Class : CompletionItemKind3.Interface,
1123
1336
  detail: isClassType2(type) ? "class" : "type"
1124
1337
  }));
1125
1338
  const primitives = PRIMITIVES2.map((name) => ({
1126
1339
  label: name,
1127
- kind: CompletionItemKind2.Keyword,
1340
+ kind: CompletionItemKind3.Keyword,
1128
1341
  detail: "type"
1129
1342
  }));
1130
- return [...named, ...primitives];
1131
- }
1132
- return valueItems(first.analysis, at);
1343
+ const keywords = TYPE_KEYWORDS.map((name) => ({
1344
+ label: name,
1345
+ kind: CompletionItemKind3.Keyword,
1346
+ sortText: `3${name}`
1347
+ }));
1348
+ const typeNames = new Set(first.analysis.types.aliases.keys());
1349
+ const imported = importItems(analyzer, analyzer.get(document), true, typeNames);
1350
+ return [...named, ...primitives, ...keywords, ...imported];
1351
+ }
1352
+ const taken = /* @__PURE__ */ new Set();
1353
+ for (const binding of first.analysis.scopes.bindings.values()) taken.add(binding.name);
1354
+ const current = analyzer.get(document);
1355
+ return [
1356
+ ...valueItems(first.analysis, at),
1357
+ ...contextKeywords(source.slice(0, start)),
1358
+ ...importItems(analyzer, current, false, taken),
1359
+ ...serviceItems(current, taken)
1360
+ ];
1133
1361
  }
1134
1362
  function stringCompletion(analyzer, document, position) {
1135
- const analysis = analyzer.get(document);
1136
- const path = pathAt(analysis.program, position, false);
1137
- const literal = [...path].reverse().find((n) => n.type === "StringLiteral");
1138
- if (!literal) return void 0;
1363
+ let analysis = analyzer.get(document);
1364
+ let literal = stringAt(analysis, position);
1365
+ if (!literal) {
1366
+ const repaired = repairedStrings(document, position);
1367
+ for (const text of repaired) {
1368
+ const candidate = analyzer.analyze(document.uri, -1, text);
1369
+ literal = stringAt(candidate, position);
1370
+ if (literal) {
1371
+ analysis = candidate;
1372
+ break;
1373
+ }
1374
+ }
1375
+ if (!literal) return repaired.length ? [] : void 0;
1376
+ }
1139
1377
  const expected = analysis.types.expectedTypeOf.get(literal);
1140
- const values = stringLiterals(expected, analysis.types.aliases);
1378
+ const values = [.../* @__PURE__ */ new Set([
1379
+ ...stringLiterals(expected, analysis.types.aliases),
1380
+ ...indexKeys(analysis, position, literal)
1381
+ ])];
1141
1382
  if (!values.length) return [];
1142
1383
  const line = literal.line.start - 1;
1143
1384
  const range = literal.line.start === literal.line.end ? {
@@ -1146,10 +1387,81 @@ function stringCompletion(analyzer, document, position) {
1146
1387
  } : void 0;
1147
1388
  return values.map((value) => ({
1148
1389
  label: value,
1149
- kind: CompletionItemKind2.Constant,
1390
+ kind: CompletionItemKind3.Constant,
1150
1391
  ...range ? { textEdit: { range, newText: value } } : {}
1151
1392
  }));
1152
1393
  }
1394
+ function stringAt(analysis, position) {
1395
+ const path = pathAt(analysis.program, position, false);
1396
+ return [...path].reverse().find((n) => n.type === "StringLiteral" || n.type === "TypeLiteralString");
1397
+ }
1398
+ function objectKeyItems(analysis, path, after) {
1399
+ const index = path.findLastIndex(
1400
+ (n) => n.type === "Identifier" && n.name === PLACEHOLDER
1401
+ );
1402
+ const literal = index > 0 ? path[index - 1] : void 0;
1403
+ if (literal?.type !== "TableExpression") return void 0;
1404
+ const fields = literal.fields;
1405
+ const atKey = fields.some((f) => f.type === "TableFieldShorthand" && f.name?.name === PLACEHOLDER);
1406
+ if (!atKey) return void 0;
1407
+ let expected = analysis.types.expectedTypeOf.get(literal);
1408
+ for (let up = index - 2; expected === void 0 && up >= 0; up--) {
1409
+ const outer = path[up];
1410
+ if (outer.type !== "AsConstExpression" && outer.type !== "ParenthesizedExpression") break;
1411
+ expected = analysis.types.expectedTypeOf.get(outer);
1412
+ }
1413
+ const members = membersOf(expected, analysis.types.aliases);
1414
+ if (!members.length) return void 0;
1415
+ const written = /* @__PURE__ */ new Set();
1416
+ for (const field of fields) {
1417
+ if (field.type === "TableFieldNamed") written.add(field.key?.name ?? field.key?.value ?? "");
1418
+ else if (field.type === "TableFieldShorthand" && field.name?.name !== PLACEHOLDER) written.add(field.name.name);
1419
+ }
1420
+ const colon = /^\s*:/.test(after);
1421
+ return members.filter((member) => !written.has(member.name)).map((member) => ({
1422
+ label: member.name,
1423
+ kind: CompletionItemKind3.Field,
1424
+ detail: `${member.property.optional ? "?" : ""}: ${formatType4(member.property.type)}`,
1425
+ insertText: colon ? member.name : `${member.name}: `
1426
+ }));
1427
+ }
1428
+ function indexKeys(analysis, position, literal) {
1429
+ const path = pathAt(analysis.program, position, false);
1430
+ const at = path.indexOf(literal);
1431
+ const parent = at > 0 ? path[at - 1] : void 0;
1432
+ if (!parent) return [];
1433
+ let indexed;
1434
+ if (parent.type === "IndexedAccessTypeNode" && parent.indexType === literal) {
1435
+ indexed = analysis.types.typeOfTypeNode.get(parent.objectType);
1436
+ } else if (parent.type === "IndexExpression" && parent.index === literal) {
1437
+ indexed = withoutNil(analysis.types.typeOf.get(parent.object));
1438
+ }
1439
+ return membersOf(indexed, analysis.types.aliases).map((member) => member.name);
1440
+ }
1441
+ function repairedStrings(document, position) {
1442
+ const source = document.getText();
1443
+ const offset = document.offsetAt(position);
1444
+ const lineStart = offset - position.character;
1445
+ const lineEndIndex = source.indexOf("\n", offset);
1446
+ const lineEnd = lineEndIndex < 0 ? source.length : lineEndIndex;
1447
+ const before = source.slice(lineStart, offset);
1448
+ let quote;
1449
+ for (let i = 0; i < before.length; i++) {
1450
+ const ch = before[i];
1451
+ if (quote) {
1452
+ if (ch === "\\") i++;
1453
+ else if (ch === quote) quote = void 0;
1454
+ } else if (ch === '"' || ch === "'") {
1455
+ quote = ch;
1456
+ }
1457
+ }
1458
+ if (!quote) return [];
1459
+ let rest = source.slice(offset, lineEnd).replace(/\r$/, "");
1460
+ if (!rest.includes(quote)) rest += quote;
1461
+ const line = before + rest;
1462
+ const endings = ["", " then end", " do end", ")", ") then end", "]"];
1463
+ return endings.map((ending) => source.slice(0, lineStart) + line + ending + source.slice(lineEnd));
1464
+ }
1153
1465
  function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
1154
1466
  if (!type || seen.has(type)) return [];
1155
1467
  seen.add(type);
@@ -1181,7 +1493,7 @@ function memberOperator(source, wordStart) {
1181
1493
  }
1182
1494
  function memberItems(analysis, access) {
1183
1495
  const object = access.object;
1184
- const type = analysis.types.typeOf.get(object);
1496
+ const type = withoutNil(analysis.types.typeOf.get(object));
1185
1497
  const colon = access.type === "MethodCallExpression";
1186
1498
  if (isStringLike(type)) {
1187
1499
  if (!colon) return [];
@@ -1191,6 +1503,11 @@ function memberItems(analysis, access) {
1191
1503
  }
1192
1504
  return membersOf(type, analysis.types.aliases).filter((member) => colon ? member.isMethod : true).map((member) => memberItem(member.name, member.property.type, member.property.readonly));
1193
1505
  }
1506
+ function withoutNil(type) {
1507
+ if (type?.kind !== "union") return type;
1508
+ const kept = type.types.filter((t) => !(t.kind === "primitive" && t.name === "nil"));
1509
+ return kept.length === 1 ? kept[0] : { ...type, types: kept };
1510
+ }
1194
1511
  function isStringLike(type) {
1195
1512
  if (!type) return false;
1196
1513
  switch (type.kind) {
@@ -1225,7 +1542,7 @@ function valueItems(analysis, at) {
1225
1542
  });
1226
1543
  }
1227
1544
  for (const keyword2 of KEYWORDS) {
1228
- items.push({ label: keyword2, kind: CompletionItemKind2.Keyword, sortText: `3${keyword2}` });
1545
+ items.push({ label: keyword2, kind: CompletionItemKind3.Keyword, sortText: `3${keyword2}` });
1229
1546
  }
1230
1547
  return items;
1231
1548
  }
@@ -1234,7 +1551,7 @@ function memberItem(name, type, readonly) {
1234
1551
  if (signatures.length) {
1235
1552
  return {
1236
1553
  label: name,
1237
- kind: CompletionItemKind2.Method,
1554
+ kind: CompletionItemKind3.Method,
1238
1555
  detail: signatureLabel(signatures[0]).label,
1239
1556
  insertText: `${name}($0)`,
1240
1557
  insertTextFormat: InsertTextFormat.Snippet
@@ -1242,14 +1559,14 @@ function memberItem(name, type, readonly) {
1242
1559
  }
1243
1560
  return {
1244
1561
  label: name,
1245
- kind: CompletionItemKind2.Field,
1562
+ kind: CompletionItemKind3.Field,
1246
1563
  detail: `${readonly ? "readonly " : ""}${formatType4(type)}`
1247
1564
  };
1248
1565
  }
1249
1566
  function kindOf(type, bindingKind) {
1250
- if (type && signaturesOf(type).length) return CompletionItemKind2.Function;
1251
- if (bindingKind === "param" || bindingKind === "self") return CompletionItemKind2.Variable;
1252
- return CompletionItemKind2.Variable;
1567
+ if (type && signaturesOf(type).length) return CompletionItemKind3.Function;
1568
+ if (bindingKind === "param" || bindingKind === "self") return CompletionItemKind3.Variable;
1569
+ return CompletionItemKind3.Variable;
1253
1570
  }
1254
1571
  function inTypePosition(path) {
1255
1572
  return path.some(
@@ -1267,6 +1584,17 @@ var PRIMITIVES2 = [
1267
1584
  "thread",
1268
1585
  "buffer"
1269
1586
  ];
1587
+ var TYPE_KEYWORDS = ["keyof", "typeof", "infer", "extends"];
1588
+ function contextKeywords(before) {
1589
+ const keyword2 = (name) => ({
1590
+ label: name,
1591
+ kind: CompletionItemKind3.Keyword,
1592
+ sortText: `3${name}`
1593
+ });
1594
+ if (/<\s*[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*\s+$/.test(before)) return [keyword2("extends")];
1595
+ if (/[)\]}"'`\w][^\S\n]+$/.test(before)) return [keyword2("as"), keyword2("satisfies")];
1596
+ return [];
1597
+ }
1270
1598
  var KEYWORDS = [
1271
1599
  "const",
1272
1600
  "let",
@@ -1874,4 +2202,4 @@ export {
1874
2202
  createServer,
1875
2203
  startServer
1876
2204
  };
1877
- //# sourceMappingURL=chunk-X2GHHRWT.js.map
2205
+ //# sourceMappingURL=chunk-SCZTSX3Y.js.map