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