@stateward/language-server 0.0.0-stage → 1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the stateward authors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,13 @@
1
- # Temporary Holding Version
1
+ # @stateward/language-server
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The language server behind the Stateward VS Code extension: [stateward](https://github.com/khoing1111/ts-stateward#readme)'s
4
+ findings as you type, quick fixes, and hints about the errors a function can throw, for any editor
5
+ that speaks the Language Server Protocol.
6
+
7
+ ```bash
8
+ npm install --save-dev @stateward/language-server
9
+ ```
10
+
11
+ Start it with `npx stateward-language-server`. It talks over stdin and stdout and checks the
12
+ project of the nearest `stateward.config.ts`. The VS Code extension uses the copy installed in your
13
+ project, else its own.
@@ -0,0 +1,73 @@
1
+ import type { CheckResult, Config, PlanRow } from "@stateward/core/internal";
2
+ import { type Counts } from "./report.js";
3
+ export declare const STATUS_NOTIFICATION = "stateward/status";
4
+ export declare const PLAN_REQUEST = "stateward/plan";
5
+ export declare const NEXT_REQUEST = "stateward/next";
6
+ export declare const RECHECK_REQUEST = "stateward/recheck";
7
+ /** Where the project stands after a full pass: the counts and, with rollout on, the rule being fixed. */
8
+ export type StatusReport = Counts & {
9
+ /** Findings the baseline accepts: not in the counts above. */
10
+ baselined: number;
11
+ /** Set when the config turns no rule on (a plugin alone turns nothing on). */
12
+ noRulesOn?: true;
13
+ rollout: {
14
+ on: boolean;
15
+ /** The rule turned on last and its findings; absent when rollout is off or no rule is on yet. */
16
+ current?: {
17
+ ruleId: string;
18
+ findings: number;
19
+ };
20
+ enabled: number;
21
+ total: number;
22
+ };
23
+ };
24
+ /**
25
+ * The status for a full pass of `config`. `result` is the configured pass, which has the findings
26
+ * of every rule that is on, which is all the status needs: the current rule is one of them.
27
+ */
28
+ export declare function statusReport(config: Config, result: CheckResult, baselined?: number): StatusReport;
29
+ /** The answer to `stateward/plan`: the plan rows, in order, and where the rollout stands. */
30
+ export type PlanAnswer = {
31
+ rollout: boolean;
32
+ /** The rules turned on, in order; empty when rollout is off (every rule runs then). */
33
+ enabled: readonly string[];
34
+ rules: readonly PlanRow[];
35
+ };
36
+ /** `result` must come from a run of the `all` selection, like `stateward plan`. */
37
+ export declare function planAnswer(config: Config, result: CheckResult): PlanAnswer;
38
+ /** The answer to `stateward/next`. Every kind carries the text to show. */
39
+ export type NextAnswer = {
40
+ kind: "turned-on";
41
+ message: string;
42
+ } | {
43
+ kind: "all-on";
44
+ message: string;
45
+ }
46
+ /** The rule turned on last still has findings; asking again with `anyway` moves on. */
47
+ | {
48
+ kind: "blocked";
49
+ message: string;
50
+ } | {
51
+ kind: "rollout-off";
52
+ message: string;
53
+ }
54
+ /** The config cannot be used, a rule failed or the rollout file is broken. */
55
+ | {
56
+ kind: "failed";
57
+ message: string;
58
+ };
59
+ /** The `stateward/next` params are not `{ anyway?: boolean }`. */
60
+ export declare class InvalidNextParamsError extends Error {
61
+ constructor();
62
+ }
63
+ /** The `anyway` flag of the params; nothing sent means false. */
64
+ export declare function parseNextParams(params: unknown): boolean;
65
+ /** What the editor tells the developer when the project has no rollout. */
66
+ export declare function rolloutOffAnswer(configFileName: string): NextAnswer;
67
+ /**
68
+ * What `stateward next` does: writes the rollout file through core's writer and says what it did
69
+ * in the CLI's words. `result` must come from a run of the `all` selection. A rule that failed
70
+ * makes the plan incomplete, so nothing is written then.
71
+ */
72
+ export declare function applyNext(config: Config, result: CheckResult, anyway: boolean): NextAnswer;
73
+ //# sourceMappingURL=adoption.d.ts.map
@@ -0,0 +1,122 @@
1
+ import { RolloutError, advance, buildPlan, describeTurnOn, readRollout, ruleIsOn, writeRollout, } from "@stateward/core/internal";
2
+ import { countFindings } from "./report.js";
3
+ export const STATUS_NOTIFICATION = "stateward/status";
4
+ export const PLAN_REQUEST = "stateward/plan";
5
+ export const NEXT_REQUEST = "stateward/next";
6
+ export const RECHECK_REQUEST = "stateward/recheck";
7
+ /** Whether the config turns any rule on, in itself or in an override. */
8
+ function anyRuleOn(config) {
9
+ return config.plugins.some((plugin) => Object.entries(plugin.rules).some(([key, rule]) => ruleIsOn(config, plugin.name, key, rule.meta)));
10
+ }
11
+ /**
12
+ * The status for a full pass of `config`. `result` is the configured pass, which has the findings
13
+ * of every rule that is on, which is all the status needs: the current rule is one of them.
14
+ */
15
+ export function statusReport(config, result, baselined = 0) {
16
+ const counts = {
17
+ ...countFindings(result),
18
+ ...(anyRuleOn(config) ? {} : { noRulesOn: true }),
19
+ };
20
+ if (!config.rollout) {
21
+ return { ...counts, baselined, rollout: { on: false, enabled: 0, total: 0 } };
22
+ }
23
+ const enabled = readRollout(config);
24
+ const plan = buildPlan(config, result);
25
+ const lastId = enabled.at(-1);
26
+ const last = plan.rules.find((row) => row.ruleId === lastId);
27
+ return {
28
+ ...counts,
29
+ baselined,
30
+ rollout: {
31
+ on: true,
32
+ ...(last === undefined ? {} : { current: { ruleId: last.ruleId, findings: last.findings } }),
33
+ enabled: enabled.length,
34
+ total: plan.rules.length,
35
+ },
36
+ };
37
+ }
38
+ /** `result` must come from a run of the `all` selection, like `stateward plan`. */
39
+ export function planAnswer(config, result) {
40
+ return {
41
+ rollout: config.rollout,
42
+ enabled: config.rollout ? readRollout(config) : [],
43
+ rules: buildPlan(config, result).rules,
44
+ };
45
+ }
46
+ function countOf(count, noun) {
47
+ return `${String(count)} ${noun}${count === 1 ? "" : "s"}`;
48
+ }
49
+ /** The `stateward/next` params are not `{ anyway?: boolean }`. */
50
+ export class InvalidNextParamsError extends Error {
51
+ constructor() {
52
+ super("stateward/next expects { anyway?: boolean }");
53
+ this.name = "InvalidNextParamsError";
54
+ Object.setPrototypeOf(this, InvalidNextParamsError.prototype);
55
+ }
56
+ }
57
+ /** The `anyway` flag of the params; nothing sent means false. */
58
+ export function parseNextParams(params) {
59
+ if (params === null || params === undefined)
60
+ return false;
61
+ if (typeof params !== "object")
62
+ throw new InvalidNextParamsError();
63
+ if (!("anyway" in params))
64
+ return false;
65
+ if (typeof params.anyway !== "boolean")
66
+ throw new InvalidNextParamsError();
67
+ return params.anyway;
68
+ }
69
+ /** What the editor tells the developer when the project has no rollout. */
70
+ export function rolloutOffAnswer(configFileName) {
71
+ return {
72
+ kind: "rollout-off",
73
+ message: `Turn on \`rollout: true\` in ${configFileName} first: without it every rule runs, and there is no next rule to turn on.`,
74
+ };
75
+ }
76
+ /**
77
+ * What `stateward next` does: writes the rollout file through core's writer and says what it did
78
+ * in the CLI's words. `result` must come from a run of the `all` selection. A rule that failed
79
+ * makes the plan incomplete, so nothing is written then.
80
+ */
81
+ export function applyNext(config, result, anyway) {
82
+ const failures = result.findings.filter((finding) => finding.kind === "failure");
83
+ if (failures.length > 0) {
84
+ return { kind: "failed", message: failures.map((finding) => finding.message).join("\n") };
85
+ }
86
+ try {
87
+ const enabled = readRollout(config);
88
+ const step = advance(buildPlan(config, result), enabled, anyway);
89
+ switch (step.kind) {
90
+ case "all-on":
91
+ return {
92
+ kind: "all-on",
93
+ message: "Every rule is on; you can remove stateward.rollout.json and `rollout`.",
94
+ };
95
+ case "blocked":
96
+ return {
97
+ kind: "blocked",
98
+ message: `\`${step.row.ruleId}\` still has ${countOf(step.row.findings, "finding")}; fix them, turn the next rule on anyway, or accept them with \`stateward baseline\` in a terminal.`,
99
+ };
100
+ case "turn-on": {
101
+ // The rule with findings goes last: it is the one the next step waits on.
102
+ const clean = step.clean.map((row) => row.ruleId);
103
+ writeRollout(config, [
104
+ ...enabled,
105
+ ...clean,
106
+ ...(step.row === undefined ? [] : [step.row.ruleId]),
107
+ ]);
108
+ return { kind: "turned-on", message: describeTurnOn(step).join("\n") };
109
+ }
110
+ default: {
111
+ const unreachable = step;
112
+ return unreachable;
113
+ }
114
+ }
115
+ }
116
+ catch (error) {
117
+ if (error instanceof RolloutError)
118
+ return { kind: "failed", message: error.message };
119
+ throw error;
120
+ }
121
+ }
122
+ //# sourceMappingURL=adoption.js.map
@@ -0,0 +1,17 @@
1
+ import type { CheckResult, Config, Finding, HiddenFindings } from "@stateward/core/internal";
2
+ /** What the editor shows after the baseline is laid over a pass. */
3
+ export type BaselinedPass = {
4
+ /** New and baselined findings together, in the order of a plain check. */
5
+ findings: readonly Finding[];
6
+ hidden: readonly HiddenFindings[];
7
+ /** The findings the baseline accepts: shown as hints. */
8
+ baselined: ReadonlySet<Finding>;
9
+ /** Only the new ones, for the counts. */
10
+ fresh: CheckResult;
11
+ };
12
+ /**
13
+ * Lays the project's baseline (if it has one) over an uncapped `result`. `ran` is the rules the
14
+ * pass ran. Throws `BaselineError` for a baseline file that cannot be used; the caller reports it.
15
+ */
16
+ export declare function layBaseline(config: Config, result: CheckResult, ran: ReadonlySet<string>, open: ReadonlyMap<string, string>): BaselinedPass;
17
+ //# sourceMappingURL=baseline.d.ts.map
@@ -0,0 +1,32 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { applyBaseline, compareFindings, readBaseline } from "@stateward/core/internal";
3
+ /** The text of `file` as the editor has it: unsaved text first, then the disk. */
4
+ function textOf(open, file) {
5
+ const unsaved = open.get(file);
6
+ if (unsaved !== undefined)
7
+ return unsaved;
8
+ try {
9
+ return readFileSync(file, "utf8");
10
+ }
11
+ catch {
12
+ return undefined;
13
+ }
14
+ }
15
+ /**
16
+ * Lays the project's baseline (if it has one) over an uncapped `result`. `ran` is the rules the
17
+ * pass ran. Throws `BaselineError` for a baseline file that cannot be used; the caller reports it.
18
+ */
19
+ export function layBaseline(config, result, ran, open) {
20
+ const view = applyBaseline(readBaseline(config), result, {
21
+ configDir: config.configDir,
22
+ readText: (file) => textOf(open, file),
23
+ ran,
24
+ });
25
+ return {
26
+ findings: [...view.fresh.findings, ...view.baselined].sort(compareFindings),
27
+ hidden: view.fresh.hidden,
28
+ baselined: new Set(view.baselined),
29
+ fresh: view.fresh,
30
+ };
31
+ }
32
+ //# sourceMappingURL=baseline.js.map
package/dist/bin.d.ts ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ export {};
3
+ //# sourceMappingURL=bin.d.ts.map
package/dist/bin.js ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ import { createConnection } from "vscode-languageserver/node";
3
+ import { limitHeapGrowth } from "./memory.js";
4
+ import { startServer } from "./server.js";
5
+ limitHeapGrowth();
6
+ const connection = createConnection(process.stdin, process.stdout);
7
+ startServer(connection);
8
+ connection.listen();
9
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1,13 @@
1
+ /** The innermost of `folders` that holds `file`, or undefined when none does. */
2
+ export declare function folderOf(file: string, folders: readonly string[]): string | undefined;
3
+ /** Which config each file belongs to: the nearest `stateward.config.*` above it, inside the workspace folders. */
4
+ export type ConfigLocator = {
5
+ /** The config file of `file`, or undefined when no config above it is inside a workspace folder. */
6
+ configOf(file: string): string | undefined;
7
+ /** The configs at the root of a workspace folder, in the folders' order: checked from the start. */
8
+ rootConfigs(): readonly string[];
9
+ /** A config file was created or deleted: look again. */
10
+ forget(): void;
11
+ };
12
+ export declare function createConfigLocator(folders: readonly string[]): ConfigLocator;
13
+ //# sourceMappingURL=configs.d.ts.map
@@ -0,0 +1,53 @@
1
+ import path from "node:path";
2
+ import { findConfigFile, findConfigUpward, toTypeScriptPath } from "@stateward/core/internal";
3
+ /** Whether `file` is `folder` or inside it (paths compared as TypeScript writes them). */
4
+ function isInside(file, folder) {
5
+ const relative = path.relative(folder, file);
6
+ return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
7
+ }
8
+ /** The innermost of `folders` that holds `file`, or undefined when none does. */
9
+ export function folderOf(file, folders) {
10
+ return folders
11
+ .filter((folder) => isInside(file, folder))
12
+ .sort((a, b) => b.length - a.length)
13
+ .at(0);
14
+ }
15
+ /**
16
+ * The outermost of `folders` that holds `file`: where the config search stops. With nested
17
+ * workspace folders (`/repo` and `/repo/packages/app`), a file of the inner one still finds the
18
+ * config at `/repo`.
19
+ */
20
+ function outermostFolderOf(file, folders) {
21
+ return folders
22
+ .filter((folder) => isInside(file, folder))
23
+ .sort((a, b) => a.length - b.length)
24
+ .at(0);
25
+ }
26
+ export function createConfigLocator(folders) {
27
+ /** By folder: the config that folder's files belong to (null: none), so a lookup walks once. */
28
+ const known = new Map();
29
+ return {
30
+ configOf(file) {
31
+ const folder = outermostFolderOf(file, folders);
32
+ if (folder === undefined)
33
+ return undefined;
34
+ const dir = toTypeScriptPath(path.dirname(file));
35
+ let found = known.get(dir);
36
+ if (found === undefined) {
37
+ found = findConfigUpward(path.dirname(file), folder) ?? null;
38
+ known.set(dir, found);
39
+ }
40
+ return found ?? undefined;
41
+ },
42
+ rootConfigs() {
43
+ return folders.flatMap((folder) => {
44
+ const found = findConfigFile(folder);
45
+ return found === undefined ? [] : [found];
46
+ });
47
+ },
48
+ forget() {
49
+ known.clear();
50
+ },
51
+ };
52
+ }
53
+ //# sourceMappingURL=configs.js.map
@@ -0,0 +1,11 @@
1
+ import type { Config } from "@stateward/core/internal";
2
+ /** The `stateward/explain` request's params are not `{ ruleId: string }`. */
3
+ export declare class InvalidExplainParamsError extends Error {
4
+ constructor();
5
+ }
6
+ /**
7
+ * The markdown for the rule id in `params`; null for an id no rule has, or when there is no config
8
+ * (the folder is not set up); a lazy rule's module is loaded for it. It is the text of the installed version, so it works offline.
9
+ */
10
+ export declare function answerExplain(config: Config | undefined, params: unknown): Promise<string | null>;
11
+ //# sourceMappingURL=explain.d.ts.map
@@ -0,0 +1,22 @@
1
+ import { explain } from "@stateward/core/internal";
2
+ /** The `stateward/explain` request's params are not `{ ruleId: string }`. */
3
+ export class InvalidExplainParamsError extends Error {
4
+ constructor() {
5
+ super("stateward/explain expects { ruleId: string }");
6
+ this.name = "InvalidExplainParamsError";
7
+ }
8
+ }
9
+ /**
10
+ * The markdown for the rule id in `params`; null for an id no rule has, or when there is no config
11
+ * (the folder is not set up); a lazy rule's module is loaded for it. It is the text of the installed version, so it works offline.
12
+ */
13
+ export async function answerExplain(config, params) {
14
+ if (typeof params !== "object" || params === null || !("ruleId" in params)) {
15
+ throw new InvalidExplainParamsError();
16
+ }
17
+ const { ruleId } = params;
18
+ if (typeof ruleId !== "string")
19
+ throw new InvalidExplainParamsError();
20
+ return config === undefined ? null : ((await explain(config, ruleId)) ?? null);
21
+ }
22
+ //# sourceMappingURL=explain.js.map
@@ -0,0 +1,44 @@
1
+ import type { FixForm } from "@stateward/core/internal";
2
+ import type { CodeAction, Range, TextEdit, WorkspaceEdit } from "vscode-languageserver/node";
3
+ /**
4
+ * Where one finding is, and the quick fixes it offers (the first is the one "fix all" applies).
5
+ * `extra` are offered after them but never applied by "fix all": "Ignore this case here" is a
6
+ * decision for a person, not a fix.
7
+ */
8
+ export type FixSite = {
9
+ range: Range;
10
+ actions: readonly CodeAction[];
11
+ extra?: readonly CodeAction[];
12
+ /** The fix of the first action as core offered it, for verifying the fixes of a file together. */
13
+ form?: FixForm;
14
+ };
15
+ /**
16
+ * Verifies the fixes "fix all" chose (by site index) as one set (`Checker.verifyFixSet`): which
17
+ * are kept, and the edits of this file that apply them, imports they leave unused removed.
18
+ */
19
+ export type VerifyFixSet = (chosen: readonly number[]) => {
20
+ kept: readonly boolean[];
21
+ edits: readonly TextEdit[];
22
+ };
23
+ /**
24
+ * One edit for the file `uri` that applies the first fix of every site, in file order. A fix goes
25
+ * in whole or not at all: it is left out when it edits another file, or clashes with a fix already
26
+ * taken (the earlier site wins). With `verify`, the fixes taken are then tried together: one
27
+ * that breaks with the others is left out, and imports the set leaves unused are removed. `count` is the number of fixes taken.
28
+ */
29
+ export declare function fixAllEdits(sites: readonly FixSite[], uri: string, verify?: VerifyFixSet): {
30
+ edits: readonly TextEdit[];
31
+ count: number;
32
+ };
33
+ /** Whether a code action request with these `only` kinds wants an action of `kind` (`source` covers `source.fixAll`). */
34
+ export declare function kindWanted(only: readonly string[] | undefined, kind: string): boolean;
35
+ /**
36
+ * The same edit as `documentChanges`, each document with the version the fixes were computed
37
+ * against (`null`: the file is not open, so there is no version to check). The editor rejects an
38
+ * edit whose version is not the document's current one, so a fix computed before an edit can
39
+ * never be applied to the text after it.
40
+ */
41
+ export declare function versionedEdit(changes: Readonly<Record<string, readonly TextEdit[]>>, versionOf: (uri: string) => number | null): WorkspaceEdit;
42
+ /** `action` with its `changes` edit turned into versioned `documentChanges`; unchanged when it has none. */
43
+ export declare function versionedAction(action: CodeAction, versionOf: (uri: string) => number | null): CodeAction;
44
+ //# sourceMappingURL=fix-all.d.ts.map
@@ -0,0 +1,92 @@
1
+ function compare(a, b) {
2
+ return a.line - b.line || a.character - b.character;
3
+ }
4
+ /**
5
+ * Edits clash when their ranges share a character, or both insert at one point; two equal edits
6
+ * (two fixes adding the same import) do not, and are made once.
7
+ */
8
+ function clashes(a, b) {
9
+ if (a.newText === b.newText && compare(a.range.start, b.range.start) === 0) {
10
+ if (compare(a.range.end, b.range.end) === 0)
11
+ return false;
12
+ }
13
+ return clash(a.range, b.range);
14
+ }
15
+ function clash(a, b) {
16
+ if (compare(a.start, a.end) === 0 && compare(b.start, b.end) === 0) {
17
+ return compare(a.start, b.start) === 0;
18
+ }
19
+ return compare(a.start, b.end) < 0 && compare(b.start, a.end) < 0;
20
+ }
21
+ function independent(edits) {
22
+ return edits.every((edit, i) => edits.slice(i + 1).every((other) => !clash(edit.range, other.range)));
23
+ }
24
+ /**
25
+ * One edit for the file `uri` that applies the first fix of every site, in file order. A fix goes
26
+ * in whole or not at all: it is left out when it edits another file, or clashes with a fix already
27
+ * taken (the earlier site wins). With `verify`, the fixes taken are then tried together: one
28
+ * that breaks with the others is left out, and imports the set leaves unused are removed. `count` is the number of fixes taken.
29
+ */
30
+ export function fixAllEdits(sites, uri, verify) {
31
+ const taken = [];
32
+ const chosen = [];
33
+ for (const [index, site] of sites.entries()) {
34
+ const changes = site.actions[0]?.edit?.changes;
35
+ if (changes === undefined)
36
+ continue;
37
+ const files = Object.keys(changes);
38
+ const edits = changes[uri];
39
+ if (files.length !== 1 || edits === undefined || edits.length === 0)
40
+ continue;
41
+ if (!independent(edits))
42
+ continue;
43
+ if (edits.some((edit) => taken.some((other) => clashes(edit, other))))
44
+ continue;
45
+ taken.push(...edits.filter((edit) => !taken.some((other) => same(edit, other))));
46
+ chosen.push(index);
47
+ }
48
+ if (verify === undefined || chosen.length === 0) {
49
+ return {
50
+ edits: taken.sort((a, b) => compare(a.range.start, b.range.start)),
51
+ count: chosen.length,
52
+ };
53
+ }
54
+ const review = verify(chosen);
55
+ return {
56
+ edits: [...review.edits].sort((a, b) => compare(a.range.start, b.range.start)),
57
+ count: review.kept.filter((kept) => kept).length,
58
+ };
59
+ }
60
+ function same(a, b) {
61
+ return (a.newText === b.newText &&
62
+ compare(a.range.start, b.range.start) === 0 &&
63
+ compare(a.range.end, b.range.end) === 0);
64
+ }
65
+ /** Whether a code action request with these `only` kinds wants an action of `kind` (`source` covers `source.fixAll`). */
66
+ export function kindWanted(only, kind) {
67
+ if (only === undefined || only.length === 0)
68
+ return true;
69
+ return only.some((wanted) => kind === wanted || kind.startsWith(`${wanted}.`));
70
+ }
71
+ /**
72
+ * The same edit as `documentChanges`, each document with the version the fixes were computed
73
+ * against (`null`: the file is not open, so there is no version to check). The editor rejects an
74
+ * edit whose version is not the document's current one, so a fix computed before an edit can
75
+ * never be applied to the text after it.
76
+ */
77
+ export function versionedEdit(changes, versionOf) {
78
+ return {
79
+ documentChanges: Object.entries(changes).map(([uri, edits]) => ({
80
+ textDocument: { uri, version: versionOf(uri) },
81
+ edits: [...edits],
82
+ })),
83
+ };
84
+ }
85
+ /** `action` with its `changes` edit turned into versioned `documentChanges`; unchanged when it has none. */
86
+ export function versionedAction(action, versionOf) {
87
+ const changes = action.edit?.changes;
88
+ if (changes === undefined)
89
+ return action;
90
+ return { ...action, edit: versionedEdit(changes, versionOf) };
91
+ }
92
+ //# sourceMappingURL=fix-all.js.map
@@ -0,0 +1,2 @@
1
+ export { startServer } from "./server.js";
2
+ //# sourceMappingURL=index.d.ts.map
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { startServer } from "./server.js";
2
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,22 @@
1
+ import { type InlayHint } from "vscode-languageserver/node";
2
+ import type { EditorHint } from "@stateward/core/internal";
3
+ import { type LineIndex } from "./mapping.js";
4
+ /**
5
+ * Which kinds of inline hint are on, by the kind a plugin gives its hints
6
+ * (`stateward.inlayHints.<kind>` in the editor's settings). A kind the settings do not mention is
7
+ * on.
8
+ */
9
+ export type HintSettings = ReadonlyMap<string, boolean>;
10
+ export declare const DEFAULT_HINT_SETTINGS: HintSettings;
11
+ /**
12
+ * The hint settings from the `stateward` section of the editor's settings
13
+ * (`{ inlayHints: { throws: false } }`); anything else is "all on".
14
+ */
15
+ export declare function readHintSettings(section: unknown): HintSettings;
16
+ export declare function hintEnabled(settings: HintSettings, kind: string): boolean;
17
+ /** The LSP hints of the plugins' hints that fall inside `range` (offsets, end excluded), as plain type labels. */
18
+ export declare function toInlayHints(hints: readonly EditorHint[], index: LineIndex, range: {
19
+ start: number;
20
+ end: number;
21
+ }): InlayHint[];
22
+ //# sourceMappingURL=inlay-hints.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { InlayHintKind, MarkupKind } from "vscode-languageserver/node";
2
+ import { positionAt } from "./mapping.js";
3
+ export const DEFAULT_HINT_SETTINGS = new Map();
4
+ function isRecord(value) {
5
+ return typeof value === "object" && value !== null && !Array.isArray(value);
6
+ }
7
+ /**
8
+ * The hint settings from the `stateward` section of the editor's settings
9
+ * (`{ inlayHints: { throws: false } }`); anything else is "all on".
10
+ */
11
+ export function readHintSettings(section) {
12
+ const hints = isRecord(section) ? section.inlayHints : undefined;
13
+ if (!isRecord(hints))
14
+ return DEFAULT_HINT_SETTINGS;
15
+ return new Map(Object.entries(hints).flatMap(([kind, on]) => (typeof on === "boolean" ? [[kind, on]] : [])));
16
+ }
17
+ export function hintEnabled(settings, kind) {
18
+ return settings.get(kind) !== false;
19
+ }
20
+ /** The LSP hints of the plugins' hints that fall inside `range` (offsets, end excluded), as plain type labels. */
21
+ export function toInlayHints(hints, index, range) {
22
+ return hints
23
+ .filter((hint) => hint.offset >= range.start && hint.offset <= range.end)
24
+ .map((hint) => ({
25
+ position: positionAt(index, hint.offset),
26
+ label: hint.label,
27
+ kind: InlayHintKind.Type,
28
+ ...(hint.paddingLeft === true ? { paddingLeft: true } : {}),
29
+ ...(hint.tooltip === undefined
30
+ ? {}
31
+ : { tooltip: { kind: MarkupKind.Markdown, value: hint.tooltip } }),
32
+ }));
33
+ }
34
+ //# sourceMappingURL=inlay-hints.js.map
@@ -0,0 +1,52 @@
1
+ import { DiagnosticSeverity, type CodeAction, type Diagnostic, type Position, type Range, type SemanticTokens, type TextEdit as LspTextEdit } from "vscode-languageserver";
2
+ import type { Finding, HiddenFindings, Location, Severity, TextEdit, TokenType } from "@stateward/core/internal";
3
+ /** Where each line of a text starts, so offsets and positions convert without rescanning. */
4
+ export type LineIndex = {
5
+ readonly starts: readonly number[];
6
+ readonly length: number;
7
+ };
8
+ export declare function buildLineIndex(text: string): LineIndex;
9
+ /** The 0-based line and UTF-16 character of `offset`, clamped to the text. */
10
+ export declare function positionAt(index: LineIndex, offset: number): Position;
11
+ /** The offset of `position`, clamped to the end of its line (and to the text). */
12
+ export declare function offsetAt(index: LineIndex, position: Position): number;
13
+ export declare function toRange(index: LineIndex, location: Location): Range;
14
+ export declare function toDiagnosticSeverity(severity: Severity): DiagnosticSeverity;
15
+ export type FileMapper = {
16
+ uriOf: (file: string) => string;
17
+ indexOf: (file: string) => LineIndex;
18
+ /** The text of a file as the editor has it. */
19
+ textOf: (file: string) => string;
20
+ };
21
+ /**
22
+ * A finding the baseline accepts is a Hint (shown faintly, not in the Problems count) whatever
23
+ * its severity, and its message starts with `baselined: `.
24
+ */
25
+ export declare function toDiagnostic(finding: Finding, files: FileMapper, baselined?: boolean): Diagnostic;
26
+ /** An information at the start of the file for findings that are counted but not listed. */
27
+ export declare function toHiddenDiagnostic(hidden: HiddenFindings): Diagnostic;
28
+ /** Core's edits as LSP edits, by document URI. */
29
+ export declare function toLspChanges(edits: readonly TextEdit[], files: FileMapper): Record<string, LspTextEdit[]>;
30
+ /**
31
+ * One quickfix per fix of a problem. Give it a finding whose fixes were tried (`reviewFixes`): a
32
+ * fix that was not is offered as it is.
33
+ */
34
+ export declare function toCodeActions(finding: Finding, diagnostic: Diagnostic, files: FileMapper): CodeAction[];
35
+ /**
36
+ * "Ignore this case here" (core's `ignoreFix`): offered for any finding that a comment can cover,
37
+ * after the fixes, and never applied by "fix all".
38
+ */
39
+ export declare function toIgnoreAction(finding: Finding, diagnostic: Diagnostic, files: FileMapper): CodeAction | undefined;
40
+ /** Ranges that overlap or touch. */
41
+ export declare function rangesIntersect(a: Range, b: Range): boolean;
42
+ /** The LSP semantic token legend: the TokenType list, in index order. */
43
+ export declare const TOKEN_TYPES: readonly string[];
44
+ /**
45
+ * Encodes tokens for LSP. Tokens are sorted by position; a token that spans lines is cut at the
46
+ * end of its first line, because LSP semantic tokens are single-line.
47
+ */
48
+ export declare function encodeTokens(tokens: readonly {
49
+ location: Location;
50
+ type: TokenType;
51
+ }[], index: LineIndex): SemanticTokens;
52
+ //# sourceMappingURL=mapping.d.ts.map