@stateward/language-server 1.0.1-rc.1 → 1.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 +16 -0
- package/dist/adoption.d.ts +5 -0
- package/dist/adoption.js +4 -2
- package/dist/fix-all.d.ts +1 -1
- package/dist/fix-all.js +14 -5
- package/dist/guarantees.d.ts +46 -0
- package/dist/guarantees.js +180 -0
- package/dist/server.d.ts +5 -1
- package/dist/server.js +59 -23
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -11,3 +11,19 @@ npm install --save-dev @stateward/language-server
|
|
|
11
11
|
Start it with `npx stateward-language-server`. It talks over stdin and stdout and checks the
|
|
12
12
|
project of the nearest `stateward.config.ts`. The VS Code extension uses the copy installed in your
|
|
13
13
|
project, else its own.
|
|
14
|
+
|
|
15
|
+
## What you see
|
|
16
|
+
|
|
17
|
+
- Findings as diagnostics, as you type, with the rule id and the change to make.
|
|
18
|
+
- Quick fixes, each tried on a copy of your code first: one that would not compile is not offered.
|
|
19
|
+
"Ignore this case here" writes a `// stateward-ignore` comment with the rule.
|
|
20
|
+
- Inlay hints after a function's signature with the errors it can throw (`throws NotFoundError`),
|
|
21
|
+
and after a `catch` variable with the errors its `try` can throw (with the errors pack).
|
|
22
|
+
- Places stateward could not check, as faint hints; baselined findings, faintly.
|
|
23
|
+
|
|
24
|
+
## Docs
|
|
25
|
+
|
|
26
|
+
- [Every rule](https://github.com/khoing1111/ts-stateward/blob/main/docs/rules/README.md), with
|
|
27
|
+
what it finds and its severity.
|
|
28
|
+
- [The config file](https://github.com/khoing1111/ts-stateward/blob/main/docs/user/config.md).
|
|
29
|
+
- [The VS Code extension](https://github.com/khoing1111/ts-stateward/blob/main/packages/vscode/README.md).
|
package/dist/adoption.d.ts
CHANGED
|
@@ -47,6 +47,11 @@ export type NextAnswer = {
|
|
|
47
47
|
| {
|
|
48
48
|
kind: "blocked";
|
|
49
49
|
message: string;
|
|
50
|
+
}
|
|
51
|
+
/** Every rule left could not run here (a library's types are missing): nothing was turned on. */
|
|
52
|
+
| {
|
|
53
|
+
kind: "not-checked";
|
|
54
|
+
message: string;
|
|
50
55
|
} | {
|
|
51
56
|
kind: "rollout-off";
|
|
52
57
|
message: string;
|
package/dist/adoption.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RolloutError, advance, buildPlan, describeTurnOn, readRollout, ruleIsOn, writeRollout, } from "@stateward/core/internal";
|
|
1
|
+
import { RolloutError, advance, buildPlan, describeNotChecked, describeTurnOn, readRollout, ruleIsOn, writeRollout, } from "@stateward/core/internal";
|
|
2
2
|
import { countFindings } from "./report.js";
|
|
3
3
|
export const STATUS_NOTIFICATION = "stateward/status";
|
|
4
4
|
export const PLAN_REQUEST = "stateward/plan";
|
|
@@ -70,7 +70,7 @@ export function parseNextParams(params) {
|
|
|
70
70
|
export function rolloutOffAnswer(configFileName) {
|
|
71
71
|
return {
|
|
72
72
|
kind: "rollout-off",
|
|
73
|
-
message: `
|
|
73
|
+
message: `Set \`rollout: true\` in ${configFileName} first: without it every rule runs, and there is no next rule to turn on.`,
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
76
|
/**
|
|
@@ -87,6 +87,8 @@ export function applyNext(config, result, anyway) {
|
|
|
87
87
|
const enabled = readRollout(config);
|
|
88
88
|
const step = advance(buildPlan(config, result), enabled, anyway);
|
|
89
89
|
switch (step.kind) {
|
|
90
|
+
case "not-checked":
|
|
91
|
+
return { kind: "not-checked", message: describeNotChecked(step.rows) };
|
|
90
92
|
case "all-on":
|
|
91
93
|
return {
|
|
92
94
|
kind: "all-on",
|
package/dist/fix-all.d.ts
CHANGED
|
@@ -38,7 +38,7 @@ export declare function kindWanted(only: readonly string[] | undefined, kind: st
|
|
|
38
38
|
* edit whose version is not the document's current one, so a fix computed before an edit can
|
|
39
39
|
* never be applied to the text after it.
|
|
40
40
|
*/
|
|
41
|
-
export declare function versionedEdit(changes: Readonly<Record<string, readonly TextEdit[]>>, versionOf: (uri: string) => number | null): WorkspaceEdit;
|
|
41
|
+
export declare function versionedEdit(changes: Readonly<Record<string, readonly TextEdit[]>>, versionOf: (uri: string) => number | null, exists?: (uri: string) => boolean): WorkspaceEdit;
|
|
42
42
|
/** `action` with its `changes` edit turned into versioned `documentChanges`; unchanged when it has none. */
|
|
43
43
|
export declare function versionedAction(action: CodeAction, versionOf: (uri: string) => number | null): CodeAction;
|
|
44
44
|
//# sourceMappingURL=fix-all.d.ts.map
|
package/dist/fix-all.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
1
3
|
function compare(a, b) {
|
|
2
4
|
return a.line - b.line || a.character - b.character;
|
|
3
5
|
}
|
|
@@ -74,14 +76,21 @@ export function kindWanted(only, kind) {
|
|
|
74
76
|
* edit whose version is not the document's current one, so a fix computed before an edit can
|
|
75
77
|
* never be applied to the text after it.
|
|
76
78
|
*/
|
|
77
|
-
export function versionedEdit(changes, versionOf) {
|
|
79
|
+
export function versionedEdit(changes, versionOf, exists = fileExists) {
|
|
78
80
|
return {
|
|
79
|
-
documentChanges: Object.entries(changes).
|
|
80
|
-
|
|
81
|
-
edits: [...edits]
|
|
82
|
-
|
|
81
|
+
documentChanges: Object.entries(changes).flatMap(([uri, edits]) => {
|
|
82
|
+
const version = versionOf(uri);
|
|
83
|
+
const edit = { textDocument: { uri, version }, edits: [...edits] };
|
|
84
|
+
// A fix may write a file once (a guarantees file): it is created first, then filled.
|
|
85
|
+
if (version !== null || exists(uri))
|
|
86
|
+
return [edit];
|
|
87
|
+
return [{ kind: "create", uri, options: { ignoreIfExists: true } }, edit];
|
|
88
|
+
}),
|
|
83
89
|
};
|
|
84
90
|
}
|
|
91
|
+
function fileExists(uri) {
|
|
92
|
+
return !uri.startsWith("file:") || existsSync(fileURLToPath(uri));
|
|
93
|
+
}
|
|
85
94
|
/** `action` with its `changes` edit turned into versioned `documentChanges`; unchanged when it has none. */
|
|
86
95
|
export function versionedAction(action, versionOf) {
|
|
87
96
|
const changes = action.edit?.changes;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { type CodeAction, type CodeLens, type Range } from "vscode-languageserver/node";
|
|
2
|
+
import type { GuaranteeProof } from "@stateward/core/internal";
|
|
3
|
+
/**
|
|
4
|
+
* The editor's view of the stated guarantees: a hover and a code lens on each marked
|
|
5
|
+
* declaration, and "Show proof path" on each error about one. Everything here answers from the
|
|
6
|
+
* last full pass (its proofs and the texts it read), so nothing is type checked on a request.
|
|
7
|
+
*/
|
|
8
|
+
/** Opens the places a guarantee covers (its errors first, then its readers) in a list to jump through. */
|
|
9
|
+
export declare const SHOW_COVERS_COMMAND = "stateward.showGuaranteeCovers";
|
|
10
|
+
/** Walks an error about a guarantee: where it fails, then each step of its path, then the declaration. */
|
|
11
|
+
export declare const SHOW_PROOF_PATH_COMMAND = "stateward.showProofPath";
|
|
12
|
+
/** One place to jump to, as the client's commands take it. */
|
|
13
|
+
export type Place = {
|
|
14
|
+
uri: string;
|
|
15
|
+
range: Range;
|
|
16
|
+
label: string;
|
|
17
|
+
};
|
|
18
|
+
/** The argument of both commands: a title and the places, in order. */
|
|
19
|
+
export type PlaceList = {
|
|
20
|
+
title: string;
|
|
21
|
+
places: Place[];
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The proofs of the last full pass, with the text of every file they point into as that pass read
|
|
25
|
+
* it: offsets are turned into lines with that text, and a file whose text has changed since gets
|
|
26
|
+
* no hover or lens until the next pass.
|
|
27
|
+
*/
|
|
28
|
+
export type ProofView = {
|
|
29
|
+
readonly proofs: readonly GuaranteeProof[];
|
|
30
|
+
/** By TypeScript path. */
|
|
31
|
+
readonly texts: ReadonlyMap<string, string>;
|
|
32
|
+
/** The project folder, for paths in a hover. */
|
|
33
|
+
readonly cwd: string;
|
|
34
|
+
};
|
|
35
|
+
export declare const NO_PROOFS: ProofView;
|
|
36
|
+
export declare function proofView(proofs: readonly GuaranteeProof[], textOf: (file: string) => string | undefined, cwd: string): ProofView;
|
|
37
|
+
/**
|
|
38
|
+
* The hover on a marked declaration: "Guaranteed: <what>; proven for N readers", or the first
|
|
39
|
+
* error and its path when it is not proven. Undefined when `offset` is on no declaration.
|
|
40
|
+
*/
|
|
41
|
+
export declare function guaranteeHover(view: ProofView, file: string, text: string, offset: number): string | undefined;
|
|
42
|
+
/** A code lens above each marked declaration of `file`: what it covers, and a command that lists it. */
|
|
43
|
+
export declare function guaranteeLenses(view: ProofView, file: string, text: string): CodeLens[];
|
|
44
|
+
/** "Show proof path" for each error about a guarantee whose place in `file` meets `range`. */
|
|
45
|
+
export declare function proofPathActions(view: ProofView, file: string, text: string, range: Range): CodeAction[];
|
|
46
|
+
//# sourceMappingURL=guarantees.d.ts.map
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import { CodeActionKind, } from "vscode-languageserver/node";
|
|
3
|
+
import { describeStatus, displayPath } from "@stateward/core/internal";
|
|
4
|
+
import { buildLineIndex, toRange } from "./mapping.js";
|
|
5
|
+
/**
|
|
6
|
+
* The editor's view of the stated guarantees: a hover and a code lens on each marked
|
|
7
|
+
* declaration, and "Show proof path" on each error about one. Everything here answers from the
|
|
8
|
+
* last full pass (its proofs and the texts it read), so nothing is type checked on a request.
|
|
9
|
+
*/
|
|
10
|
+
/** Opens the places a guarantee covers (its errors first, then its readers) in a list to jump through. */
|
|
11
|
+
export const SHOW_COVERS_COMMAND = "stateward.showGuaranteeCovers";
|
|
12
|
+
/** Walks an error about a guarantee: where it fails, then each step of its path, then the declaration. */
|
|
13
|
+
export const SHOW_PROOF_PATH_COMMAND = "stateward.showProofPath";
|
|
14
|
+
/** At most this many steps of a path are shown in a hover; the command shows them all. */
|
|
15
|
+
const HOVER_STEPS = 4;
|
|
16
|
+
export const NO_PROOFS = { proofs: [], texts: new Map(), cwd: "" };
|
|
17
|
+
/** Every file a proof points into. */
|
|
18
|
+
function filesOf(proofs) {
|
|
19
|
+
const files = new Set();
|
|
20
|
+
for (const { stated, status } of proofs) {
|
|
21
|
+
files.add(stated.location.file);
|
|
22
|
+
for (const place of stated.covered)
|
|
23
|
+
files.add(place.location.file);
|
|
24
|
+
if (status.kind !== "unproven")
|
|
25
|
+
continue;
|
|
26
|
+
for (const error of status.errors) {
|
|
27
|
+
files.add(error.location.file);
|
|
28
|
+
for (const note of error.related)
|
|
29
|
+
files.add(note.location.file);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return files;
|
|
33
|
+
}
|
|
34
|
+
export function proofView(proofs, textOf, cwd) {
|
|
35
|
+
const texts = new Map();
|
|
36
|
+
for (const file of filesOf(proofs)) {
|
|
37
|
+
const text = textOf(file);
|
|
38
|
+
if (text !== undefined)
|
|
39
|
+
texts.set(file, text);
|
|
40
|
+
}
|
|
41
|
+
return { proofs, texts, cwd };
|
|
42
|
+
}
|
|
43
|
+
function indexer(view) {
|
|
44
|
+
const indexes = new Map();
|
|
45
|
+
return (file) => {
|
|
46
|
+
const known = indexes.get(file);
|
|
47
|
+
if (known !== undefined)
|
|
48
|
+
return known;
|
|
49
|
+
const index = buildLineIndex(view.texts.get(file) ?? "");
|
|
50
|
+
indexes.set(file, index);
|
|
51
|
+
return index;
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/** The proofs declared in `file`, when the editor's text is still the one the pass read. */
|
|
55
|
+
function declaredIn(view, file, text) {
|
|
56
|
+
if (view.texts.get(file) !== text)
|
|
57
|
+
return [];
|
|
58
|
+
return view.proofs.filter((proof) => proof.stated.location.file === file);
|
|
59
|
+
}
|
|
60
|
+
function place(index, location, label) {
|
|
61
|
+
return {
|
|
62
|
+
uri: pathToFileURL(location.file).href,
|
|
63
|
+
range: toRange(index(location.file), location),
|
|
64
|
+
label,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function lineOf(view, index, location) {
|
|
68
|
+
const line = toRange(index(location.file), location).start.line + 1;
|
|
69
|
+
return `${displayPath(view.cwd, location.file)}:${String(line)}`;
|
|
70
|
+
}
|
|
71
|
+
/** An error's path as places: where it fails, each note (the declaration's last). */
|
|
72
|
+
function pathOf(view, index, error) {
|
|
73
|
+
return [
|
|
74
|
+
place(index, error.location, error.message),
|
|
75
|
+
...error.related.map((note) => place(index, note.location, note.message)),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
function errorsOf(proof) {
|
|
79
|
+
return proof.status.kind === "unproven" ? proof.status.errors : [];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The hover on a marked declaration: "Guaranteed: <what>; proven for N readers", or the first
|
|
83
|
+
* error and its path when it is not proven. Undefined when `offset` is on no declaration.
|
|
84
|
+
*/
|
|
85
|
+
export function guaranteeHover(view, file, text, offset) {
|
|
86
|
+
const proof = declaredIn(view, file, text).find(({ stated }) => offset >= stated.location.start && offset <= stated.location.end);
|
|
87
|
+
if (proof === undefined)
|
|
88
|
+
return undefined;
|
|
89
|
+
const { stated } = proof;
|
|
90
|
+
const head = `**Guaranteed:** ${stated.promise} (\`${stated.marker}\`); ${describeStatus(proof)}`;
|
|
91
|
+
const first = errorsOf(proof)[0];
|
|
92
|
+
if (first === undefined)
|
|
93
|
+
return head;
|
|
94
|
+
const index = indexer(view);
|
|
95
|
+
// The declaration's own note says what the hover already says.
|
|
96
|
+
const steps = first.related.filter((note) => note.location.file !== stated.location.file || note.location.start !== stated.location.start);
|
|
97
|
+
const shown = steps
|
|
98
|
+
.slice(0, HOVER_STEPS)
|
|
99
|
+
.map((note) => `- ${lineOf(view, index, note.location)}: ${note.message}`);
|
|
100
|
+
const more = steps.length > shown.length
|
|
101
|
+
? [`- … ${String(steps.length - shown.length)} more: "Show proof path" on the error`]
|
|
102
|
+
: [];
|
|
103
|
+
return [
|
|
104
|
+
head,
|
|
105
|
+
`First: ${first.message} (${lineOf(view, index, first.location)})`,
|
|
106
|
+
[...shown, ...more].join("\n"),
|
|
107
|
+
]
|
|
108
|
+
.filter((part) => part !== "")
|
|
109
|
+
.join("\n\n");
|
|
110
|
+
}
|
|
111
|
+
function lensTitle(proof) {
|
|
112
|
+
const { stated, status } = proof;
|
|
113
|
+
const count = stated.covered.length;
|
|
114
|
+
const covers = count === 1 ? stated.covers.replace(/s$/, "") : stated.covers;
|
|
115
|
+
switch (status.kind) {
|
|
116
|
+
case "proven":
|
|
117
|
+
return `${String(count)} ${covers} proven · show`;
|
|
118
|
+
case "unproven":
|
|
119
|
+
return `${String(status.errors.length)} not proven · ${String(count)} ${covers} · show`;
|
|
120
|
+
case "unused":
|
|
121
|
+
case "off":
|
|
122
|
+
return describeStatus(proof);
|
|
123
|
+
default: {
|
|
124
|
+
const unreachable = status;
|
|
125
|
+
return unreachable;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** A code lens above each marked declaration of `file`: what it covers, and a command that lists it. */
|
|
130
|
+
export function guaranteeLenses(view, file, text) {
|
|
131
|
+
const index = indexer(view);
|
|
132
|
+
return declaredIn(view, file, text).map((proof) => {
|
|
133
|
+
const { stated } = proof;
|
|
134
|
+
const places = [
|
|
135
|
+
...errorsOf(proof).map((error) => place(index, error.location, `not proven: ${error.message}`)),
|
|
136
|
+
...stated.covered.map((covered) => place(index, covered.location, covered.name)),
|
|
137
|
+
];
|
|
138
|
+
const argument = { title: `${stated.name}: ${describeStatus(proof)}`, places };
|
|
139
|
+
const range = toRange(index(file), stated.location);
|
|
140
|
+
return {
|
|
141
|
+
range: { start: range.start, end: range.start },
|
|
142
|
+
command: {
|
|
143
|
+
title: lensTitle(proof),
|
|
144
|
+
// A lens with nothing to list is a label: no command runs when it is clicked.
|
|
145
|
+
command: places.length === 0 ? "" : SHOW_COVERS_COMMAND,
|
|
146
|
+
arguments: places.length === 0 ? [] : [argument],
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
/** "Show proof path" for each error about a guarantee whose place in `file` meets `range`. */
|
|
152
|
+
export function proofPathActions(view, file, text, range) {
|
|
153
|
+
if (view.texts.get(file) !== text)
|
|
154
|
+
return [];
|
|
155
|
+
const index = indexer(view);
|
|
156
|
+
return view.proofs.flatMap((proof) => errorsOf(proof).flatMap((error) => {
|
|
157
|
+
if (error.location.file !== file)
|
|
158
|
+
return [];
|
|
159
|
+
const at = toRange(index(file), error.location);
|
|
160
|
+
const meets = !(isBefore(at.end, range.start) || isBefore(range.end, at.start));
|
|
161
|
+
if (!meets)
|
|
162
|
+
return [];
|
|
163
|
+
const argument = {
|
|
164
|
+
title: `Proof path: ${error.message}`,
|
|
165
|
+
places: pathOf(view, index, error),
|
|
166
|
+
};
|
|
167
|
+
const title = `Show the proof path of ${proof.stated.name}`;
|
|
168
|
+
return [
|
|
169
|
+
{
|
|
170
|
+
title,
|
|
171
|
+
kind: CodeActionKind.QuickFix,
|
|
172
|
+
command: { title, command: SHOW_PROOF_PATH_COMMAND, arguments: [argument] },
|
|
173
|
+
},
|
|
174
|
+
];
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
function isBefore(a, b) {
|
|
178
|
+
return a.line < b.line || (a.line === b.line && a.character < b.character);
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=guarantees.js.map
|
package/dist/server.d.ts
CHANGED
|
@@ -23,12 +23,16 @@ export declare const DISK_SAVE_MS: number;
|
|
|
23
23
|
* Wires the stateward language server to `connection`. The caller starts listening. The returned
|
|
24
24
|
* function stops the server's timers (also done on shutdown); call it before disposing the connection.
|
|
25
25
|
*/
|
|
26
|
-
export declare function startServer(connection: Connection, { idleMs, cacheDir, saveMs, }?: {
|
|
26
|
+
export declare function startServer(connection: Connection, { idleMs, cacheDir, saveMs, fastMs, fullMs, }?: {
|
|
27
27
|
/** How long a nested config's engine waits, with none of its files open, before it stops. */
|
|
28
28
|
idleMs?: number;
|
|
29
29
|
/** The results cache folder of a project, as the CLI finds it; undefined for none. */
|
|
30
30
|
cacheDir?: (project: string) => string | undefined;
|
|
31
31
|
/** The least time between two writes of a config's results cache. */
|
|
32
32
|
saveMs?: number;
|
|
33
|
+
/** How long after an edit its fast pass waits (tests: long, to see the state before it). */
|
|
34
|
+
fastMs?: number;
|
|
35
|
+
/** How long after an edit the full pass waits. */
|
|
36
|
+
fullMs?: number;
|
|
33
37
|
}): () => void;
|
|
34
38
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
-
import { CodeActionKind, DidChangeConfigurationNotification, DidChangeWatchedFilesNotification, MarkupKind, ErrorCodes, MessageType, ResponseError, ShowMessageNotification, TextDocumentSyncKind, TextDocuments, } from "vscode-languageserver/node";
|
|
4
|
+
import { CodeActionKind, CodeLensRefreshRequest, DidChangeConfigurationNotification, DidChangeWatchedFilesNotification, MarkupKind, ErrorCodes, MessageType, ResponseError, ShowMessageNotification, TextDocumentSyncKind, TextDocuments, } from "vscode-languageserver/node";
|
|
5
5
|
import { TextDocument } from "vscode-languageserver-textdocument";
|
|
6
|
-
import { BaselineError, NothingToCheckError, activeRuleIds, TsconfigError, TypeScriptLibMissingError, check, computeImplementationId, configLocalImports, createChecker, defaultCacheDir, displayPath, loadConfigFile, openDiskCache, reloadsLocalImports, resultsCacheLimit, toTypeScriptPath, } from "@stateward/core/internal";
|
|
6
|
+
import { BaselineError, NothingToCheckError, activeRuleIds, TsconfigError, TypeScriptLibMissingError, check, computeImplementationId, configLocalImports, createChecker, defaultCacheDir, displayPath, loadConfigFile, proveGuarantees, openDiskCache, reloadsLocalImports, resultsCacheLimit, toTypeScriptPath, } from "@stateward/core/internal";
|
|
7
7
|
import { InvalidNextParamsError, NEXT_REQUEST, PLAN_REQUEST, RECHECK_REQUEST, STATUS_NOTIFICATION, applyNext, parseNextParams, planAnswer, rolloutOffAnswer, statusReport, } from "./adoption.js";
|
|
8
8
|
import { layBaseline } from "./baseline.js";
|
|
9
9
|
import { DEFAULT_HINT_SETTINGS, hintEnabled, readHintSettings, toInlayHints, } from "./inlay-hints.js";
|
|
10
10
|
import { InvalidExplainParamsError, answerExplain } from "./explain.js";
|
|
11
11
|
import { fixAllEdits, kindWanted, versionedAction, versionedEdit, } from "./fix-all.js";
|
|
12
|
+
import { NO_PROOFS, guaranteeHover, guaranteeLenses, proofPathActions, proofView, } from "./guarantees.js";
|
|
12
13
|
import { mergeFastPass } from "./merge.js";
|
|
13
14
|
import { SILENT_PROGRESS, withProgress } from "./progress.js";
|
|
14
15
|
import { fastPassLine, fullPassLine, longestStepLine } from "./report.js";
|
|
15
|
-
import { TOKEN_TYPES, buildLineIndex, encodeTokens, offsetAt, rangesIntersect,
|
|
16
|
+
import { TOKEN_TYPES, buildLineIndex, encodeTokens, offsetAt, rangesIntersect, toCodeActions, toLspChanges, toDiagnostic, toHiddenDiagnostic, toIgnoreAction, } from "./mapping.js";
|
|
16
17
|
import { SCRIPT_GLOB, classifyChange, createErrorGate, isImportableJson } from "./watch.js";
|
|
17
18
|
import { createConfigLocator, folderOf } from "./configs.js";
|
|
18
19
|
/** The wait after a config load, a watched-file change or a closed document, before a full pass. */
|
|
@@ -89,7 +90,7 @@ function describe(error) {
|
|
|
89
90
|
* Wires the stateward language server to `connection`. The caller starts listening. The returned
|
|
90
91
|
* function stops the server's timers (also done on shutdown); call it before disposing the connection.
|
|
91
92
|
*/
|
|
92
|
-
export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (project) => defaultCacheDir(project, process.env), saveMs = DISK_SAVE_MS, } = {}) {
|
|
93
|
+
export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (project) => defaultCacheDir(project, process.env), saveMs = DISK_SAVE_MS, fastMs = FAST_MS, fullMs = FULL_MS, } = {}) {
|
|
93
94
|
const documents = new TextDocuments(TextDocument);
|
|
94
95
|
/** The workspace folders; undefined until the client has initialized. */
|
|
95
96
|
let folders = [];
|
|
@@ -97,6 +98,8 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
97
98
|
let watchScripts = false;
|
|
98
99
|
/** The client can be told to ask for inline hints again (after a check, a settings change). */
|
|
99
100
|
let refreshHints = false;
|
|
101
|
+
/** The client can be told to ask for code lenses again (after a full pass). */
|
|
102
|
+
let refreshLenses = false;
|
|
100
103
|
/** The client answers `workspace/configuration`. */
|
|
101
104
|
let canPullSettings = false;
|
|
102
105
|
/** The client lets the server ask to be told when settings change. */
|
|
@@ -148,16 +151,18 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
148
151
|
const folder = folderOf(engine.configFile, folders) ?? engine.cwd;
|
|
149
152
|
return ` (${displayPath(folder, engine.configFile)})`;
|
|
150
153
|
};
|
|
154
|
+
/** A file's text on disk; empty for a file a fix creates (it does not exist yet). */
|
|
155
|
+
const textOnDisk = (file) => (existsSync(file) ? readFileSync(file, "utf8") : "");
|
|
151
156
|
const fileMapper = (open) => {
|
|
152
157
|
const indexes = new Map();
|
|
153
158
|
return {
|
|
154
159
|
uriOf: (file) => pathToFileURL(file).href,
|
|
155
|
-
textOf: (file) => open.get(file) ??
|
|
160
|
+
textOf: (file) => open.get(file) ?? textOnDisk(file),
|
|
156
161
|
indexOf: (file) => {
|
|
157
162
|
const cached = indexes.get(file);
|
|
158
163
|
if (cached !== undefined)
|
|
159
164
|
return cached;
|
|
160
|
-
const index = buildLineIndex(open.get(file) ??
|
|
165
|
+
const index = buildLineIndex(open.get(file) ?? textOnDisk(file));
|
|
161
166
|
indexes.set(file, index);
|
|
162
167
|
return index;
|
|
163
168
|
},
|
|
@@ -244,7 +249,10 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
244
249
|
return;
|
|
245
250
|
}
|
|
246
251
|
const raw = outcome.result;
|
|
247
|
-
const
|
|
252
|
+
const active = activeRuleIds(state.config, { kind: "configured" });
|
|
253
|
+
const pass = layBaseline(state.config, raw, active, open);
|
|
254
|
+
// Before the baseline: an accepted error still leaves its guarantee unproven.
|
|
255
|
+
engine.proofs = proofView(proveGuarantees(state.checker.guarantees(), raw.findings, active), (file) => state.checker.analysedText(file), engine.cwd);
|
|
248
256
|
const result = pass.fresh;
|
|
249
257
|
const { byUri, sites } = render(pass.findings.filter((finding) => shows(engine, finding.location.file)), pass.hidden.filter((entry) => shows(engine, entry.file)), open, pass.baselined);
|
|
250
258
|
for (const uri of engine.published) {
|
|
@@ -262,9 +270,11 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
262
270
|
...statusReport(state.config, result, pass.baselined.size),
|
|
263
271
|
configFile: engine.configFile,
|
|
264
272
|
});
|
|
265
|
-
// The hints come from this pass's summaries.
|
|
273
|
+
// The hints and lenses come from this pass's summaries.
|
|
266
274
|
if (refreshHints)
|
|
267
275
|
connection.languages.inlayHint.refresh().catch(logUnexpected);
|
|
276
|
+
if (refreshLenses)
|
|
277
|
+
connection.sendRequest(CodeLensRefreshRequest.type).catch(logUnexpected);
|
|
268
278
|
saveSoon(engine);
|
|
269
279
|
}
|
|
270
280
|
catch (error) {
|
|
@@ -430,8 +440,8 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
430
440
|
clearTimeout(engine.fastTimer);
|
|
431
441
|
engine.fastTimer = setTimeout(() => {
|
|
432
442
|
fastPass(engine);
|
|
433
|
-
},
|
|
434
|
-
scheduleFull(engine,
|
|
443
|
+
}, fastMs);
|
|
444
|
+
scheduleFull(engine, fullMs);
|
|
435
445
|
};
|
|
436
446
|
const clearDiagnostics = (engine) => {
|
|
437
447
|
for (const uri of engine.published)
|
|
@@ -540,6 +550,7 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
540
550
|
disk: undefined,
|
|
541
551
|
saveTimer: undefined,
|
|
542
552
|
savedAt: undefined,
|
|
553
|
+
proofs: NO_PROOFS,
|
|
543
554
|
};
|
|
544
555
|
engines.set(configFile, engine);
|
|
545
556
|
void loadEngine(engine).catch(logUnexpected);
|
|
@@ -606,6 +617,7 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
606
617
|
watchScripts =
|
|
607
618
|
params.capabilities.workspace?.didChangeWatchedFiles?.dynamicRegistration === true;
|
|
608
619
|
refreshHints = params.capabilities.workspace?.inlayHint?.refreshSupport === true;
|
|
620
|
+
refreshLenses = params.capabilities.workspace?.codeLens?.refreshSupport === true;
|
|
609
621
|
canPullSettings = params.capabilities.workspace?.configuration === true;
|
|
610
622
|
watchSettings =
|
|
611
623
|
params.capabilities.workspace?.didChangeConfiguration?.dynamicRegistration === true;
|
|
@@ -616,6 +628,7 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
616
628
|
textDocumentSync: TextDocumentSyncKind.Incremental,
|
|
617
629
|
hoverProvider: true,
|
|
618
630
|
inlayHintProvider: true,
|
|
631
|
+
codeLensProvider: { resolveProvider: false },
|
|
619
632
|
codeActionProvider: {
|
|
620
633
|
codeActionKinds: [CodeActionKind.QuickFix, CodeActionKind.RefactorRewrite, FIX_ALL_KIND],
|
|
621
634
|
},
|
|
@@ -707,7 +720,7 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
707
720
|
// An imported JSON file: no findings of its own, but the files that import it change, and
|
|
708
721
|
// only the full pass checks those.
|
|
709
722
|
cancelFull(engine);
|
|
710
|
-
scheduleFull(engine,
|
|
723
|
+
scheduleFull(engine, fullMs);
|
|
711
724
|
return;
|
|
712
725
|
}
|
|
713
726
|
// Fixes hold offsets of the text before this edit: offer none until the fast pass recomputes them.
|
|
@@ -888,10 +901,15 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
888
901
|
actions.push(...sites
|
|
889
902
|
.filter((site) => rangesIntersect(site.range, params.range))
|
|
890
903
|
.flatMap((site) => [...site.actions, ...(site.extra ?? [])])
|
|
891
|
-
.map((action) => versionedAction(action, versionOf)), ...explainActions(params.context.diagnostics))
|
|
904
|
+
.map((action) => versionedAction(action, versionOf)), ...explainActions(params.context.diagnostics), ...(guarded(() => {
|
|
905
|
+
const document = documents.get(params.textDocument.uri);
|
|
906
|
+
if (engine === undefined || key === undefined || document === undefined)
|
|
907
|
+
return [];
|
|
908
|
+
return proofPathActions(engine.proofs, key, document.getText(), params.range);
|
|
909
|
+
}) ?? []));
|
|
892
910
|
if (all.count >= 2) {
|
|
893
911
|
actions.push({
|
|
894
|
-
title: "Fix all stateward problems in
|
|
912
|
+
title: "Fix all stateward problems in the file",
|
|
895
913
|
kind: CodeActionKind.QuickFix,
|
|
896
914
|
...fixAll,
|
|
897
915
|
});
|
|
@@ -899,7 +917,7 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
899
917
|
}
|
|
900
918
|
if (all.count >= 1 && kindWanted(only, FIX_ALL_KIND)) {
|
|
901
919
|
actions.push({
|
|
902
|
-
title: "Fix all stateward problems in
|
|
920
|
+
title: "Fix all stateward problems in the file",
|
|
903
921
|
kind: FIX_ALL_KIND,
|
|
904
922
|
...fixAll,
|
|
905
923
|
});
|
|
@@ -925,17 +943,12 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
925
943
|
return [];
|
|
926
944
|
const index = buildLineIndex(text);
|
|
927
945
|
const found = editor.refactors(file, offsetAt(index, range.start), offsetAt(index, range.end));
|
|
946
|
+
// The edits may reach other files (a helper file the refactoring writes once).
|
|
947
|
+
const files = fileMapper(new Map([[file, text]]));
|
|
928
948
|
return found.map((action) => versionedAction({
|
|
929
949
|
title: action.title,
|
|
930
950
|
kind: CodeActionKind.RefactorRewrite,
|
|
931
|
-
edit: {
|
|
932
|
-
changes: {
|
|
933
|
-
[uri]: action.edits.map((edit) => ({
|
|
934
|
-
range: toRange(index, edit.location),
|
|
935
|
-
newText: edit.newText,
|
|
936
|
-
})),
|
|
937
|
-
},
|
|
938
|
-
},
|
|
951
|
+
edit: { changes: toLspChanges(action.edits, files) },
|
|
939
952
|
}, versionOf));
|
|
940
953
|
}) ?? [];
|
|
941
954
|
/** The checker of the config `uri`'s file belongs to, when it is ready. */
|
|
@@ -948,11 +961,34 @@ export function startServer(connection, { idleMs = ENGINE_IDLE_MS, cacheDir = (p
|
|
|
948
961
|
const editor = checkerOf(params.textDocument.uri);
|
|
949
962
|
if (document === undefined || editor === undefined)
|
|
950
963
|
return null;
|
|
951
|
-
const markdown = guarded(() =>
|
|
964
|
+
const markdown = guarded(() => {
|
|
965
|
+
const file = fileURLToPath(document.uri);
|
|
966
|
+
const text = document.getText();
|
|
967
|
+
const offset = offsetAt(buildLineIndex(text), params.position);
|
|
968
|
+
// From the last full pass's proofs: no type checking for it.
|
|
969
|
+
const engine = engineOf(file);
|
|
970
|
+
const guarantee = engine === undefined
|
|
971
|
+
? undefined
|
|
972
|
+
: guaranteeHover(engine.proofs, toTypeScriptPath(file), text, offset);
|
|
973
|
+
return [...(guarantee === undefined ? [] : [guarantee]), ...editor.hover(file, offset)];
|
|
974
|
+
});
|
|
952
975
|
if (markdown === undefined || markdown.length === 0)
|
|
953
976
|
return null;
|
|
954
977
|
return { contents: { kind: MarkupKind.Markdown, value: markdown.join("\n\n---\n\n") } };
|
|
955
978
|
});
|
|
979
|
+
connection.onCodeLens((params) => {
|
|
980
|
+
const document = documents.get(params.textDocument.uri);
|
|
981
|
+
if (document === undefined)
|
|
982
|
+
return [];
|
|
983
|
+
const lenses = guarded(() => {
|
|
984
|
+
const file = fileURLToPath(document.uri);
|
|
985
|
+
const engine = engineOf(file);
|
|
986
|
+
if (engine === undefined)
|
|
987
|
+
return [];
|
|
988
|
+
return guaranteeLenses(engine.proofs, toTypeScriptPath(file), document.getText());
|
|
989
|
+
});
|
|
990
|
+
return lenses ?? [];
|
|
991
|
+
});
|
|
956
992
|
/** Which inline hints the editor's settings turn on (`stateward.inlayHints.*`). */
|
|
957
993
|
let hintSettings = DEFAULT_HINT_SETTINGS;
|
|
958
994
|
connection.languages.inlayHint.on((params) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stateward/language-server",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "The stateward language server: exclusive states and predictable code paths in any editor that speaks LSP, with findings, quick fixes and hints.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -41,12 +41,13 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"vscode-languageserver": "^11.0.0",
|
|
43
43
|
"vscode-languageserver-textdocument": "^1.0.16",
|
|
44
|
-
"@stateward/core": "1.
|
|
44
|
+
"@stateward/core": "1.1.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/react": "^19.3.0",
|
|
48
48
|
"react": "^19.3.1",
|
|
49
49
|
"vscode-jsonrpc": "10.0.0",
|
|
50
|
-
"@stateward/plugin-
|
|
50
|
+
"@stateward/plugin-ownership": "1.1.0",
|
|
51
|
+
"@stateward/plugin-react-context": "1.1.0"
|
|
51
52
|
}
|
|
52
53
|
}
|