luaut-language-server 3.1.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
@@ -1,5 +1,5 @@
1
1
  // src/features/members.ts
2
- import { formatType } from "luaut-parser";
2
+ import { formatType, substitute, union } from "luaut-parser";
3
3
  function literalKeys(key, aliases) {
4
4
  if (!key) return [];
5
5
  const resolved = key.kind === "genericRef" ? aliases.get(key.name) : key;
@@ -48,13 +48,28 @@ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
48
48
  }
49
49
  case "typeParam":
50
50
  return membersOf(type.constraint, aliases, seen);
51
+ // An array and a string answer to the methods the language gives them
52
+ // — `names:filter(f)`, `text:trim()`. They are written in the parser's
53
+ // prelude as `ArrayMethods<T>` and `StringMethods`, so the element
54
+ // type goes in where `T` stands.
55
+ case "array":
56
+ case "tuple": {
57
+ const element = type.kind === "array" ? type.element : union(type.elements);
58
+ const methods = aliases.get("ArrayMethods");
59
+ return methods ? membersOf(substitute(methods, /* @__PURE__ */ new Map([["T", element]])), aliases, seen) : [];
60
+ }
61
+ case "primitive":
62
+ return type.name === "string" ? membersOf(aliases.get("StringMethods"), aliases, seen) : [];
63
+ case "literal":
64
+ return type.base === "string" ? membersOf(aliases.get("StringMethods"), aliases, seen) : [];
51
65
  default:
52
66
  return [];
53
67
  }
54
68
  }
55
69
  function takesSelf(type) {
56
70
  for (const signature of signaturesOf(type)) {
57
- if (signature.params[0]?.name === "self") return true;
71
+ const first = signature.params[0]?.name;
72
+ if (first === "self" || first === "this") return true;
58
73
  }
59
74
  return false;
60
75
  }
@@ -75,7 +90,7 @@ function signatureLabel(signature) {
75
90
  });
76
91
  const generics = signature.typeParams?.length ? `<${signature.typeParams.join(", ")}>` : "";
77
92
  const varargs = signature.varargs ? [`...: ${formatType(signature.varargs)}`] : [];
78
- const label = `${generics}(${[...parameters, ...varargs].join(", ")}) -> ${formatType(signature.returns)}`;
93
+ const label = `${generics}(${[...parameters, ...varargs].join(", ")}) => ${formatType(signature.returns)}`;
79
94
  return { label, parameters };
80
95
  }
81
96
 
@@ -683,7 +698,7 @@ function exportDeclaration(analyzer, module, name, seen = /* @__PURE__ */ new Se
683
698
  break;
684
699
  case "ExportStatement": {
685
700
  const declaration = statement.declaration;
686
- if (declaration.type === "FunctionDeclaration") {
701
+ if (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") {
687
702
  if (declaration.name.name === name) return here(declaration.name);
688
703
  } else {
689
704
  for (const target of declaration.names) {
@@ -797,17 +812,101 @@ function diagnostics(analysis) {
797
812
  // src/features/hover.ts
798
813
  import {
799
814
  formatType as formatType3,
800
- isClassType
815
+ isClassType as isClassType2
801
816
  } from "luaut-parser";
802
- 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) {
803
881
  const path = pathAt(analysis.program, position, true);
804
882
  for (let i = path.length - 1; i >= 0; i--) {
805
883
  if (UNNAMED.has(path[i].type)) return null;
806
- const text = describe(analysis, path, i);
807
- 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
+ };
808
893
  }
809
894
  return null;
810
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
+ };
811
910
  var UNNAMED = /* @__PURE__ */ new Set([
812
911
  "BinaryExpression",
813
912
  "UnaryExpression",
@@ -840,8 +939,8 @@ function describe(analysis, path, index) {
840
939
  case "TableExpression": {
841
940
  const field = fieldWithKey(parent, node);
842
941
  if (!field) break;
843
- const objectType = types.typeOf.get(parent);
844
- 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;
845
944
  const type2 = property?.type ?? types.typeOf.get(field.value);
846
945
  return type2 && `(property) ${name}: ${pretty(type2)}`;
847
946
  }
@@ -889,6 +988,19 @@ function describe(analysis, path, index) {
889
988
  case "DeclareClassStatement":
890
989
  if (parent.name === node) return classText(analysis, name);
891
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;
892
1004
  case "TableTypeProperty":
893
1005
  if (parent.key === node) {
894
1006
  const type2 = typeOfNode(parent.valueType);
@@ -939,7 +1051,7 @@ function describe(analysis, path, index) {
939
1051
  case "TypedIdentifier": {
940
1052
  const binding = bindingOfNode(analysis, node);
941
1053
  const type2 = binding && types.bindingType.get(binding.id);
942
- return type2 ? bindingText(binding, type2) : void 0;
1054
+ return type2 ? bindingText(binding, type2, asWritten(node.typeAnnotation)) : void 0;
943
1055
  }
944
1056
  // A type written by name: `number`, `Shape`, `Partial<User>`, or a type
945
1057
  // parameter in scope.
@@ -953,7 +1065,7 @@ function describe(analysis, path, index) {
953
1065
  if (!node.typeArguments.length) {
954
1066
  const qualified = node.namespace ? `${node.namespace}.${base}` : base;
955
1067
  const alias = types.aliases.get(qualified);
956
- if (alias && isClassType(alias)) return classText(analysis, qualified);
1068
+ if (alias && isClassType2(alias)) return classText(analysis, qualified);
957
1069
  if (alias) return `type ${qualified} = ${pretty(alias)}`;
958
1070
  }
959
1071
  const type2 = typeOfNode(node);
@@ -985,16 +1097,25 @@ function declareText(analysis, statement) {
985
1097
  }
986
1098
  function classText(analysis, name) {
987
1099
  const type = analysis.types.aliases.get(name);
988
- if (!type || !isClassType(type)) return void 0;
1100
+ if (!type || !isClassType2(type)) return void 0;
989
1101
  const superclass = type.class.superclass;
990
1102
  const inherited = superclass ? analysis.types.aliases.get(superclass) : void 0;
991
- const own = [...type.properties].filter(([key, property]) => inherited?.kind !== "object" || inherited.properties.get(key) !== property);
992
- const head = `declare class ${name}${superclass ? ` extends ${superclass}` : ""}`;
993
- if (!own.length) return `${head} {}`;
994
- const lines = own.map(([key, property]) => ` ${property.readonly ? "readonly " : ""}${key}${property.optional ? "?" : ""}: ${formatType3(property.type)},`);
995
- 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} {
996
1111
  ${lines.join("\n")}
997
- }`;
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
+ });
998
1119
  }
999
1120
  function typeParameterText(analysis, parameter) {
1000
1121
  return `(type parameter) ${typeParameterSignature(analysis, parameter)}`;
@@ -1036,7 +1157,27 @@ function fieldWithKey(table, key) {
1036
1157
  const fields = table.fields;
1037
1158
  return fields.find((f) => f.type === "TableFieldNamed" && f.key === key);
1038
1159
  }
1039
- 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) {
1040
1181
  const flat = formatType3(type);
1041
1182
  if (flat.length <= 80) return flat;
1042
1183
  if (type.kind === "object") {
@@ -1055,11 +1196,11 @@ ${lines.join("\n")}
1055
1196
  }
1056
1197
  return flat;
1057
1198
  }
1058
- function bindingText(binding, type) {
1199
+ function bindingText(binding, type, written) {
1059
1200
  if (binding.declaredBy === "function" && type.kind === "function") {
1060
- return `function ${binding.name}${formatType3(type)}`;
1201
+ return `function ${binding.name}${pretty(type)}`;
1061
1202
  }
1062
- return `${keyword(binding)} ${binding.name}: ${pretty(type)}`;
1203
+ return `${keyword(binding)} ${binding.name}: ${pretty(type, written)}`;
1063
1204
  }
1064
1205
  function keyword(binding) {
1065
1206
  if (binding.kind === "param" || binding.kind === "self") return "(parameter)";
@@ -1140,7 +1281,7 @@ import {
1140
1281
  CompletionItemKind as CompletionItemKind3,
1141
1282
  InsertTextFormat
1142
1283
  } from "vscode-languageserver";
1143
- import { formatType as formatType4, isClassType as isClassType2 } from "luaut-parser";
1284
+ import { formatType as formatType4, isClassType as isClassType3 } from "luaut-parser";
1144
1285
 
1145
1286
  // src/features/autoImport.ts
1146
1287
  import { readdirSync as readdirSync2 } from "fs";
@@ -1177,7 +1318,7 @@ function importItems(analyzer, analysis, typePosition, taken) {
1177
1318
  function serviceItems(analysis, taken) {
1178
1319
  const services = analysis.types.aliases.get("Services");
1179
1320
  if (!services || services.kind !== "object") return [];
1180
- const at = serviceInsertion(analysis.program.body.statements);
1321
+ const at2 = serviceInsertion(analysis.program.body.statements);
1181
1322
  const items = [];
1182
1323
  for (const name of services.properties.keys()) {
1183
1324
  if (taken.has(name)) continue;
@@ -1188,8 +1329,8 @@ function serviceItems(analysis, taken) {
1188
1329
  labelDetails: { description: "service" },
1189
1330
  detail: line,
1190
1331
  sortText: `5${name}`,
1191
- additionalTextEdits: [{ range: { start: at.position, end: at.position }, newText: `${line}
1192
- ${at.gap}` }]
1332
+ additionalTextEdits: [{ range: { start: at2.position, end: at2.position }, newText: `${line}
1333
+ ${at2.gap}` }]
1193
1334
  });
1194
1335
  }
1195
1336
  return items;
@@ -1201,24 +1342,24 @@ function importEdit(analyzer, analysis, file, name, specifier, typePosition) {
1201
1342
  if (existing) {
1202
1343
  const last = existing.specifiers[existing.specifiers.length - 1];
1203
1344
  if (last) {
1204
- const at2 = endOf(last);
1205
- return { range: { start: at2, end: at2 }, newText: `, ${name}` };
1345
+ const at3 = endOf(last);
1346
+ return { range: { start: at3, end: at3 }, newText: `, ${name}` };
1206
1347
  }
1207
1348
  if (existing.defaultImport) {
1208
- const at2 = endOf(existing.defaultImport);
1209
- return { range: { start: at2, end: at2 }, newText: `, { ${name} }` };
1349
+ const at3 = endOf(existing.defaultImport);
1350
+ return { range: { start: at3, end: at3 }, newText: `, { ${name} }` };
1210
1351
  }
1211
1352
  }
1212
1353
  const line = `import { ${name} } from "${specifier}"`;
1213
1354
  const lastImport = imports[imports.length - 1];
1214
1355
  if (lastImport) {
1215
- const at2 = { line: lastImport.line.end, character: 0 };
1216
- 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}
1217
1358
  ` };
1218
1359
  }
1219
1360
  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}
1361
+ const at2 = { line: first ? first.line.start - 1 : 0, character: 0 };
1362
+ return { range: { start: at2, end: at2 }, newText: first ? `${line}
1222
1363
 
1223
1364
  ` : `${line}
1224
1365
  ` };
@@ -1311,12 +1452,12 @@ function completion(analyzer, document, position) {
1311
1452
  const operator = memberOperator(source, start);
1312
1453
  const alreadyCalled = /^\s*\(/.test(source.slice(end));
1313
1454
  const standIns = operator === ":" ? [alreadyCalled ? PLACEHOLDER : `${PLACEHOLDER}()`] : operator === "." && !alreadyCalled ? [PLACEHOLDER, `${PLACEHOLDER}()`] : [PLACEHOLDER];
1314
- const at = { line: position.line, character: position.character - (offset - start) };
1455
+ const at2 = { line: position.line, character: position.character - (offset - start) };
1315
1456
  let first;
1316
1457
  for (const standIn of standIns) {
1317
1458
  const patched = source.slice(0, start) + standIn + source.slice(end);
1318
1459
  const analysis = analyzer.analyze(document.uri, -1, patched);
1319
- const path = pathAt(analysis.program, at, true);
1460
+ const path = pathAt(analysis.program, at2, true);
1320
1461
  const index = path.findLastIndex(
1321
1462
  (n) => n.type === "Identifier" && n.name === PLACEHOLDER
1322
1463
  );
@@ -1332,8 +1473,8 @@ function completion(analyzer, document, position) {
1332
1473
  if (inTypePosition(first.path)) {
1333
1474
  const named = [...first.analysis.types.aliases].map(([name, type]) => ({
1334
1475
  label: name,
1335
- kind: isClassType2(type) ? CompletionItemKind3.Class : CompletionItemKind3.Interface,
1336
- detail: isClassType2(type) ? "class" : "type"
1476
+ kind: isClassType3(type) ? CompletionItemKind3.Class : CompletionItemKind3.Interface,
1477
+ detail: isClassType3(type) ? "class" : "type"
1337
1478
  }));
1338
1479
  const primitives = PRIMITIVES2.map((name) => ({
1339
1480
  label: name,
@@ -1353,7 +1494,7 @@ function completion(analyzer, document, position) {
1353
1494
  for (const binding of first.analysis.scopes.bindings.values()) taken.add(binding.name);
1354
1495
  const current = analyzer.get(document);
1355
1496
  return [
1356
- ...valueItems(first.analysis, at),
1497
+ ...valueItems(first.analysis, at2, insideFunction(first.path)),
1357
1498
  ...contextKeywords(source.slice(0, start)),
1358
1499
  ...importItems(analyzer, current, false, taken),
1359
1500
  ...serviceItems(current, taken)
@@ -1427,8 +1568,8 @@ function objectKeyItems(analysis, path, after) {
1427
1568
  }
1428
1569
  function indexKeys(analysis, position, literal) {
1429
1570
  const path = pathAt(analysis.program, position, false);
1430
- const at = path.indexOf(literal);
1431
- 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;
1432
1573
  if (!parent) return [];
1433
1574
  let indexed;
1434
1575
  if (parent.type === "IndexedAccessTypeNode" && parent.indexType === literal) {
@@ -1495,12 +1636,7 @@ function memberItems(analysis, access) {
1495
1636
  const object = access.object;
1496
1637
  const type = withoutNil(analysis.types.typeOf.get(object));
1497
1638
  const colon = access.type === "MethodCallExpression";
1498
- if (isStringLike(type)) {
1499
- if (!colon) return [];
1500
- const id = analysis.scopes.globalsByName.get("string");
1501
- const library = id === void 0 ? void 0 : analysis.types.bindingType.get(id);
1502
- return membersOf(library, analysis.types.aliases).filter((member) => signaturesOf(member.property.type).length > 0).map((member) => memberItem(member.name, member.property.type, member.property.readonly));
1503
- }
1639
+ if (isMethodOnly(type) && !colon) return [];
1504
1640
  return membersOf(type, analysis.types.aliases).filter((member) => colon ? member.isMethod : true).map((member) => memberItem(member.name, member.property.type, member.property.readonly));
1505
1641
  }
1506
1642
  function withoutNil(type) {
@@ -1508,29 +1644,35 @@ function withoutNil(type) {
1508
1644
  const kept = type.types.filter((t) => !(t.kind === "primitive" && t.name === "nil"));
1509
1645
  return kept.length === 1 ? kept[0] : { ...type, types: kept };
1510
1646
  }
1511
- function isStringLike(type) {
1647
+ function isMethodOnly(type) {
1512
1648
  if (!type) return false;
1513
1649
  switch (type.kind) {
1650
+ case "array":
1651
+ case "tuple":
1652
+ case "templateLiteral":
1653
+ return true;
1514
1654
  case "primitive":
1515
1655
  return type.name === "string";
1516
1656
  case "literal":
1517
1657
  return typeof type.value === "string";
1518
- case "templateLiteral":
1519
- return true;
1520
1658
  case "union":
1521
- return type.types.length > 0 && type.types.every(isStringLike);
1659
+ return type.types.length > 0 && type.types.every(isMethodOnly);
1522
1660
  default:
1523
1661
  return false;
1524
1662
  }
1525
1663
  }
1526
- 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) {
1527
1668
  const items = [];
1528
1669
  const seen = /* @__PURE__ */ new Set();
1529
1670
  for (const binding of analysis.scopes.bindings.values()) {
1530
1671
  if (binding.name === PLACEHOLDER || seen.has(binding.name)) continue;
1531
1672
  if (binding.declaredBy === "type") continue;
1532
1673
  const declaration = binding.declarationNode;
1533
- 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;
1534
1676
  seen.add(binding.name);
1535
1677
  const type = analysis.types.bindingType.get(binding.id);
1536
1678
  items.push({
@@ -1668,9 +1810,9 @@ function helpAt(analysis, position) {
1668
1810
  function methodType(analysis, call) {
1669
1811
  const object = call.object;
1670
1812
  const method = call.method;
1671
- const objectType = analysis.types.typeOf.get(object);
1672
- if (!objectType) return void 0;
1673
- 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);
1674
1816
  }
1675
1817
  function memberType(type, name, analysis) {
1676
1818
  if (type.kind === "object") return type.properties.get(name)?.type;
@@ -1722,6 +1864,15 @@ function documentSymbols(analysis) {
1722
1864
  }
1723
1865
  break;
1724
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
+ }
1725
1876
  case "DeclareClassStatement": {
1726
1877
  const name = node.name.name;
1727
1878
  const superclass = node.superclass?.base;
@@ -1739,6 +1890,23 @@ function documentSymbols(analysis) {
1739
1890
  });
1740
1891
  return out;
1741
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
+ }
1742
1910
  function functionName(node) {
1743
1911
  const named = node;
1744
1912
  if (typeof named.name === "string") return named.name;
@@ -1765,7 +1933,7 @@ function symbol(name, kind, node, detail) {
1765
1933
  }
1766
1934
 
1767
1935
  // src/features/semanticTokens.ts
1768
- import { tokenize, isClassType as isClassType3, unknownType } from "luaut-parser";
1936
+ import { tokenize, isClassType as isClassType4, unknownType } from "luaut-parser";
1769
1937
  var TOKEN_TYPES = [
1770
1938
  "namespace",
1771
1939
  "type",
@@ -1795,15 +1963,17 @@ var SOFT_KEYWORDS = /* @__PURE__ */ new Set([
1795
1963
  "asserts",
1796
1964
  "satisfies",
1797
1965
  "typeof",
1798
- "default"
1966
+ "default",
1967
+ "new",
1968
+ "super"
1799
1969
  ]);
1800
1970
  var CONTROL_KEYWORDS = /* @__PURE__ */ new Set(["default"]);
1801
1971
  var PRIMITIVES3 = /* @__PURE__ */ new Set(["any", "unknown", "never", "nil", "boolean", "number", "string", "thread", "buffer"]);
1802
1972
  function semanticTokens(analysis) {
1803
1973
  const entries = /* @__PURE__ */ new Map();
1804
- const add = (at, length, type, modifiers = []) => {
1805
- const line = at.line.start - 1;
1806
- 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;
1807
1977
  const key = `${line}:${character}`;
1808
1978
  if (!entries.has(key)) entries.set(key, { line, character, length, type, modifiers });
1809
1979
  };
@@ -1844,6 +2014,25 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1844
2014
  add(node, name.length, valueKind(analysis, binding), modifiersOf(binding, true));
1845
2015
  return;
1846
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
+ }
1847
2036
  case "TypeReference": {
1848
2037
  const base = node.base;
1849
2038
  const namespace = node.namespace;
@@ -1853,7 +2042,7 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1853
2042
  if (!baseToken) return;
1854
2043
  if (!namespace && typeParameterInScope2(ancestors, base)) {
1855
2044
  add(baseToken, base.length, "typeParameter");
1856
- } 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)) {
1857
2046
  add(baseToken, base.length, "class");
1858
2047
  } else {
1859
2048
  add(baseToken, base.length, "type", PRIMITIVES3.has(base) ? ["defaultLibrary"] : []);
@@ -1862,6 +2051,10 @@ function classify(analysis, spanned, ancestors, identifiers, add) {
1862
2051
  }
1863
2052
  }
1864
2053
  }
2054
+ function wordOf(token) {
2055
+ const value = token.value;
2056
+ return typeof value === "string" ? value : void 0;
2057
+ }
1865
2058
  function identifier(analysis, node, parent, add) {
1866
2059
  const name = node.name;
1867
2060
  const as = (type, modifiers = []) => add(node, name.length, type, modifiers);
@@ -1885,6 +2078,19 @@ function identifier(analysis, node, parent, add) {
1885
2078
  case "DeclareClassStatement":
1886
2079
  if (parent.name === node) return as("class", ["declaration"]);
1887
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;
1888
2094
  case "DeclareStatement":
1889
2095
  if (parent.id === node) return as(isFunction(typeOfNode(parent.valueType)) ? "function" : "variable", ["declaration"]);
1890
2096
  break;
@@ -2094,7 +2300,7 @@ function createServer(connection, options = {}) {
2094
2300
  severity: DiagnosticSeverity2.Information,
2095
2301
  source: "luaut",
2096
2302
  code: "no-config",
2097
- message: 'No luaut.config.json applies to this file, so no types are loaded \u2014 not even `print`. Add one to this folder or a folder above, such as { "types": ["luau"], "paths": {}, "sourceMap": null }'
2303
+ message: 'No luaut.config.json applies to this file, so no types are loaded \u2014 not even `print`. Add one to this folder or a folder above: { "types": [], "paths": {}, "sourceMap": null }, listing in `types` the type libraries the project has installed.'
2098
2304
  }];
2099
2305
  };
2100
2306
  documents.onDidOpen(publishAll);
@@ -2113,6 +2319,11 @@ function createServer(connection, options = {}) {
2113
2319
  (d) => hover(analyzer.get(d), p.position),
2114
2320
  null
2115
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
+ ));
2116
2327
  connection.onDefinition((p) => withDocument(
2117
2328
  p.textDocument.uri,
2118
2329
  (d) => {
@@ -2202,4 +2413,4 @@ export {
2202
2413
  createServer,
2203
2414
  startServer
2204
2415
  };
2205
- //# sourceMappingURL=chunk-SCZTSX3Y.js.map
2416
+ //# sourceMappingURL=chunk-GHAR7NOH.js.map