luaut-language-server 3.0.0 → 4.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 +2 -2
- package/dist/{chunk-X2GHHRWT.js → chunk-XK3KS3T4.js} +381 -42
- package/dist/chunk-XK3KS3T4.js.map +1 -0
- package/dist/cli.cjs +406 -67
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/index.cjs +406 -67
- 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-X2GHHRWT.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
|
-
import { formatType } from "luaut-parser";
|
|
2
|
+
import { formatType, substitute, union } 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": {
|
|
@@ -30,6 +48,20 @@ function membersOf(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
|
30
48
|
}
|
|
31
49
|
case "typeParam":
|
|
32
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) : [];
|
|
33
65
|
default:
|
|
34
66
|
return [];
|
|
35
67
|
}
|
|
@@ -343,11 +375,13 @@ var Analyzer = class {
|
|
|
343
375
|
const script = path ? context.sourceMap?.scriptFor(path) : void 0;
|
|
344
376
|
const libs = script ? [...context.libs, script] : context.libs;
|
|
345
377
|
const globals = script ? [...context.globals, "script"] : context.globals;
|
|
346
|
-
const { program, errors } = parseWithRecovery(source);
|
|
347
|
-
const
|
|
378
|
+
const { program, errors, directives } = parseWithRecovery(source);
|
|
379
|
+
const reportUndeclared = context.libs.length > 0;
|
|
380
|
+
const scopes = analyzeScopes(program, { builtinGlobals: [...globals], reportUndeclared });
|
|
348
381
|
const dependencies = new Map(context.reads);
|
|
349
382
|
const types = analyzeTypes(program, scopes, {
|
|
350
383
|
libs,
|
|
384
|
+
reportUnknownTypes: reportUndeclared,
|
|
351
385
|
resolveModule: (specifier) => {
|
|
352
386
|
if (!path) return void 0;
|
|
353
387
|
const candidates = this.candidatesFor(path, specifier);
|
|
@@ -361,7 +395,7 @@ var Analyzer = class {
|
|
|
361
395
|
return exports;
|
|
362
396
|
}
|
|
363
397
|
});
|
|
364
|
-
return { uri, version, source, program, parseErrors: errors, scopes, types, dependencies, project: context.project };
|
|
398
|
+
return { uri, version, source, program, parseErrors: errors, directives, scopes, types, dependencies, project: context.project };
|
|
365
399
|
}
|
|
366
400
|
exportsOf(path, importing) {
|
|
367
401
|
const key = pathKey(path);
|
|
@@ -450,7 +484,7 @@ function collect(container, out) {
|
|
|
450
484
|
}
|
|
451
485
|
}
|
|
452
486
|
function isSpanlessNode(v) {
|
|
453
|
-
return !!v && typeof v === "object" &&
|
|
487
|
+
return !!v && typeof v === "object" && !Array.isArray(v);
|
|
454
488
|
}
|
|
455
489
|
function pathAt(root, pos, inclusive = false) {
|
|
456
490
|
let best;
|
|
@@ -729,6 +763,7 @@ function patternNamed(target, name) {
|
|
|
729
763
|
|
|
730
764
|
// src/features/diagnostics.ts
|
|
731
765
|
import { DiagnosticSeverity } from "vscode-languageserver";
|
|
766
|
+
import { applyDirectives, UNUSED_EXPECT_ERROR } from "luaut-parser";
|
|
732
767
|
function diagnostics(analysis) {
|
|
733
768
|
const out = [];
|
|
734
769
|
for (const error of analysis.parseErrors) {
|
|
@@ -742,22 +777,32 @@ function diagnostics(analysis) {
|
|
|
742
777
|
message: error.message.replace(/\s*\(\d+:\d+\)$/, "")
|
|
743
778
|
});
|
|
744
779
|
}
|
|
745
|
-
|
|
746
|
-
|
|
780
|
+
const semantic = [
|
|
781
|
+
...analysis.scopes.diagnostics.map((d) => ({
|
|
747
782
|
range: toRange(d.node),
|
|
748
783
|
severity: DiagnosticSeverity.Error,
|
|
749
784
|
source: "luaut",
|
|
750
785
|
code: d.kind,
|
|
751
786
|
message: d.message
|
|
752
|
-
})
|
|
753
|
-
|
|
754
|
-
for (const d of analysis.types.diagnostics) {
|
|
755
|
-
out.push({
|
|
787
|
+
})),
|
|
788
|
+
...analysis.types.diagnostics.map((d) => ({
|
|
756
789
|
range: toRange(d.node),
|
|
757
790
|
severity: DiagnosticSeverity.Error,
|
|
758
791
|
source: "luaut",
|
|
759
792
|
code: "type",
|
|
760
793
|
message: d.message
|
|
794
|
+
}))
|
|
795
|
+
];
|
|
796
|
+
const { kept, unusedExpectErrors } = applyDirectives(analysis.directives, semantic, (d) => d.range.start.line + 1);
|
|
797
|
+
out.push(...kept);
|
|
798
|
+
for (const directive of unusedExpectErrors) {
|
|
799
|
+
const start = toPosition(directive.line, directive.column);
|
|
800
|
+
out.push({
|
|
801
|
+
range: { start, end: { line: start.line, character: start.character + "--@luaut-expect-error".length } },
|
|
802
|
+
severity: DiagnosticSeverity.Error,
|
|
803
|
+
source: "luaut",
|
|
804
|
+
code: "directive",
|
|
805
|
+
message: UNUSED_EXPECT_ERROR
|
|
761
806
|
});
|
|
762
807
|
}
|
|
763
808
|
return out;
|
|
@@ -814,6 +859,27 @@ function describe(analysis, path, index) {
|
|
|
814
859
|
const type2 = property?.type ?? types.typeOf.get(field.value);
|
|
815
860
|
return type2 && `(property) ${name}: ${pretty(type2)}`;
|
|
816
861
|
}
|
|
862
|
+
// `const { name } = t`: a shorthand key *is* the binding it
|
|
863
|
+
// declares, and has the same span, so the cursor can land on
|
|
864
|
+
// either. A renamed key (`{ name: other }`) names the property
|
|
865
|
+
// the value is read from.
|
|
866
|
+
case "ObjectPatternProperty": {
|
|
867
|
+
if (parent.key !== node || parent.computed) break;
|
|
868
|
+
const value = parent.value;
|
|
869
|
+
if (parent.shorthand) return describe(analysis, [...path.slice(0, index), value], index);
|
|
870
|
+
const binding2 = value.type === "IdentifierPattern" ? bindingOfNode(analysis, value) : void 0;
|
|
871
|
+
const type2 = binding2 && types.bindingType.get(binding2.id);
|
|
872
|
+
return type2 && `(property) ${name}: ${pretty(type2)}`;
|
|
873
|
+
}
|
|
874
|
+
// One line of an overload set reads as its own signature.
|
|
875
|
+
// The line the body is on reads as the whole set, which is
|
|
876
|
+
// what the binding says and what the default path gives.
|
|
877
|
+
case "FunctionSignature": {
|
|
878
|
+
if (parent.name !== node) break;
|
|
879
|
+
const own = types.typeOfTypeNode.get(parent);
|
|
880
|
+
if (own) return `function ${name}${pretty(own)}`;
|
|
881
|
+
break;
|
|
882
|
+
}
|
|
817
883
|
case "ImportSpecifier": {
|
|
818
884
|
const alias = types.aliases.get(name);
|
|
819
885
|
const binding2 = bindingOfNode(analysis, identifier2);
|
|
@@ -876,6 +942,11 @@ function describe(analysis, path, index) {
|
|
|
876
942
|
}
|
|
877
943
|
return void 0;
|
|
878
944
|
}
|
|
945
|
+
// `...` — what this function's extra arguments are.
|
|
946
|
+
case "VarargExpression": {
|
|
947
|
+
const type2 = types.typeOf.get(node);
|
|
948
|
+
return type2 ? `(vararg) ...: ${pretty(type2)}` : void 0;
|
|
949
|
+
}
|
|
879
950
|
// Declarations: `const x`, a parameter.
|
|
880
951
|
case "IdentifierPattern":
|
|
881
952
|
case "FunctionParameter":
|
|
@@ -1080,10 +1151,164 @@ function isIdentifier(name) {
|
|
|
1080
1151
|
|
|
1081
1152
|
// src/features/completion.ts
|
|
1082
1153
|
import {
|
|
1083
|
-
CompletionItemKind as
|
|
1154
|
+
CompletionItemKind as CompletionItemKind3,
|
|
1084
1155
|
InsertTextFormat
|
|
1085
1156
|
} from "vscode-languageserver";
|
|
1086
1157
|
import { formatType as formatType4, isClassType as isClassType2 } from "luaut-parser";
|
|
1158
|
+
|
|
1159
|
+
// src/features/autoImport.ts
|
|
1160
|
+
import { readdirSync as readdirSync2 } from "fs";
|
|
1161
|
+
import { dirname as dirname3, join, relative, resolve as resolve3 } from "path";
|
|
1162
|
+
import { CompletionItemKind as CompletionItemKind2 } from "vscode-languageserver";
|
|
1163
|
+
function importItems(analyzer, analysis, typePosition, taken) {
|
|
1164
|
+
const from = pathOfUri(analysis.uri);
|
|
1165
|
+
if (!from) return [];
|
|
1166
|
+
const config = analysis.project.config;
|
|
1167
|
+
const items = [];
|
|
1168
|
+
const offered = /* @__PURE__ */ new Set();
|
|
1169
|
+
for (const file of projectFiles(config?.directory ?? dirname3(from))) {
|
|
1170
|
+
if (samePath(file, from)) continue;
|
|
1171
|
+
const exports = analyzer.exportsAt(file);
|
|
1172
|
+
if (!exports || exports.partial) continue;
|
|
1173
|
+
const names = typePosition ? [...exports.types.keys()] : [...exports.values.keys()];
|
|
1174
|
+
const specifier = specifierFor(from, file, config);
|
|
1175
|
+
for (const name of names) {
|
|
1176
|
+
if (taken.has(name) || offered.has(name)) continue;
|
|
1177
|
+
offered.add(name);
|
|
1178
|
+
const type = typePosition ? exports.types.get(name)?.type : exports.values.get(name);
|
|
1179
|
+
items.push({
|
|
1180
|
+
label: name,
|
|
1181
|
+
kind: typePosition ? CompletionItemKind2.Interface : type && signaturesOf(type).length ? CompletionItemKind2.Function : CompletionItemKind2.Variable,
|
|
1182
|
+
labelDetails: { description: specifier },
|
|
1183
|
+
detail: `import { ${name} } from "${specifier}"`,
|
|
1184
|
+
sortText: `4${name}`,
|
|
1185
|
+
additionalTextEdits: [importEdit(analyzer, analysis, file, name, specifier, typePosition)]
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return items;
|
|
1190
|
+
}
|
|
1191
|
+
function serviceItems(analysis, taken) {
|
|
1192
|
+
const services = analysis.types.aliases.get("Services");
|
|
1193
|
+
if (!services || services.kind !== "object") return [];
|
|
1194
|
+
const at = serviceInsertion(analysis.program.body.statements);
|
|
1195
|
+
const items = [];
|
|
1196
|
+
for (const name of services.properties.keys()) {
|
|
1197
|
+
if (taken.has(name)) continue;
|
|
1198
|
+
const line = `const ${name} = game:GetService("${name}")`;
|
|
1199
|
+
items.push({
|
|
1200
|
+
label: name,
|
|
1201
|
+
kind: CompletionItemKind2.Module,
|
|
1202
|
+
labelDetails: { description: "service" },
|
|
1203
|
+
detail: line,
|
|
1204
|
+
sortText: `5${name}`,
|
|
1205
|
+
additionalTextEdits: [{ range: { start: at.position, end: at.position }, newText: `${line}
|
|
1206
|
+
${at.gap}` }]
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
return items;
|
|
1210
|
+
}
|
|
1211
|
+
function importEdit(analyzer, analysis, file, name, specifier, typePosition) {
|
|
1212
|
+
const statements = analysis.program.body.statements;
|
|
1213
|
+
const imports = statements.filter((s) => s.type === "ImportStatement");
|
|
1214
|
+
const existing = imports.find((s) => !s.namespaceImport && (typePosition || !s.isTypeOnly) && samePathOrUndefined(analyzer.resolveModulePath(analysis.uri, s.source.value), file));
|
|
1215
|
+
if (existing) {
|
|
1216
|
+
const last = existing.specifiers[existing.specifiers.length - 1];
|
|
1217
|
+
if (last) {
|
|
1218
|
+
const at2 = endOf(last);
|
|
1219
|
+
return { range: { start: at2, end: at2 }, newText: `, ${name}` };
|
|
1220
|
+
}
|
|
1221
|
+
if (existing.defaultImport) {
|
|
1222
|
+
const at2 = endOf(existing.defaultImport);
|
|
1223
|
+
return { range: { start: at2, end: at2 }, newText: `, { ${name} }` };
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
const line = `import { ${name} } from "${specifier}"`;
|
|
1227
|
+
const lastImport = imports[imports.length - 1];
|
|
1228
|
+
if (lastImport) {
|
|
1229
|
+
const at2 = { line: lastImport.line.end, character: 0 };
|
|
1230
|
+
return { range: { start: at2, end: at2 }, newText: `${line}
|
|
1231
|
+
` };
|
|
1232
|
+
}
|
|
1233
|
+
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}
|
|
1236
|
+
|
|
1237
|
+
` : `${line}
|
|
1238
|
+
` };
|
|
1239
|
+
}
|
|
1240
|
+
function serviceInsertion(statements) {
|
|
1241
|
+
let last;
|
|
1242
|
+
for (const statement of statements) {
|
|
1243
|
+
if (statement.type !== "ImportStatement" && !isServiceDeclaration(statement)) break;
|
|
1244
|
+
last = statement;
|
|
1245
|
+
}
|
|
1246
|
+
if (last) return { position: { line: last.line.end, character: 0 }, gap: "" };
|
|
1247
|
+
const first = statements[0];
|
|
1248
|
+
return first ? { position: { line: first.line.start - 1, character: 0 }, gap: "\n" } : { position: { line: 0, character: 0 }, gap: "" };
|
|
1249
|
+
}
|
|
1250
|
+
function isServiceDeclaration(statement) {
|
|
1251
|
+
if (statement.type !== "VariableDeclaration") return false;
|
|
1252
|
+
const init = statement.init[0];
|
|
1253
|
+
return init?.type === "MethodCallExpression" && init.method.name === "GetService" && init.object.type === "Identifier" && init.object.name === "game";
|
|
1254
|
+
}
|
|
1255
|
+
function endOf(node) {
|
|
1256
|
+
return { line: node.line.end - 1, character: node.column.end - 1 };
|
|
1257
|
+
}
|
|
1258
|
+
function samePathOrUndefined(a, b) {
|
|
1259
|
+
return a !== void 0 && samePath(a, b);
|
|
1260
|
+
}
|
|
1261
|
+
function specifierFor(from, target, config) {
|
|
1262
|
+
const withoutExtension = (path) => {
|
|
1263
|
+
const bare = path.replace(/\\/g, "/").replace(/\.luaut$/, "");
|
|
1264
|
+
return bare.endsWith("/index") ? bare.slice(0, -"/index".length) : bare;
|
|
1265
|
+
};
|
|
1266
|
+
let relativePath = withoutExtension(relative(dirname3(from), target));
|
|
1267
|
+
if (!relativePath.startsWith(".")) relativePath = `./${relativePath}`;
|
|
1268
|
+
if (!relativePath.startsWith("../") || !config) return relativePath;
|
|
1269
|
+
for (const [pattern, targets] of Object.entries(config.paths)) {
|
|
1270
|
+
const star = pattern.indexOf("*");
|
|
1271
|
+
if (star < 0) continue;
|
|
1272
|
+
for (const targetPattern of targets) {
|
|
1273
|
+
const cut = targetPattern.indexOf("*");
|
|
1274
|
+
if (cut < 0) continue;
|
|
1275
|
+
const head = resolve3(config.baseUrl, targetPattern.slice(0, cut));
|
|
1276
|
+
const rest = relative(head, target);
|
|
1277
|
+
if (rest.startsWith("..") || resolve3(head, rest) !== resolve3(target)) continue;
|
|
1278
|
+
return `${pattern.slice(0, star)}${withoutExtension(rest)}${pattern.slice(star + 1)}`;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
return relativePath;
|
|
1282
|
+
}
|
|
1283
|
+
var FILE_LIMIT = 2e3;
|
|
1284
|
+
var LISTING_TTL = 3e3;
|
|
1285
|
+
var listings = /* @__PURE__ */ new Map();
|
|
1286
|
+
function projectFiles(root) {
|
|
1287
|
+
const cached = listings.get(root);
|
|
1288
|
+
if (cached && Date.now() - cached.at < LISTING_TTL) return cached.files;
|
|
1289
|
+
const files = [];
|
|
1290
|
+
const walk2 = (directory, depth) => {
|
|
1291
|
+
if (files.length >= FILE_LIMIT || depth > 12) return;
|
|
1292
|
+
let entries;
|
|
1293
|
+
try {
|
|
1294
|
+
entries = readdirSync2(directory, { withFileTypes: true });
|
|
1295
|
+
} catch {
|
|
1296
|
+
return;
|
|
1297
|
+
}
|
|
1298
|
+
for (const entry of entries) {
|
|
1299
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
1300
|
+
const path = join(directory, entry.name);
|
|
1301
|
+
if (entry.isDirectory()) walk2(path, depth + 1);
|
|
1302
|
+
else if (entry.name.endsWith(".luaut") && !entry.name.endsWith(".d.luaut")) files.push(path);
|
|
1303
|
+
if (files.length >= FILE_LIMIT) return;
|
|
1304
|
+
}
|
|
1305
|
+
};
|
|
1306
|
+
walk2(root, 0);
|
|
1307
|
+
listings.set(root, { at: Date.now(), files });
|
|
1308
|
+
return files;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// src/features/completion.ts
|
|
1087
1312
|
var PLACEHOLDER = "__luautCompletion__";
|
|
1088
1313
|
var IDENTIFIER_CHAR = /[A-Za-z0-9_]/;
|
|
1089
1314
|
function completion(analyzer, document, position) {
|
|
@@ -1116,28 +1341,58 @@ function completion(analyzer, document, position) {
|
|
|
1116
1341
|
first ??= { analysis, path };
|
|
1117
1342
|
}
|
|
1118
1343
|
if (operator || !first) return [];
|
|
1344
|
+
const keys = objectKeyItems(first.analysis, first.path, source.slice(end));
|
|
1345
|
+
if (keys) return keys;
|
|
1119
1346
|
if (inTypePosition(first.path)) {
|
|
1120
1347
|
const named = [...first.analysis.types.aliases].map(([name, type]) => ({
|
|
1121
1348
|
label: name,
|
|
1122
|
-
kind: isClassType2(type) ?
|
|
1349
|
+
kind: isClassType2(type) ? CompletionItemKind3.Class : CompletionItemKind3.Interface,
|
|
1123
1350
|
detail: isClassType2(type) ? "class" : "type"
|
|
1124
1351
|
}));
|
|
1125
1352
|
const primitives = PRIMITIVES2.map((name) => ({
|
|
1126
1353
|
label: name,
|
|
1127
|
-
kind:
|
|
1354
|
+
kind: CompletionItemKind3.Keyword,
|
|
1128
1355
|
detail: "type"
|
|
1129
1356
|
}));
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1357
|
+
const keywords = TYPE_KEYWORDS.map((name) => ({
|
|
1358
|
+
label: name,
|
|
1359
|
+
kind: CompletionItemKind3.Keyword,
|
|
1360
|
+
sortText: `3${name}`
|
|
1361
|
+
}));
|
|
1362
|
+
const typeNames = new Set(first.analysis.types.aliases.keys());
|
|
1363
|
+
const imported = importItems(analyzer, analyzer.get(document), true, typeNames);
|
|
1364
|
+
return [...named, ...primitives, ...keywords, ...imported];
|
|
1365
|
+
}
|
|
1366
|
+
const taken = /* @__PURE__ */ new Set();
|
|
1367
|
+
for (const binding of first.analysis.scopes.bindings.values()) taken.add(binding.name);
|
|
1368
|
+
const current = analyzer.get(document);
|
|
1369
|
+
return [
|
|
1370
|
+
...valueItems(first.analysis, at),
|
|
1371
|
+
...contextKeywords(source.slice(0, start)),
|
|
1372
|
+
...importItems(analyzer, current, false, taken),
|
|
1373
|
+
...serviceItems(current, taken)
|
|
1374
|
+
];
|
|
1133
1375
|
}
|
|
1134
1376
|
function stringCompletion(analyzer, document, position) {
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1377
|
+
let analysis = analyzer.get(document);
|
|
1378
|
+
let literal = stringAt(analysis, position);
|
|
1379
|
+
if (!literal) {
|
|
1380
|
+
const repaired = repairedStrings(document, position);
|
|
1381
|
+
for (const text of repaired) {
|
|
1382
|
+
const candidate = analyzer.analyze(document.uri, -1, text);
|
|
1383
|
+
literal = stringAt(candidate, position);
|
|
1384
|
+
if (literal) {
|
|
1385
|
+
analysis = candidate;
|
|
1386
|
+
break;
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
if (!literal) return repaired.length ? [] : void 0;
|
|
1390
|
+
}
|
|
1139
1391
|
const expected = analysis.types.expectedTypeOf.get(literal);
|
|
1140
|
-
const values =
|
|
1392
|
+
const values = [.../* @__PURE__ */ new Set([
|
|
1393
|
+
...stringLiterals(expected, analysis.types.aliases),
|
|
1394
|
+
...indexKeys(analysis, position, literal)
|
|
1395
|
+
])];
|
|
1141
1396
|
if (!values.length) return [];
|
|
1142
1397
|
const line = literal.line.start - 1;
|
|
1143
1398
|
const range = literal.line.start === literal.line.end ? {
|
|
@@ -1146,10 +1401,81 @@ function stringCompletion(analyzer, document, position) {
|
|
|
1146
1401
|
} : void 0;
|
|
1147
1402
|
return values.map((value) => ({
|
|
1148
1403
|
label: value,
|
|
1149
|
-
kind:
|
|
1404
|
+
kind: CompletionItemKind3.Constant,
|
|
1150
1405
|
...range ? { textEdit: { range, newText: value } } : {}
|
|
1151
1406
|
}));
|
|
1152
1407
|
}
|
|
1408
|
+
function stringAt(analysis, position) {
|
|
1409
|
+
const path = pathAt(analysis.program, position, false);
|
|
1410
|
+
return [...path].reverse().find((n) => n.type === "StringLiteral" || n.type === "TypeLiteralString");
|
|
1411
|
+
}
|
|
1412
|
+
function objectKeyItems(analysis, path, after) {
|
|
1413
|
+
const index = path.findLastIndex(
|
|
1414
|
+
(n) => n.type === "Identifier" && n.name === PLACEHOLDER
|
|
1415
|
+
);
|
|
1416
|
+
const literal = index > 0 ? path[index - 1] : void 0;
|
|
1417
|
+
if (literal?.type !== "TableExpression") return void 0;
|
|
1418
|
+
const fields = literal.fields;
|
|
1419
|
+
const atKey = fields.some((f) => f.type === "TableFieldShorthand" && f.name?.name === PLACEHOLDER);
|
|
1420
|
+
if (!atKey) return void 0;
|
|
1421
|
+
let expected = analysis.types.expectedTypeOf.get(literal);
|
|
1422
|
+
for (let up = index - 2; expected === void 0 && up >= 0; up--) {
|
|
1423
|
+
const outer = path[up];
|
|
1424
|
+
if (outer.type !== "AsConstExpression" && outer.type !== "ParenthesizedExpression") break;
|
|
1425
|
+
expected = analysis.types.expectedTypeOf.get(outer);
|
|
1426
|
+
}
|
|
1427
|
+
const members = membersOf(expected, analysis.types.aliases);
|
|
1428
|
+
if (!members.length) return void 0;
|
|
1429
|
+
const written = /* @__PURE__ */ new Set();
|
|
1430
|
+
for (const field of fields) {
|
|
1431
|
+
if (field.type === "TableFieldNamed") written.add(field.key?.name ?? field.key?.value ?? "");
|
|
1432
|
+
else if (field.type === "TableFieldShorthand" && field.name?.name !== PLACEHOLDER) written.add(field.name.name);
|
|
1433
|
+
}
|
|
1434
|
+
const colon = /^\s*:/.test(after);
|
|
1435
|
+
return members.filter((member) => !written.has(member.name)).map((member) => ({
|
|
1436
|
+
label: member.name,
|
|
1437
|
+
kind: CompletionItemKind3.Field,
|
|
1438
|
+
detail: `${member.property.optional ? "?" : ""}: ${formatType4(member.property.type)}`,
|
|
1439
|
+
insertText: colon ? member.name : `${member.name}: `
|
|
1440
|
+
}));
|
|
1441
|
+
}
|
|
1442
|
+
function indexKeys(analysis, position, literal) {
|
|
1443
|
+
const path = pathAt(analysis.program, position, false);
|
|
1444
|
+
const at = path.indexOf(literal);
|
|
1445
|
+
const parent = at > 0 ? path[at - 1] : void 0;
|
|
1446
|
+
if (!parent) return [];
|
|
1447
|
+
let indexed;
|
|
1448
|
+
if (parent.type === "IndexedAccessTypeNode" && parent.indexType === literal) {
|
|
1449
|
+
indexed = analysis.types.typeOfTypeNode.get(parent.objectType);
|
|
1450
|
+
} else if (parent.type === "IndexExpression" && parent.index === literal) {
|
|
1451
|
+
indexed = withoutNil(analysis.types.typeOf.get(parent.object));
|
|
1452
|
+
}
|
|
1453
|
+
return membersOf(indexed, analysis.types.aliases).map((member) => member.name);
|
|
1454
|
+
}
|
|
1455
|
+
function repairedStrings(document, position) {
|
|
1456
|
+
const source = document.getText();
|
|
1457
|
+
const offset = document.offsetAt(position);
|
|
1458
|
+
const lineStart = offset - position.character;
|
|
1459
|
+
const lineEndIndex = source.indexOf("\n", offset);
|
|
1460
|
+
const lineEnd = lineEndIndex < 0 ? source.length : lineEndIndex;
|
|
1461
|
+
const before = source.slice(lineStart, offset);
|
|
1462
|
+
let quote;
|
|
1463
|
+
for (let i = 0; i < before.length; i++) {
|
|
1464
|
+
const ch = before[i];
|
|
1465
|
+
if (quote) {
|
|
1466
|
+
if (ch === "\\") i++;
|
|
1467
|
+
else if (ch === quote) quote = void 0;
|
|
1468
|
+
} else if (ch === '"' || ch === "'") {
|
|
1469
|
+
quote = ch;
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
if (!quote) return [];
|
|
1473
|
+
let rest = source.slice(offset, lineEnd).replace(/\r$/, "");
|
|
1474
|
+
if (!rest.includes(quote)) rest += quote;
|
|
1475
|
+
const line = before + rest;
|
|
1476
|
+
const endings = ["", " then end", " do end", ")", ") then end", "]"];
|
|
1477
|
+
return endings.map((ending) => source.slice(0, lineStart) + line + ending + source.slice(lineEnd));
|
|
1478
|
+
}
|
|
1153
1479
|
function stringLiterals(type, aliases, seen = /* @__PURE__ */ new Set()) {
|
|
1154
1480
|
if (!type || seen.has(type)) return [];
|
|
1155
1481
|
seen.add(type);
|
|
@@ -1181,27 +1507,29 @@ function memberOperator(source, wordStart) {
|
|
|
1181
1507
|
}
|
|
1182
1508
|
function memberItems(analysis, access) {
|
|
1183
1509
|
const object = access.object;
|
|
1184
|
-
const type = analysis.types.typeOf.get(object);
|
|
1510
|
+
const type = withoutNil(analysis.types.typeOf.get(object));
|
|
1185
1511
|
const colon = access.type === "MethodCallExpression";
|
|
1186
|
-
if (
|
|
1187
|
-
if (!colon) return [];
|
|
1188
|
-
const id = analysis.scopes.globalsByName.get("string");
|
|
1189
|
-
const library = id === void 0 ? void 0 : analysis.types.bindingType.get(id);
|
|
1190
|
-
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));
|
|
1191
|
-
}
|
|
1512
|
+
if (isMethodOnly(type) && !colon) return [];
|
|
1192
1513
|
return membersOf(type, analysis.types.aliases).filter((member) => colon ? member.isMethod : true).map((member) => memberItem(member.name, member.property.type, member.property.readonly));
|
|
1193
1514
|
}
|
|
1194
|
-
function
|
|
1515
|
+
function withoutNil(type) {
|
|
1516
|
+
if (type?.kind !== "union") return type;
|
|
1517
|
+
const kept = type.types.filter((t) => !(t.kind === "primitive" && t.name === "nil"));
|
|
1518
|
+
return kept.length === 1 ? kept[0] : { ...type, types: kept };
|
|
1519
|
+
}
|
|
1520
|
+
function isMethodOnly(type) {
|
|
1195
1521
|
if (!type) return false;
|
|
1196
1522
|
switch (type.kind) {
|
|
1523
|
+
case "array":
|
|
1524
|
+
case "tuple":
|
|
1525
|
+
case "templateLiteral":
|
|
1526
|
+
return true;
|
|
1197
1527
|
case "primitive":
|
|
1198
1528
|
return type.name === "string";
|
|
1199
1529
|
case "literal":
|
|
1200
1530
|
return typeof type.value === "string";
|
|
1201
|
-
case "templateLiteral":
|
|
1202
|
-
return true;
|
|
1203
1531
|
case "union":
|
|
1204
|
-
return type.types.length > 0 && type.types.every(
|
|
1532
|
+
return type.types.length > 0 && type.types.every(isMethodOnly);
|
|
1205
1533
|
default:
|
|
1206
1534
|
return false;
|
|
1207
1535
|
}
|
|
@@ -1225,7 +1553,7 @@ function valueItems(analysis, at) {
|
|
|
1225
1553
|
});
|
|
1226
1554
|
}
|
|
1227
1555
|
for (const keyword2 of KEYWORDS) {
|
|
1228
|
-
items.push({ label: keyword2, kind:
|
|
1556
|
+
items.push({ label: keyword2, kind: CompletionItemKind3.Keyword, sortText: `3${keyword2}` });
|
|
1229
1557
|
}
|
|
1230
1558
|
return items;
|
|
1231
1559
|
}
|
|
@@ -1234,7 +1562,7 @@ function memberItem(name, type, readonly) {
|
|
|
1234
1562
|
if (signatures.length) {
|
|
1235
1563
|
return {
|
|
1236
1564
|
label: name,
|
|
1237
|
-
kind:
|
|
1565
|
+
kind: CompletionItemKind3.Method,
|
|
1238
1566
|
detail: signatureLabel(signatures[0]).label,
|
|
1239
1567
|
insertText: `${name}($0)`,
|
|
1240
1568
|
insertTextFormat: InsertTextFormat.Snippet
|
|
@@ -1242,14 +1570,14 @@ function memberItem(name, type, readonly) {
|
|
|
1242
1570
|
}
|
|
1243
1571
|
return {
|
|
1244
1572
|
label: name,
|
|
1245
|
-
kind:
|
|
1573
|
+
kind: CompletionItemKind3.Field,
|
|
1246
1574
|
detail: `${readonly ? "readonly " : ""}${formatType4(type)}`
|
|
1247
1575
|
};
|
|
1248
1576
|
}
|
|
1249
1577
|
function kindOf(type, bindingKind) {
|
|
1250
|
-
if (type && signaturesOf(type).length) return
|
|
1251
|
-
if (bindingKind === "param" || bindingKind === "self") return
|
|
1252
|
-
return
|
|
1578
|
+
if (type && signaturesOf(type).length) return CompletionItemKind3.Function;
|
|
1579
|
+
if (bindingKind === "param" || bindingKind === "self") return CompletionItemKind3.Variable;
|
|
1580
|
+
return CompletionItemKind3.Variable;
|
|
1253
1581
|
}
|
|
1254
1582
|
function inTypePosition(path) {
|
|
1255
1583
|
return path.some(
|
|
@@ -1267,6 +1595,17 @@ var PRIMITIVES2 = [
|
|
|
1267
1595
|
"thread",
|
|
1268
1596
|
"buffer"
|
|
1269
1597
|
];
|
|
1598
|
+
var TYPE_KEYWORDS = ["keyof", "typeof", "infer", "extends"];
|
|
1599
|
+
function contextKeywords(before) {
|
|
1600
|
+
const keyword2 = (name) => ({
|
|
1601
|
+
label: name,
|
|
1602
|
+
kind: CompletionItemKind3.Keyword,
|
|
1603
|
+
sortText: `3${name}`
|
|
1604
|
+
});
|
|
1605
|
+
if (/<\s*[A-Za-z_]\w*(?:\s*,\s*[A-Za-z_]\w*)*\s+$/.test(before)) return [keyword2("extends")];
|
|
1606
|
+
if (/[)\]}"'`\w][^\S\n]+$/.test(before)) return [keyword2("as"), keyword2("satisfies")];
|
|
1607
|
+
return [];
|
|
1608
|
+
}
|
|
1270
1609
|
var KEYWORDS = [
|
|
1271
1610
|
"const",
|
|
1272
1611
|
"let",
|
|
@@ -1766,7 +2105,7 @@ function createServer(connection, options = {}) {
|
|
|
1766
2105
|
severity: DiagnosticSeverity2.Information,
|
|
1767
2106
|
source: "luaut",
|
|
1768
2107
|
code: "no-config",
|
|
1769
|
-
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
|
|
2108
|
+
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.'
|
|
1770
2109
|
}];
|
|
1771
2110
|
};
|
|
1772
2111
|
documents.onDidOpen(publishAll);
|
|
@@ -1874,4 +2213,4 @@ export {
|
|
|
1874
2213
|
createServer,
|
|
1875
2214
|
startServer
|
|
1876
2215
|
};
|
|
1877
|
-
//# sourceMappingURL=chunk-
|
|
2216
|
+
//# sourceMappingURL=chunk-XK3KS3T4.js.map
|