luaut-language-server 4.0.0 → 5.0.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
@@ -18,11 +18,12 @@ luaut-language-server --stdio
18
18
  |---|---|
19
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
+ | `luaut/hover` | the same hover, at a level the editor asks for (`depth`), and whether there is another (`canExpand`). Level 0 is the shortest true reading — names left as names — and each one opens the names standing a step further in: `const b: Shape`, then `{ kind: "circle", size: number }`, then whatever those are named after. A class stays its name, and a type that names itself opens once. LSP has no way to ask for this, so every other editor gets level 0 through `hover` |
21
22
  | `semanticTokens` | colours from the parser, not from patterns — see [Highlighting](#highlighting) |
22
23
  | `definition` | the binding's declaration — and from an `import`, the export in the other module |
23
24
  | `references`, `documentHighlight` | every use of the binding |
24
25
  | `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. 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
+ | `completion` | members after `.` / `:` (never the globals there), names in scope — including what hoisting makes visible before its declaration: a function declaration anywhere in its block, and every name of the module inside a function body — 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
27
  | `signatureHelp` | every overload, with the active parameter — `:` calls count `self` for you |
27
28
  | `documentSymbol` | functions, type aliases, top-level bindings |
28
29
 
@@ -91,6 +92,14 @@ name. The grammar in the editor extension keeps just what characters decide
91
92
  alone — comments, strings, numbers, reserved words — so a file looks right
92
93
  before the server answers, and never disagrees with it after.
93
94
 
95
+ ### Saying more
96
+
97
+ A hover opens with the shortest thing that is true and says more when asked,
98
+ as TypeScript's does. The server answers `luaut/hover` at whatever level it is
99
+ given; how the editor offers the next one is the editor's business. The VS
100
+ Code extension puts a link under the type, because the hover API that would
101
+ draw the buttons is still a proposed one.
102
+
94
103
  ## Not yet
95
104
 
96
105
  - **One file at a time.** No workspace indexing, so no cross-file
@@ -68,7 +68,8 @@ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
68
68
  }
69
69
  function takesSelf(type) {
70
70
  for (const signature of signaturesOf(type)) {
71
- if (signature.params[0]?.name === "self") return true;
71
+ const first = signature.params[0]?.name;
72
+ if (first === "self" || first === "this") return true;
72
73
  }
73
74
  return false;
74
75
  }
@@ -89,7 +90,7 @@ function signatureLabel(signature) {
89
90
  });
90
91
  const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
91
92
  const varargs = signature.varargs ? [`...: ${formatType(signature.varargs)}`] : [];
92
- const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${formatType(signature.returns)}`;
93
+ const label = `${generics}(${[...parameters, ...varargs].join(", ")}) => ${formatType(signature.returns)}`;
93
94
  return { label, parameters };
94
95
  }
95
96
 
@@ -697,7 +698,7 @@ function exportDeclaration(analyzer, module, name, seen = /* @__PURE__ */ new Se
697
698
  break;
698
699
  case "ExportStatement": {
699
700
  const declaration = statement.declaration;
700
- if (declaration.type === "FunctionDeclaration") {
701
+ if (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") {
701
702
  if (declaration.name.name === name) return here(declaration.name);
702
703
  } else {
703
704
  for (const target of declaration.names) {
@@ -811,17 +812,101 @@ function diagnostics(analysis) {
811
812
  // src/features/hover.ts
812
813
  import {
813
814
  formatType as formatType3,
814
- isClassType
815
+ isClassType as isClassType2
815
816
  } from "luaut-parser";
816
- function hover(analysis, position) {
817
+
818
+ // src/features/expand.ts
819
+ import { isClassType, arrayOf, tuple, objectType, fn, union as union2, intersection } from "luaut-parser";
820
+ function expandAliases(type, aliases, depth, seen = []) {
821
+ if (depth <= 0) return type;
822
+ const name = withheldName(type);
823
+ if (name !== void 0 && !seen.includes(name)) {
824
+ const opened = aliases.get(name) ?? type;
825
+ const inner = [...seen, name];
826
+ return children2(unnamed(opened), (t) => expandAliases(t, aliases, depth - 1, inner));
827
+ }
828
+ return children2(type, (t) => expandAliases(t, aliases, depth, seen));
829
+ }
830
+ function withheldName(type) {
831
+ if (isClassType(type)) return void 0;
832
+ if (type.kind === "object" || type.kind === "intersection") return type.name;
833
+ if (type.kind === "genericRef") return type.typeArguments.length ? void 0 : type.name;
834
+ return void 0;
835
+ }
836
+ function unnamed(type) {
837
+ if (type.kind === "object") {
838
+ const out = objectType(type.properties, type.indexer, type.frozen);
839
+ return type.class ? Object.assign(out, { class: type.class, name: type.name }) : out;
840
+ }
841
+ if (type.kind === "intersection" && type.name) return { ...type, name: void 0 };
842
+ return type;
843
+ }
844
+ function children2(type, f) {
845
+ switch (type.kind) {
846
+ case "array":
847
+ return arrayOf(f(type.element));
848
+ case "tuple":
849
+ return tuple(type.elements.map(f), type.isPack);
850
+ case "union":
851
+ return union2(type.types.map(f));
852
+ case "intersection": {
853
+ const out = intersection(type.types.map(f));
854
+ return type.name && out.kind === "intersection" ? { ...out, name: type.name } : out;
855
+ }
856
+ case "object": {
857
+ if (isClassType(type)) return type;
858
+ const out = objectType(
859
+ [...type.properties].map(([name, property]) => [name, { ...property, type: f(property.type) }]),
860
+ type.indexer && { key: type.indexer.key, value: f(type.indexer.value) },
861
+ type.frozen
862
+ );
863
+ if (type.name) out.name = type.name;
864
+ return out;
865
+ }
866
+ case "function":
867
+ return fn(
868
+ type.params.map((p) => ({ ...p, type: f(p.type) })),
869
+ f(type.returns),
870
+ type.varargs && f(type.varargs),
871
+ type.typeParams,
872
+ type.predicate
873
+ );
874
+ default:
875
+ return type;
876
+ }
877
+ }
878
+
879
+ // src/features/hover.ts
880
+ function hover(analysis, position, depth = 0) {
817
881
  const path = pathAt(analysis.program, position, true);
818
882
  for (let i = path.length - 1; i >= 0; i--) {
819
883
  if (UNNAMED.has(path[i].type)) return null;
820
- const text = describe(analysis, path, i);
821
- if (text) return { contents: { kind: "markdown", value: code(text) }, range: toRange(path[i]) };
884
+ const text = at(analysis, path, i, depth);
885
+ if (!text) continue;
886
+ const canExpand = at(analysis, path, i, depth + 1) !== text;
887
+ return {
888
+ contents: { kind: "markdown", value: code(text) },
889
+ range: toRange(path[i]),
890
+ depth,
891
+ canExpand
892
+ };
822
893
  }
823
894
  return null;
824
895
  }
896
+ function at(analysis, path, index, depth) {
897
+ const previous = expansion;
898
+ expansion = { depth, aliases: analysis.types.aliases, source: analysis.source };
899
+ try {
900
+ return describe(analysis, path, index);
901
+ } finally {
902
+ expansion = previous;
903
+ }
904
+ }
905
+ var expansion = {
906
+ depth: 0,
907
+ aliases: /* @__PURE__ */ new Map(),
908
+ source: ""
909
+ };
825
910
  var UNNAMED = /* @__PURE__ */ new Set([
826
911
  "BinaryExpression",
827
912
  "UnaryExpression",
@@ -854,8 +939,8 @@ function describe(analysis, path, index) {
854
939
  case "TableExpression": {
855
940
  const field = fieldWithKey(parent, node);
856
941
  if (!field) break;
857
- const objectType = types.typeOf.get(parent);
858
- const property = objectType?.kind === "object" ? objectType.properties.get(name) : void 0;
942
+ const objectType2 = types.typeOf.get(parent);
943
+ const property = objectType2?.kind === "object" ? objectType2.properties.get(name) : void 0;
859
944
  const type2 = property?.type ?? types.typeOf.get(field.value);
860
945
  return type2 && `(property) ${name}: ${pretty(type2)}`;
861
946
  }
@@ -903,6 +988,19 @@ function describe(analysis, path, index) {
903
988
  case "DeclareClassStatement":
904
989
  if (parent.name === node) return classText(analysis, name);
905
990
  break;
991
+ case "ClassDeclaration":
992
+ if (parent.name === node) return classText(analysis, name);
993
+ break;
994
+ case "ClassField":
995
+ if (parent.name === node) {
996
+ const type2 = typeOfNode(parent.typeAnnotation) ?? types.typeOf.get(parent.init);
997
+ const prefix = parent.isStatic ? "(static) " : "(field) ";
998
+ return type2 ? `${prefix}${name}: ${pretty(type2)}` : `${prefix}${name}`;
999
+ }
1000
+ break;
1001
+ case "ClassAccessor":
1002
+ if (parent.name === node) return `(${parent.kind === "get" ? "getter" : "setter"}) ${name}`;
1003
+ break;
906
1004
  case "TableTypeProperty":
907
1005
  if (parent.key === node) {
908
1006
  const type2 = typeOfNode(parent.valueType);
@@ -953,7 +1051,7 @@ function describe(analysis, path, index) {
953
1051
  case "TypedIdentifier": {
954
1052
  const binding = bindingOfNode(analysis, node);
955
1053
  const type2 = binding && types.bindingType.get(binding.id);
956
- return type2 ? bindingText(binding, type2) : void 0;
1054
+ return type2 ? bindingText(binding, type2, asWritten(node.typeAnnotation)) : void 0;
957
1055
  }
958
1056
  // A type written by name: `number`, `Shape`, `Partial<User>`, or a type
959
1057
  // parameter in scope.
@@ -967,7 +1065,7 @@ function describe(analysis, path, index) {
967
1065
  if (!node.typeArguments.length) {
968
1066
  const qualified = node.namespace ? `${node.namespace}.${base}` : base;
969
1067
  const alias = types.aliases.get(qualified);
970
- if (alias && isClassType(alias)) return classText(analysis, qualified);
1068
+ if (alias && isClassType2(alias)) return classText(analysis, qualified);
971
1069
  if (alias) return `type ${qualified} = ${pretty(alias)}`;
972
1070
  }
973
1071
  const type2 = typeOfNode(node);
@@ -999,16 +1097,25 @@ function declareText(analysis, statement) {
999
1097
  }
1000
1098
  function classText(analysis, name) {
1001
1099
  const type = analysis.types.aliases.get(name);
1002
- if (!type || !isClassType(type)) return void 0;
1100
+ if (!type || !isClassType2(type)) return void 0;
1003
1101
  const superclass = type.class.superclass;
1004
1102
  const inherited = superclass ? analysis.types.aliases.get(superclass) : void 0;
1005
- const own = [...type.properties].filter(([key, property]) => inherited?.kind !== "object" || inherited.properties.get(key) !== property);
1006
- const head = `declare class ${name}${superclass ? ` extends ${superclass}` : ""}`;
1007
- if (!own.length) return `${head} {}`;
1008
- const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${formatType3(property.type)},`);
1009
- return `${head} {
1103
+ const own = [...type.properties].filter(([key, property]) => (
1104
+ // `ClassObject` is on every instance and says nothing about this one.
1105
+ key !== "ClassObject" && (inherited?.kind !== "object" || inherited.properties.get(key) !== property)
1106
+ ));
1107
+ const written = isRuntimeClass(analysis, name);
1108
+ const head = `${written ? "" : "declare "}class ${name}${superclass ? ` extends ${superclass}` : ""}`;
1109
+ const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${formatType3(property.type)}${written ? "" : ","}`);
1110
+ return own.length ? `${head} {
1010
1111
  ${lines.join("\n")}
1011
- }`;
1112
+ }` : `${head} {}`;
1113
+ }
1114
+ function isRuntimeClass(analysis, name) {
1115
+ return analysis.program.body.statements.some((statement) => {
1116
+ const declaration = statement.type === "ExportStatement" ? statement.declaration : statement;
1117
+ return declaration.type === "ClassDeclaration" && declaration.name.name === name;
1118
+ });
1012
1119
  }
1013
1120
  function typeParameterText(analysis, parameter) {
1014
1121
  return `(type parameter) ${typeParameterSignature(analysis, parameter)}`;
@@ -1050,7 +1157,27 @@ function fieldWithKey(table, key) {
1050
1157
  const fields = table.fields;
1051
1158
  return fields.find((f) => f.type === "TableFieldNamed" && f.key === key);
1052
1159
  }
1053
- function pretty(type) {
1160
+ function pretty(type, written) {
1161
+ const named = render(expandAliases(type, expansion.aliases, 0));
1162
+ const shorthand = written !== void 0 && written !== named ? written : void 0;
1163
+ const depth = shorthand === void 0 ? expansion.depth : expansion.depth - 1;
1164
+ if (depth < 0) return shorthand;
1165
+ return depth === 0 ? named : render(expandAliases(type, expansion.aliases, depth));
1166
+ }
1167
+ function asWritten(node) {
1168
+ if (!isSpanned(node) || !expansion.source) return void 0;
1169
+ const lines = expansion.source.split(/\r?\n/);
1170
+ const { line, column } = node;
1171
+ if (line.start < 1 || line.end > lines.length) return void 0;
1172
+ const text = line.start === line.end ? lines[line.start - 1].slice(column.start - 1, column.end - 1) : [
1173
+ lines[line.start - 1].slice(column.start - 1),
1174
+ ...lines.slice(line.start, line.end - 1),
1175
+ lines[line.end - 1].slice(0, column.end - 1)
1176
+ ].join(" ");
1177
+ const folded = text.trim().replace(/\s+/g, " ");
1178
+ return folded.length ? folded : void 0;
1179
+ }
1180
+ function render(type) {
1054
1181
  const flat = formatType3(type);
1055
1182
  if (flat.length <= 80) return flat;
1056
1183
  if (type.kind === "object") {
@@ -1069,11 +1196,11 @@ ${lines.join("\n")}
1069
1196
  }
1070
1197
  return flat;
1071
1198
  }
1072
- function bindingText(binding, type) {
1199
+ function bindingText(binding, type, written) {
1073
1200
  if (binding.declaredBy === "function" && type.kind === "function") {
1074
- return `function ${binding.name}${formatType3(type)}`;
1201
+ return `function ${binding.name}${pretty(type)}`;
1075
1202
  }
1076
- return `${keyword(binding)} ${binding.name}: ${pretty(type)}`;
1203
+ return `${keyword(binding)} ${binding.name}: ${pretty(type, written)}`;
1077
1204
  }
1078
1205
  function keyword(binding) {
1079
1206
  if (binding.kind === "param" || binding.kind === "self") return "(parameter)";
@@ -1154,7 +1281,7 @@ import {
1154
1281
  CompletionItemKind as CompletionItemKind3,
1155
1282
  InsertTextFormat
1156
1283
  } from "vscode-languageserver";
1157
- import { formatType as formatType4, isClassType as isClassType2 } from "luaut-parser";
1284
+ import { formatType as formatType4, isClassType as isClassType3 } from "luaut-parser";
1158
1285
 
1159
1286
  // src/features/autoImport.ts
1160
1287
  import { readdirSync as readdirSync2 } from "fs";
@@ -1191,7 +1318,7 @@ function importItems(analyzer, analysis, typePosition, taken) {
1191
1318
  function serviceItems(analysis, taken) {
1192
1319
  const services = analysis.types.aliases.get("Services");
1193
1320
  if (!services || services.kind !== "object") return [];
1194
- const at = serviceInsertion(analysis.program.body.statements);
1321
+ const at2 = serviceInsertion(analysis.program.body.statements);
1195
1322
  const items = [];
1196
1323
  for (const name of services.properties.keys()) {
1197
1324
  if (taken.has(name)) continue;
@@ -1202,8 +1329,8 @@ function serviceItems(analysis, taken) {
1202
1329
  labelDetails: { description: "service" },
1203
1330
  detail: line,
1204
1331
  sortText: `5${name}`,
1205
- additionalTextEdits: [{ range: { start: at.position, end: at.position }, newText: `${line}
1206
- ${at.gap}` }]
1332
+ additionalTextEdits: [{ range: { start: at2.position, end: at2.position }, newText: `${line}
1333
+ ${at2.gap}` }]
1207
1334
  });
1208
1335
  }
1209
1336
  return items;
@@ -1215,24 +1342,24 @@ function importEdit(analyzer, analysis, file, name, specifier, typePosition) {
1215
1342
  if (existing) {
1216
1343
  const last = existing.specifiers[existing.specifiers.length - 1];
1217
1344
  if (last) {
1218
- const at2 = endOf(last);
1219
- return { range: { start: at2, end: at2 }, newText: `, ${name}` };
1345
+ const at3 = endOf(last);
1346
+ return { range: { start: at3, end: at3 }, newText: `, ${name}` };
1220
1347
  }
1221
1348
  if (existing.defaultImport) {
1222
- const at2 = endOf(existing.defaultImport);
1223
- return { range: { start: at2, end: at2 }, newText: `, { ${name} }` };
1349
+ const at3 = endOf(existing.defaultImport);
1350
+ return { range: { start: at3, end: at3 }, newText: `, { ${name} }` };
1224
1351
  }
1225
1352
  }
1226
1353
  const line = `import { ${name} } from "${specifier}"`;
1227
1354
  const lastImport = imports[imports.length - 1];
1228
1355
  if (lastImport) {
1229
- const at2 = { line: lastImport.line.end, character: 0 };
1230
- return { range: { start: at2, end: at2 }, newText: `${line}
1356
+ const at3 = { line: lastImport.line.end, character: 0 };
1357
+ return { range: { start: at3, end: at3 }, newText: `${line}
1231
1358
  ` };
1232
1359
  }
1233
1360
  const first = statements[0];
1234
- const at = { line: first ? first.line.start - 1 : 0, character: 0 };
1235
- return { range: { start: at, end: at }, newText: first ? `${line}
1361
+ const at2 = { line: first ? first.line.start - 1 : 0, character: 0 };
1362
+ return { range: { start: at2, end: at2 }, newText: first ? `${line}
1236
1363
 
1237
1364
  ` : `${line}
1238
1365
  ` };
@@ -1325,12 +1452,12 @@ function completion(analyzer, document, position) {
1325
1452
  const operator = memberOperator(source, start);
1326
1453
  const alreadyCalled = /^\s*\(/.test(source.slice(end));
1327
1454
  const standIns = operator === ":" ? [alreadyCalled ? PLACEHOLDER : `${PLACEHOLDER}()`] : operator === "." && !alreadyCalled ? [PLACEHOLDER, `${PLACEHOLDER}()`] : [PLACEHOLDER];
1328
- const at = { line: position.line, character: position.character - (offset - start) };
1455
+ const at2 = { line: position.line, character: position.character - (offset - start) };
1329
1456
  let first;
1330
1457
  for (const standIn of standIns) {
1331
1458
  const patched = source.slice(0, start) + standIn + source.slice(end);
1332
1459
  const analysis = analyzer.analyze(document.uri, -1, patched);
1333
- const path = pathAt(analysis.program, at, true);
1460
+ const path = pathAt(analysis.program, at2, true);
1334
1461
  const index = path.findLastIndex(
1335
1462
  (n) => n.type === "Identifier" && n.name === PLACEHOLDER
1336
1463
  );
@@ -1346,8 +1473,8 @@ function completion(analyzer, document, position) {
1346
1473
  if (inTypePosition(first.path)) {
1347
1474
  const named = [...first.analysis.types.aliases].map(([name, type]) => ({
1348
1475
  label: name,
1349
- kind: isClassType2(type) ? CompletionItemKind3.Class : CompletionItemKind3.Interface,
1350
- detail: isClassType2(type) ? "class" : "type"
1476
+ kind: isClassType3(type) ? CompletionItemKind3.Class : CompletionItemKind3.Interface,
1477
+ detail: isClassType3(type) ? "class" : "type"
1351
1478
  }));
1352
1479
  const primitives = PRIMITIVES2.map((name) => ({
1353
1480
  label: name,
@@ -1367,7 +1494,7 @@ function completion(analyzer, document, position) {
1367
1494
  for (const binding of first.analysis.scopes.bindings.values()) taken.add(binding.name);
1368
1495
  const current = analyzer.get(document);
1369
1496
  return [
1370
- ...valueItems(first.analysis, at),
1497
+ ...valueItems(first.analysis, at2, insideFunction(first.path)),
1371
1498
  ...contextKeywords(source.slice(0, start)),
1372
1499
  ...importItems(analyzer, current, false, taken),
1373
1500
  ...serviceItems(current, taken)
@@ -1441,8 +1568,8 @@ function objectKeyItems(analysis, path, after) {
1441
1568
  }
1442
1569
  function indexKeys(analysis, position, literal) {
1443
1570
  const path = pathAt(analysis.program, position, false);
1444
- const at = path.indexOf(literal);
1445
- const parent = at > 0 ? path[at - 1] : void 0;
1571
+ const at2 = path.indexOf(literal);
1572
+ const parent = at2 > 0 ? path[at2 - 1] : void 0;
1446
1573
  if (!parent) return [];
1447
1574
  let indexed;
1448
1575
  if (parent.type === "IndexedAccessTypeNode" && parent.indexType === literal) {
@@ -1534,14 +1661,18 @@ function isMethodOnly(type) {
1534
1661
  return false;
1535
1662
  }
1536
1663
  }
1537
- function valueItems(analysis, at) {
1664
+ function insideFunction(path) {
1665
+ return path.some((n) => n.type === "FunctionExpression" || n.type === "FunctionDeclaration" || n.type === "FunctionDeclarationStatement" || n.type === "FunctionBody");
1666
+ }
1667
+ function valueItems(analysis, at2, inFunction) {
1538
1668
  const items = [];
1539
1669
  const seen = /* @__PURE__ */ new Set();
1540
1670
  for (const binding of analysis.scopes.bindings.values()) {
1541
1671
  if (binding.name === PLACEHOLDER || seen.has(binding.name)) continue;
1542
1672
  if (binding.declaredBy === "type") continue;
1543
1673
  const declaration = binding.declarationNode;
1544
- if (declaration && declaration.line.start - 1 > at.line) continue;
1674
+ const later = declaration !== void 0 && declaration.line.start - 1 > at2.line;
1675
+ if (later && !inFunction && binding.declaredBy !== "function") continue;
1545
1676
  seen.add(binding.name);
1546
1677
  const type = analysis.types.bindingType.get(binding.id);
1547
1678
  items.push({
@@ -1679,9 +1810,9 @@ function helpAt(analysis, position) {
1679
1810
  function methodType(analysis, call) {
1680
1811
  const object = call.object;
1681
1812
  const method = call.method;
1682
- const objectType = analysis.types.typeOf.get(object);
1683
- if (!objectType) return void 0;
1684
- return memberType(objectType, method.name, analysis);
1813
+ const objectType2 = analysis.types.typeOf.get(object);
1814
+ if (!objectType2) return void 0;
1815
+ return memberType(objectType2, method.name, analysis);
1685
1816
  }
1686
1817
  function memberType(type, name, analysis) {
1687
1818
  if (type.kind === "object") return type.properties.get(name)?.type;
@@ -1733,6 +1864,15 @@ function documentSymbols(analysis) {
1733
1864
  }
1734
1865
  break;
1735
1866
  }
1867
+ case "ClassDeclaration": {
1868
+ const declaration = node;
1869
+ const superclass = declaration.superclass?.name;
1870
+ out.push({
1871
+ ...symbol(declaration.name.name, SymbolKind.Class, node, superclass && `extends ${superclass}`),
1872
+ children: declaration.members.map((member) => classMember(analysis, member))
1873
+ });
1874
+ break;
1875
+ }
1736
1876
  case "DeclareClassStatement": {
1737
1877
  const name = node.name.name;
1738
1878
  const superclass = node.superclass?.base;
@@ -1750,6 +1890,23 @@ function documentSymbols(analysis) {
1750
1890
  });
1751
1891
  return out;
1752
1892
  }
1893
+ function classMember(analysis, member) {
1894
+ switch (member.type) {
1895
+ case "ClassConstructor":
1896
+ return symbol("constructor", SymbolKind.Constructor, member);
1897
+ case "ClassField":
1898
+ return symbol(
1899
+ member.name.name,
1900
+ member.isStatic ? SymbolKind.Constant : SymbolKind.Field,
1901
+ member,
1902
+ member.typeAnnotation ? void 0 : detailOf(analysis, member)
1903
+ );
1904
+ case "ClassAccessor":
1905
+ return symbol(member.name.name, SymbolKind.Property, member, member.kind);
1906
+ case "ClassMethod":
1907
+ return symbol(member.name.name, SymbolKind.Method, member, member.isStatic ? "static" : void 0);
1908
+ }
1909
+ }
1753
1910
  function functionName(node) {
1754
1911
  const named = node;
1755
1912
  if (typeof named.name === "string") return named.name;
@@ -1776,7 +1933,7 @@ function symbol(name, kind, node, detail) {
1776
1933
  }
1777
1934
 
1778
1935
  // src/features/semanticTokens.ts
1779
- import { tokenize, isClassType as isClassType3, unknownType } from "luaut-parser";
1936
+ import { tokenize, isClassType as isClassType4, unknownType } from "luaut-parser";
1780
1937
  var TOKEN_TYPES = [
1781
1938
  "namespace",
1782
1939
  "type",
@@ -1806,15 +1963,17 @@ var SOFT_KEYWORDS = /* @__PURE__ */ new Set([
1806
1963
  "asserts",
1807
1964
  "satisfies",
1808
1965
  "typeof",
1809
- "default"
1966
+ "default",
1967
+ "new",
1968
+ "super"
1810
1969
  ]);
1811
1970
  var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["default"]);
1812
1971
  var PRIMITIVES3 = /* @__PURE__ */ new Set(["any", "unknown", "never", "nil", "boolean", "number", "string", "thread", "buffer"]);
1813
1972
  function semanticTokens(analysis) {
1814
1973
  const entries = /* @__PURE__ */ new Map();
1815
- const add = (at, length, type, modifiers = []) => {
1816
- const line = at.line.start - 1;
1817
- const character = at.column.start - 1;
1974
+ const add = (at2, length, type, modifiers = []) => {
1975
+ const line = at2.line.start - 1;
1976
+ const character = at2.column.start - 1;
1818
1977
  const key = `${line}:${character}`;
1819
1978
  if (!entries.has(key)) entries.set(key, { line, character, length, type, modifiers });
1820
1979
  };
@@ -1855,6 +2014,25 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1855
2014
  add(node, name.length, valueKind(analysis, binding), modifiersOf(binding, true));
1856
2015
  return;
1857
2016
  }
2017
+ // A class body's own words. `get`, `set`, `static` and `constructor`
2018
+ // are ordinary names anywhere else, so they are coloured from the
2019
+ // member they open rather than wherever they are written.
2020
+ case "ClassMethod":
2021
+ case "ClassField":
2022
+ case "ClassAccessor":
2023
+ case "ClassConstructor": {
2024
+ const opener = node.type === "ClassConstructor" ? "constructor" : node.type === "ClassAccessor" ? node.kind : void 0;
2025
+ const words = firstTokensWithin(identifiers, node, 2);
2026
+ let index = 0;
2027
+ if (node.isStatic === true && words[index] && wordOf(words[index]) === "static") {
2028
+ add(words[index], "static".length, "keyword");
2029
+ index++;
2030
+ }
2031
+ if (opener && words[index] && wordOf(words[index]) === opener) {
2032
+ add(words[index], opener.length, "keyword");
2033
+ }
2034
+ return;
2035
+ }
1858
2036
  case "TypeReference": {
1859
2037
  const base = node.base;
1860
2038
  const namespace = node.namespace;
@@ -1864,7 +2042,7 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1864
2042
  if (!baseToken) return;
1865
2043
  if (!namespace && typeParameterInScope2(ancestors, base)) {
1866
2044
  add(baseToken, base.length, "typeParameter");
1867
- } else if (isClassType3(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? unknownType)) {
2045
+ } else if (isClassType4(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? unknownType)) {
1868
2046
  add(baseToken, base.length, "class");
1869
2047
  } else {
1870
2048
  add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);
@@ -1873,6 +2051,10 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1873
2051
  }
1874
2052
  }
1875
2053
  }
2054
+ function wordOf(token) {
2055
+ const value = token.value;
2056
+ return typeof value === "string" ? value : void 0;
2057
+ }
1876
2058
  function identifier(analysis, node, parent, add) {
1877
2059
  const name = node.name;
1878
2060
  const as = (type, modifiers = []) => add(node, name.length, type, modifiers);
@@ -1896,6 +2078,19 @@ function identifier(analysis, node, parent, add) {
1896
2078
  case "DeclareClassStatement":
1897
2079
  if (parent.name === node) return as("class", ["declaration"]);
1898
2080
  break;
2081
+ case "ClassDeclaration":
2082
+ if (parent.name === node) return as("class", ["declaration"]);
2083
+ if (parent.superclass === node) return as("class");
2084
+ break;
2085
+ case "ClassMethod":
2086
+ if (parent.name === node) return as("method", ["declaration"]);
2087
+ break;
2088
+ case "ClassField":
2089
+ if (parent.name === node) return as("property", ["declaration"]);
2090
+ break;
2091
+ case "ClassAccessor":
2092
+ if (parent.name === node) return as("property", ["declaration"]);
2093
+ break;
1899
2094
  case "DeclareStatement":
1900
2095
  if (parent.id === node) return as(isFunction(typeOfNode(parent.valueType)) ? "function" : "variable", ["declaration"]);
1901
2096
  break;
@@ -2124,6 +2319,11 @@ function createServer(connection, options = {}) {
2124
2319
  (d) => hover(analyzer.get(d), p.position),
2125
2320
  null
2126
2321
  ));
2322
+ connection.onRequest("luaut/hover", (p) => withDocument(
2323
+ p.textDocument.uri,
2324
+ (d) => hover(analyzer.get(d), p.position, Math.max(0, p.depth ?? 0)),
2325
+ null
2326
+ ));
2127
2327
  connection.onDefinition((p) => withDocument(
2128
2328
  p.textDocument.uri,
2129
2329
  (d) => {
@@ -2213,4 +2413,4 @@ export {
2213
2413
  createServer,
2214
2414
  startServer
2215
2415
  };
2216
- //# sourceMappingURL=chunk-XK3KS3T4.js.map
2416
+ //# sourceMappingURL=chunk-GHAR7NOH.js.map