luaut-language-server 4.0.0 → 5.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
@@ -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,104 @@ 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 {
820
+ isClassType,
821
+ arrayOf,
822
+ tuple,
823
+ objectType,
824
+ fn,
825
+ union as union2,
826
+ intersection,
827
+ aliasNameOf,
828
+ withoutAliasName
829
+ } from "luaut-parser";
830
+ function expandAliases(type, aliases, depth, seen = []) {
831
+ if (depth <= 0) return type;
832
+ const name = withheldName(type);
833
+ if (name !== void 0 && !seen.includes(name)) {
834
+ const opened = aliases.get(name) ?? type;
835
+ const inner = [...seen, name];
836
+ return children2(unnamed(opened), (t) => expandAliases(t, aliases, depth - 1, inner));
837
+ }
838
+ return children2(type, (t) => expandAliases(t, aliases, depth, seen));
839
+ }
840
+ function withheldName(type) {
841
+ if (type.kind === "genericRef") return type.typeArguments.length ? void 0 : type.name;
842
+ return aliasNameOf(type);
843
+ }
844
+ function unnamed(type) {
845
+ return withoutAliasName(type);
846
+ }
847
+ function children2(type, f) {
848
+ switch (type.kind) {
849
+ case "array":
850
+ return arrayOf(f(type.element));
851
+ case "tuple":
852
+ return tuple(type.elements.map(f), type.isPack);
853
+ case "union":
854
+ return union2(type.types.map(f));
855
+ case "intersection": {
856
+ const out = intersection(type.types.map(f));
857
+ return type.name && out.kind === "intersection" ? { ...out, name: type.name } : out;
858
+ }
859
+ case "object": {
860
+ if (isClassType(type)) return type;
861
+ const out = objectType(
862
+ [...type.properties].map(([name, property]) => [name, { ...property, type: f(property.type) }]),
863
+ type.indexer && { key: type.indexer.key, value: f(type.indexer.value) },
864
+ type.frozen
865
+ );
866
+ if (type.name) out.name = type.name;
867
+ return out;
868
+ }
869
+ case "function":
870
+ return fn(
871
+ type.params.map((p) => ({ ...p, type: f(p.type) })),
872
+ f(type.returns),
873
+ type.varargs && f(type.varargs),
874
+ type.typeParams,
875
+ type.predicate
876
+ );
877
+ default:
878
+ return type;
879
+ }
880
+ }
881
+
882
+ // src/features/hover.ts
883
+ function hover(analysis, position, depth = 0) {
817
884
  const path = pathAt(analysis.program, position, true);
818
885
  for (let i = path.length - 1; i >= 0; i--) {
819
886
  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]) };
887
+ const text = at(analysis, path, i, depth);
888
+ if (!text) continue;
889
+ const canExpand = at(analysis, path, i, depth + 1) !== text;
890
+ return {
891
+ contents: { kind: "markdown", value: code(text) },
892
+ range: toRange(path[i]),
893
+ depth,
894
+ canExpand
895
+ };
822
896
  }
823
897
  return null;
824
898
  }
899
+ function at(analysis, path, index, depth) {
900
+ const previous = expansion;
901
+ expansion = { depth, aliases: analysis.types.aliases, source: analysis.source };
902
+ try {
903
+ return describe(analysis, path, index);
904
+ } finally {
905
+ expansion = previous;
906
+ }
907
+ }
908
+ var expansion = {
909
+ depth: 0,
910
+ aliases: /* @__PURE__ */ new Map(),
911
+ source: ""
912
+ };
825
913
  var UNNAMED = /* @__PURE__ */ new Set([
826
914
  "BinaryExpression",
827
915
  "UnaryExpression",
@@ -854,8 +942,8 @@ function describe(analysis, path, index) {
854
942
  case "TableExpression": {
855
943
  const field = fieldWithKey(parent, node);
856
944
  if (!field) break;
857
- const objectType = types.typeOf.get(parent);
858
- const property = objectType?.kind === "object" ? objectType.properties.get(name) : void 0;
945
+ const objectType2 = types.typeOf.get(parent);
946
+ const property = objectType2?.kind === "object" ? objectType2.properties.get(name) : void 0;
859
947
  const type2 = property?.type ?? types.typeOf.get(field.value);
860
948
  return type2 && `(property) ${name}: ${pretty(type2)}`;
861
949
  }
@@ -903,6 +991,19 @@ function describe(analysis, path, index) {
903
991
  case "DeclareClassStatement":
904
992
  if (parent.name === node) return classText(analysis, name);
905
993
  break;
994
+ case "ClassDeclaration":
995
+ if (parent.name === node) return classText(analysis, name);
996
+ break;
997
+ case "ClassField":
998
+ if (parent.name === node) {
999
+ const type2 = typeOfNode(parent.typeAnnotation) ?? types.typeOf.get(parent.init);
1000
+ const prefix = parent.isStatic ? "(static) " : "(field) ";
1001
+ return type2 ? `${prefix}${name}: ${pretty(type2)}` : `${prefix}${name}`;
1002
+ }
1003
+ break;
1004
+ case "ClassAccessor":
1005
+ if (parent.name === node) return `(${parent.kind === "get" ? "getter" : "setter"}) ${name}`;
1006
+ break;
906
1007
  case "TableTypeProperty":
907
1008
  if (parent.key === node) {
908
1009
  const type2 = typeOfNode(parent.valueType);
@@ -953,7 +1054,7 @@ function describe(analysis, path, index) {
953
1054
  case "TypedIdentifier": {
954
1055
  const binding = bindingOfNode(analysis, node);
955
1056
  const type2 = binding && types.bindingType.get(binding.id);
956
- return type2 ? bindingText(binding, type2) : void 0;
1057
+ return type2 ? bindingText(binding, type2, asWritten(node.typeAnnotation)) : void 0;
957
1058
  }
958
1059
  // A type written by name: `number`, `Shape`, `Partial<User>`, or a type
959
1060
  // parameter in scope.
@@ -967,7 +1068,7 @@ function describe(analysis, path, index) {
967
1068
  if (!node.typeArguments.length) {
968
1069
  const qualified = node.namespace ? `${node.namespace}.${base}` : base;
969
1070
  const alias = types.aliases.get(qualified);
970
- if (alias && isClassType(alias)) return classText(analysis, qualified);
1071
+ if (alias && isClassType2(alias)) return classText(analysis, qualified);
971
1072
  if (alias) return `type ${qualified} = ${pretty(alias)}`;
972
1073
  }
973
1074
  const type2 = typeOfNode(node);
@@ -999,16 +1100,25 @@ function declareText(analysis, statement) {
999
1100
  }
1000
1101
  function classText(analysis, name) {
1001
1102
  const type = analysis.types.aliases.get(name);
1002
- if (!type || !isClassType(type)) return void 0;
1103
+ if (!type || !isClassType2(type)) return void 0;
1003
1104
  const superclass = type.class.superclass;
1004
1105
  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} {
1106
+ const own = [...type.properties].filter(([key, property]) => (
1107
+ // `ClassObject` is on every instance and says nothing about this one.
1108
+ key !== "ClassObject" && (inherited?.kind !== "object" || inherited.properties.get(key) !== property)
1109
+ ));
1110
+ const written = isRuntimeClass(analysis, name);
1111
+ const head = `${written ? "" : "declare "}class ${name}${superclass ? ` extends ${superclass}` : ""}`;
1112
+ const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${formatType3(property.type)}${written ? "" : ","}`);
1113
+ return own.length ? `${head} {
1010
1114
  ${lines.join("\n")}
1011
- }`;
1115
+ }` : `${head} {}`;
1116
+ }
1117
+ function isRuntimeClass(analysis, name) {
1118
+ return analysis.program.body.statements.some((statement) => {
1119
+ const declaration = statement.type === "ExportStatement" ? statement.declaration : statement;
1120
+ return declaration.type === "ClassDeclaration" && declaration.name.name === name;
1121
+ });
1012
1122
  }
1013
1123
  function typeParameterText(analysis, parameter) {
1014
1124
  return `(type parameter) ${typeParameterSignature(analysis, parameter)}`;
@@ -1050,7 +1160,27 @@ function fieldWithKey(table, key) {
1050
1160
  const fields = table.fields;
1051
1161
  return fields.find((f) => f.type === "TableFieldNamed" && f.key === key);
1052
1162
  }
1053
- function pretty(type) {
1163
+ function pretty(type, written) {
1164
+ const named = render(expandAliases(type, expansion.aliases, 0));
1165
+ const shorthand = written !== void 0 && written !== named ? written : void 0;
1166
+ const depth = shorthand === void 0 ? expansion.depth : expansion.depth - 1;
1167
+ if (depth < 0) return shorthand;
1168
+ return depth === 0 ? named : render(expandAliases(type, expansion.aliases, depth));
1169
+ }
1170
+ function asWritten(node) {
1171
+ if (!isSpanned(node) || !expansion.source) return void 0;
1172
+ const lines = expansion.source.split(/\r?\n/);
1173
+ const { line, column } = node;
1174
+ if (line.start < 1 || line.end > lines.length) return void 0;
1175
+ const text = line.start === line.end ? lines[line.start - 1].slice(column.start - 1, column.end - 1) : [
1176
+ lines[line.start - 1].slice(column.start - 1),
1177
+ ...lines.slice(line.start, line.end - 1),
1178
+ lines[line.end - 1].slice(0, column.end - 1)
1179
+ ].join(" ");
1180
+ const folded = text.trim().replace(/\s+/g, " ");
1181
+ return folded.length ? folded : void 0;
1182
+ }
1183
+ function render(type) {
1054
1184
  const flat = formatType3(type);
1055
1185
  if (flat.length <= 80) return flat;
1056
1186
  if (type.kind === "object") {
@@ -1069,11 +1199,11 @@ ${lines.join("\n")}
1069
1199
  }
1070
1200
  return flat;
1071
1201
  }
1072
- function bindingText(binding, type) {
1202
+ function bindingText(binding, type, written) {
1073
1203
  if (binding.declaredBy === "function" && type.kind === "function") {
1074
- return `function ${binding.name}${formatType3(type)}`;
1204
+ return `function ${binding.name}${pretty(type)}`;
1075
1205
  }
1076
- return `${keyword(binding)} ${binding.name}: ${pretty(type)}`;
1206
+ return `${keyword(binding)} ${binding.name}: ${pretty(type, written)}`;
1077
1207
  }
1078
1208
  function keyword(binding) {
1079
1209
  if (binding.kind === "param" || binding.kind === "self") return "(parameter)";
@@ -1154,7 +1284,7 @@ import {
1154
1284
  CompletionItemKind as CompletionItemKind3,
1155
1285
  InsertTextFormat
1156
1286
  } from "vscode-languageserver";
1157
- import { formatType as formatType4, isClassType as isClassType2 } from "luaut-parser";
1287
+ import { formatType as formatType4, isClassType as isClassType3 } from "luaut-parser";
1158
1288
 
1159
1289
  // src/features/autoImport.ts
1160
1290
  import { readdirSync as readdirSync2 } from "fs";
@@ -1191,7 +1321,7 @@ function importItems(analyzer, analysis, typePosition, taken) {
1191
1321
  function serviceItems(analysis, taken) {
1192
1322
  const services = analysis.types.aliases.get("Services");
1193
1323
  if (!services || services.kind !== "object") return [];
1194
- const at = serviceInsertion(analysis.program.body.statements);
1324
+ const at2 = serviceInsertion(analysis.program.body.statements);
1195
1325
  const items = [];
1196
1326
  for (const name of services.properties.keys()) {
1197
1327
  if (taken.has(name)) continue;
@@ -1202,8 +1332,8 @@ function serviceItems(analysis, taken) {
1202
1332
  labelDetails: { description: "service" },
1203
1333
  detail: line,
1204
1334
  sortText: `5${name}`,
1205
- additionalTextEdits: [{ range: { start: at.position, end: at.position }, newText: `${line}
1206
- ${at.gap}` }]
1335
+ additionalTextEdits: [{ range: { start: at2.position, end: at2.position }, newText: `${line}
1336
+ ${at2.gap}` }]
1207
1337
  });
1208
1338
  }
1209
1339
  return items;
@@ -1215,24 +1345,24 @@ function importEdit(analyzer, analysis, file, name, specifier, typePosition) {
1215
1345
  if (existing) {
1216
1346
  const last = existing.specifiers[existing.specifiers.length - 1];
1217
1347
  if (last) {
1218
- const at2 = endOf(last);
1219
- return { range: { start: at2, end: at2 }, newText: `, ${name}` };
1348
+ const at3 = endOf(last);
1349
+ return { range: { start: at3, end: at3 }, newText: `, ${name}` };
1220
1350
  }
1221
1351
  if (existing.defaultImport) {
1222
- const at2 = endOf(existing.defaultImport);
1223
- return { range: { start: at2, end: at2 }, newText: `, { ${name} }` };
1352
+ const at3 = endOf(existing.defaultImport);
1353
+ return { range: { start: at3, end: at3 }, newText: `, { ${name} }` };
1224
1354
  }
1225
1355
  }
1226
1356
  const line = `import { ${name} } from "${specifier}"`;
1227
1357
  const lastImport = imports[imports.length - 1];
1228
1358
  if (lastImport) {
1229
- const at2 = { line: lastImport.line.end, character: 0 };
1230
- return { range: { start: at2, end: at2 }, newText: `${line}
1359
+ const at3 = { line: lastImport.line.end, character: 0 };
1360
+ return { range: { start: at3, end: at3 }, newText: `${line}
1231
1361
  ` };
1232
1362
  }
1233
1363
  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}
1364
+ const at2 = { line: first ? first.line.start - 1 : 0, character: 0 };
1365
+ return { range: { start: at2, end: at2 }, newText: first ? `${line}
1236
1366
 
1237
1367
  ` : `${line}
1238
1368
  ` };
@@ -1325,12 +1455,12 @@ function completion(analyzer, document, position) {
1325
1455
  const operator = memberOperator(source, start);
1326
1456
  const alreadyCalled = /^\s*\(/.test(source.slice(end));
1327
1457
  const standIns = operator === ":" ? [alreadyCalled ? PLACEHOLDER : `${PLACEHOLDER}()`] : operator === "." && !alreadyCalled ? [PLACEHOLDER, `${PLACEHOLDER}()`] : [PLACEHOLDER];
1328
- const at = { line: position.line, character: position.character - (offset - start) };
1458
+ const at2 = { line: position.line, character: position.character - (offset - start) };
1329
1459
  let first;
1330
1460
  for (const standIn of standIns) {
1331
1461
  const patched = source.slice(0, start) + standIn + source.slice(end);
1332
1462
  const analysis = analyzer.analyze(document.uri, -1, patched);
1333
- const path = pathAt(analysis.program, at, true);
1463
+ const path = pathAt(analysis.program, at2, true);
1334
1464
  const index = path.findLastIndex(
1335
1465
  (n) => n.type === "Identifier" && n.name === PLACEHOLDER
1336
1466
  );
@@ -1346,8 +1476,8 @@ function completion(analyzer, document, position) {
1346
1476
  if (inTypePosition(first.path)) {
1347
1477
  const named = [...first.analysis.types.aliases].map(([name, type]) => ({
1348
1478
  label: name,
1349
- kind: isClassType2(type) ? CompletionItemKind3.Class : CompletionItemKind3.Interface,
1350
- detail: isClassType2(type) ? "class" : "type"
1479
+ kind: isClassType3(type) ? CompletionItemKind3.Class : CompletionItemKind3.Interface,
1480
+ detail: isClassType3(type) ? "class" : "type"
1351
1481
  }));
1352
1482
  const primitives = PRIMITIVES2.map((name) => ({
1353
1483
  label: name,
@@ -1367,7 +1497,7 @@ function completion(analyzer, document, position) {
1367
1497
  for (const binding of first.analysis.scopes.bindings.values()) taken.add(binding.name);
1368
1498
  const current = analyzer.get(document);
1369
1499
  return [
1370
- ...valueItems(first.analysis, at),
1500
+ ...valueItems(first.analysis, at2, insideFunction(first.path)),
1371
1501
  ...contextKeywords(source.slice(0, start)),
1372
1502
  ...importItems(analyzer, current, false, taken),
1373
1503
  ...serviceItems(current, taken)
@@ -1441,8 +1571,8 @@ function objectKeyItems(analysis, path, after) {
1441
1571
  }
1442
1572
  function indexKeys(analysis, position, literal) {
1443
1573
  const path = pathAt(analysis.program, position, false);
1444
- const at = path.indexOf(literal);
1445
- const parent = at > 0 ? path[at - 1] : void 0;
1574
+ const at2 = path.indexOf(literal);
1575
+ const parent = at2 > 0 ? path[at2 - 1] : void 0;
1446
1576
  if (!parent) return [];
1447
1577
  let indexed;
1448
1578
  if (parent.type === "IndexedAccessTypeNode" && parent.indexType === literal) {
@@ -1534,14 +1664,18 @@ function isMethodOnly(type) {
1534
1664
  return false;
1535
1665
  }
1536
1666
  }
1537
- function valueItems(analysis, at) {
1667
+ function insideFunction(path) {
1668
+ return path.some((n) => n.type === "FunctionExpression" || n.type === "FunctionDeclaration" || n.type === "FunctionDeclarationStatement" || n.type === "FunctionBody");
1669
+ }
1670
+ function valueItems(analysis, at2, inFunction) {
1538
1671
  const items = [];
1539
1672
  const seen = /* @__PURE__ */ new Set();
1540
1673
  for (const binding of analysis.scopes.bindings.values()) {
1541
1674
  if (binding.name === PLACEHOLDER || seen.has(binding.name)) continue;
1542
1675
  if (binding.declaredBy === "type") continue;
1543
1676
  const declaration = binding.declarationNode;
1544
- if (declaration && declaration.line.start - 1 > at.line) continue;
1677
+ const later = declaration !== void 0 && declaration.line.start - 1 > at2.line;
1678
+ if (later && !inFunction && binding.declaredBy !== "function") continue;
1545
1679
  seen.add(binding.name);
1546
1680
  const type = analysis.types.bindingType.get(binding.id);
1547
1681
  items.push({
@@ -1679,9 +1813,9 @@ function helpAt(analysis, position) {
1679
1813
  function methodType(analysis, call) {
1680
1814
  const object = call.object;
1681
1815
  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);
1816
+ const objectType2 = analysis.types.typeOf.get(object);
1817
+ if (!objectType2) return void 0;
1818
+ return memberType(objectType2, method.name, analysis);
1685
1819
  }
1686
1820
  function memberType(type, name, analysis) {
1687
1821
  if (type.kind === "object") return type.properties.get(name)?.type;
@@ -1733,6 +1867,15 @@ function documentSymbols(analysis) {
1733
1867
  }
1734
1868
  break;
1735
1869
  }
1870
+ case "ClassDeclaration": {
1871
+ const declaration = node;
1872
+ const superclass = declaration.superclass?.name;
1873
+ out.push({
1874
+ ...symbol(declaration.name.name, SymbolKind.Class, node, superclass && `extends ${superclass}`),
1875
+ children: declaration.members.map((member) => classMember(analysis, member))
1876
+ });
1877
+ break;
1878
+ }
1736
1879
  case "DeclareClassStatement": {
1737
1880
  const name = node.name.name;
1738
1881
  const superclass = node.superclass?.base;
@@ -1750,6 +1893,23 @@ function documentSymbols(analysis) {
1750
1893
  });
1751
1894
  return out;
1752
1895
  }
1896
+ function classMember(analysis, member) {
1897
+ switch (member.type) {
1898
+ case "ClassConstructor":
1899
+ return symbol("constructor", SymbolKind.Constructor, member);
1900
+ case "ClassField":
1901
+ return symbol(
1902
+ member.name.name,
1903
+ member.isStatic ? SymbolKind.Constant : SymbolKind.Field,
1904
+ member,
1905
+ member.typeAnnotation ? void 0 : detailOf(analysis, member)
1906
+ );
1907
+ case "ClassAccessor":
1908
+ return symbol(member.name.name, SymbolKind.Property, member, member.kind);
1909
+ case "ClassMethod":
1910
+ return symbol(member.name.name, SymbolKind.Method, member, member.isStatic ? "static" : void 0);
1911
+ }
1912
+ }
1753
1913
  function functionName(node) {
1754
1914
  const named = node;
1755
1915
  if (typeof named.name === "string") return named.name;
@@ -1776,7 +1936,7 @@ function symbol(name, kind, node, detail) {
1776
1936
  }
1777
1937
 
1778
1938
  // src/features/semanticTokens.ts
1779
- import { tokenize, isClassType as isClassType3, unknownType } from "luaut-parser";
1939
+ import { tokenize, isClassType as isClassType4, unknownType } from "luaut-parser";
1780
1940
  var TOKEN_TYPES = [
1781
1941
  "namespace",
1782
1942
  "type",
@@ -1806,15 +1966,17 @@ var SOFT_KEYWORDS = /* @__PURE__ */ new Set([
1806
1966
  "asserts",
1807
1967
  "satisfies",
1808
1968
  "typeof",
1809
- "default"
1969
+ "default",
1970
+ "new",
1971
+ "super"
1810
1972
  ]);
1811
1973
  var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["default"]);
1812
1974
  var PRIMITIVES3 = /* @__PURE__ */ new Set(["any", "unknown", "never", "nil", "boolean", "number", "string", "thread", "buffer"]);
1813
1975
  function semanticTokens(analysis) {
1814
1976
  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;
1977
+ const add = (at2, length, type, modifiers = []) => {
1978
+ const line = at2.line.start - 1;
1979
+ const character = at2.column.start - 1;
1818
1980
  const key = `${line}:${character}`;
1819
1981
  if (!entries.has(key)) entries.set(key, { line, character, length, type, modifiers });
1820
1982
  };
@@ -1855,6 +2017,25 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1855
2017
  add(node, name.length, valueKind(analysis, binding), modifiersOf(binding, true));
1856
2018
  return;
1857
2019
  }
2020
+ // A class body's own words. `get`, `set`, `static` and `constructor`
2021
+ // are ordinary names anywhere else, so they are coloured from the
2022
+ // member they open rather than wherever they are written.
2023
+ case "ClassMethod":
2024
+ case "ClassField":
2025
+ case "ClassAccessor":
2026
+ case "ClassConstructor": {
2027
+ const opener = node.type === "ClassConstructor" ? "constructor" : node.type === "ClassAccessor" ? node.kind : void 0;
2028
+ const words = firstTokensWithin(identifiers, node, 2);
2029
+ let index = 0;
2030
+ if (node.isStatic === true && words[index] && wordOf(words[index]) === "static") {
2031
+ add(words[index], "static".length, "keyword");
2032
+ index++;
2033
+ }
2034
+ if (opener && words[index] && wordOf(words[index]) === opener) {
2035
+ add(words[index], opener.length, "keyword");
2036
+ }
2037
+ return;
2038
+ }
1858
2039
  case "TypeReference": {
1859
2040
  const base = node.base;
1860
2041
  const namespace = node.namespace;
@@ -1864,7 +2045,7 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1864
2045
  if (!baseToken) return;
1865
2046
  if (!namespace && typeParameterInScope2(ancestors, base)) {
1866
2047
  add(baseToken, base.length, "typeParameter");
1867
- } else if (isClassType3(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? unknownType)) {
2048
+ } else if (isClassType4(analysis.types.aliases.get(namespace ? `${namespace}.${base}` : base) ?? unknownType)) {
1868
2049
  add(baseToken, base.length, "class");
1869
2050
  } else {
1870
2051
  add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);
@@ -1873,6 +2054,10 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1873
2054
  }
1874
2055
  }
1875
2056
  }
2057
+ function wordOf(token) {
2058
+ const value = token.value;
2059
+ return typeof value === "string" ? value : void 0;
2060
+ }
1876
2061
  function identifier(analysis, node, parent, add) {
1877
2062
  const name = node.name;
1878
2063
  const as = (type, modifiers = []) => add(node, name.length, type, modifiers);
@@ -1896,6 +2081,19 @@ function identifier(analysis, node, parent, add) {
1896
2081
  case "DeclareClassStatement":
1897
2082
  if (parent.name === node) return as("class", ["declaration"]);
1898
2083
  break;
2084
+ case "ClassDeclaration":
2085
+ if (parent.name === node) return as("class", ["declaration"]);
2086
+ if (parent.superclass === node) return as("class");
2087
+ break;
2088
+ case "ClassMethod":
2089
+ if (parent.name === node) return as("method", ["declaration"]);
2090
+ break;
2091
+ case "ClassField":
2092
+ if (parent.name === node) return as("property", ["declaration"]);
2093
+ break;
2094
+ case "ClassAccessor":
2095
+ if (parent.name === node) return as("property", ["declaration"]);
2096
+ break;
1899
2097
  case "DeclareStatement":
1900
2098
  if (parent.id === node) return as(isFunction(typeOfNode(parent.valueType)) ? "function" : "variable", ["declaration"]);
1901
2099
  break;
@@ -2124,6 +2322,11 @@ function createServer(connection, options = {}) {
2124
2322
  (d) => hover(analyzer.get(d), p.position),
2125
2323
  null
2126
2324
  ));
2325
+ connection.onRequest("luaut/hover", (p) => withDocument(
2326
+ p.textDocument.uri,
2327
+ (d) => hover(analyzer.get(d), p.position, Math.max(0, p.depth ?? 0)),
2328
+ null
2329
+ ));
2127
2330
  connection.onDefinition((p) => withDocument(
2128
2331
  p.textDocument.uri,
2129
2332
  (d) => {
@@ -2213,4 +2416,4 @@ export {
2213
2416
  createServer,
2214
2417
  startServer
2215
2418
  };
2216
- //# sourceMappingURL=chunk-XK3KS3T4.js.map
2419
+ //# sourceMappingURL=chunk-7N7PVZTQ.js.map