@mrclrchtr/supi-lsp 0.1.0 → 1.1.2

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.
Files changed (72) hide show
  1. package/README.md +112 -0
  2. package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
  3. package/node_modules/@mrclrchtr/supi-core/package.json +26 -0
  4. package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
  5. package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
  6. package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
  7. package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
  8. package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
  9. package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
  10. package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
  11. package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
  12. package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
  13. package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
  14. package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
  15. package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
  16. package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
  17. package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
  18. package/package.json +16 -11
  19. package/{capabilities.ts → src/capabilities.ts} +8 -0
  20. package/src/client/client-refresh.ts +229 -0
  21. package/{client.ts → src/client/client.ts} +178 -30
  22. package/{transport.ts → src/client/transport.ts} +10 -6
  23. package/src/config.ts +143 -0
  24. package/src/defaults.json +82 -0
  25. package/src/diagnostics/diagnostic-augmentation.ts +82 -0
  26. package/src/diagnostics/diagnostic-display.ts +68 -0
  27. package/{diagnostic-summary.ts → src/diagnostics/diagnostic-summary.ts} +11 -7
  28. package/{diagnostics.ts → src/diagnostics/diagnostics.ts} +9 -4
  29. package/src/diagnostics/stale-diagnostics.ts +47 -0
  30. package/src/diagnostics/suppression-diagnostics.ts +58 -0
  31. package/src/format.ts +359 -0
  32. package/src/guidance.ts +163 -0
  33. package/src/index.ts +17 -0
  34. package/src/lsp-state.ts +82 -0
  35. package/src/lsp.ts +481 -0
  36. package/src/manager/manager-client-state.ts +34 -0
  37. package/src/manager/manager-diagnostics.ts +139 -0
  38. package/src/manager/manager-helpers.ts +39 -0
  39. package/src/manager/manager-project-info.ts +46 -0
  40. package/src/manager/manager-stale-resync.ts +47 -0
  41. package/src/manager/manager-types.ts +39 -0
  42. package/src/manager/manager-workspace-recovery.ts +83 -0
  43. package/src/manager/manager-workspace-symbol.ts +18 -0
  44. package/src/manager/manager.ts +550 -0
  45. package/src/overrides.ts +173 -0
  46. package/src/pattern-matcher.ts +197 -0
  47. package/src/renderer.ts +120 -0
  48. package/src/scanner.ts +153 -0
  49. package/src/search-fallback.ts +98 -0
  50. package/src/service-registry.ts +153 -0
  51. package/src/settings-registration.ts +292 -0
  52. package/{summary.ts → src/summary.ts} +44 -9
  53. package/src/tool-actions.ts +430 -0
  54. package/src/tree-persist.ts +48 -0
  55. package/src/tsconfig-scope.ts +156 -0
  56. package/{types.ts → src/types.ts} +123 -0
  57. package/src/ui.ts +358 -0
  58. package/{utils.ts → src/utils.ts} +8 -25
  59. package/src/workspace-sentinels.ts +114 -0
  60. package/bash-guard.ts +0 -58
  61. package/config.ts +0 -99
  62. package/defaults.json +0 -40
  63. package/format.ts +0 -190
  64. package/guidance.ts +0 -140
  65. package/lsp.ts +0 -375
  66. package/manager.ts +0 -396
  67. package/overrides.ts +0 -95
  68. package/recent-paths.ts +0 -126
  69. package/runtime-state.ts +0 -113
  70. package/tool-actions.ts +0 -211
  71. package/tsconfig.json +0 -5
  72. package/ui.ts +0 -303
package/runtime-state.ts DELETED
@@ -1,113 +0,0 @@
1
- // Runtime LSP guidance state — tracks qualifying source interactions and
2
- // computes stateful pre-turn guidance so runtime guidance stays dormant
3
- // until the session actually touches supported source files.
4
-
5
- import { existsSync } from "node:fs";
6
- import * as path from "node:path";
7
- import {
8
- buildRuntimeLspGuidance,
9
- computeTrackedDiagnosticsSummary,
10
- type RuntimeGuidanceInput,
11
- } from "./guidance.ts";
12
- import type { LspManager } from "./manager.ts";
13
- import { getRawFilePathFromToolEvent } from "./recent-paths.ts";
14
- import { displayRelativeFilePath } from "./summary.ts";
15
-
16
- export const MAX_TRACKED_SOURCE_PATHS = 8;
17
-
18
- export interface LspRuntimeGuidanceState {
19
- runtimeActive: boolean;
20
- trackedSourcePaths: string[];
21
- pendingActivation: boolean;
22
- lastInjectedFingerprint: string | null;
23
- }
24
-
25
- export function createRuntimeGuidanceState(): LspRuntimeGuidanceState {
26
- return {
27
- runtimeActive: false,
28
- trackedSourcePaths: [],
29
- pendingActivation: false,
30
- lastInjectedFingerprint: null,
31
- };
32
- }
33
-
34
- export function resetRuntimeGuidanceState(state: LspRuntimeGuidanceState): void {
35
- state.runtimeActive = false;
36
- state.trackedSourcePaths = [];
37
- state.pendingActivation = false;
38
- state.lastInjectedFingerprint = null;
39
- }
40
-
41
- export function registerQualifyingSourceInteraction(
42
- state: LspRuntimeGuidanceState,
43
- manager: LspManager,
44
- toolName: string,
45
- input: Record<string, unknown>,
46
- ): void {
47
- const rawPath = getRawFilePathFromToolEvent(toolName, input);
48
- if (!rawPath) return;
49
- if (!manager.isSupportedSourceFile(rawPath)) return;
50
-
51
- // displayRelativeFilePath is the same form diagnostics get keyed under, so
52
- // the tracked-files list lines up with diagnostic relevance matching for
53
- // both in-tree files (relative form) and out-of-tree absolute paths.
54
- const trackedPath = displayRelativeFilePath(rawPath);
55
-
56
- // pendingActivation is a one-shot signal: set only on the first qualifying
57
- // interaction so the next turn can inject the "LSP ready" hint exactly once.
58
- // Subsequent interactions keep tracking files but must not re-arm activation
59
- // — the caller clears the flag after injecting.
60
- const wasDormant = !state.runtimeActive;
61
- state.runtimeActive = true;
62
-
63
- if (wasDormant) {
64
- state.pendingActivation = true;
65
- }
66
-
67
- state.trackedSourcePaths = [
68
- trackedPath,
69
- ...state.trackedSourcePaths.filter((entry) => entry !== trackedPath),
70
- ].slice(0, MAX_TRACKED_SOURCE_PATHS);
71
- }
72
-
73
- /**
74
- * Drop tracked source paths whose underlying file is gone (deleted/renamed).
75
- * Without this, `pruneMissingFiles()` cleans the live LSP clients but the
76
- * runtime guidance would keep advertising the stale path on subsequent turns,
77
- * and the session couldn't return to a dormant state until other interactions
78
- * evicted the entry. Tracked paths are in `displayRelativeFilePath` form so
79
- * `path.resolve` transparently handles both in-tree relative and out-of-tree
80
- * absolute entries.
81
- */
82
- export function pruneMissingTrackedPaths(state: LspRuntimeGuidanceState): void {
83
- if (state.trackedSourcePaths.length === 0) return;
84
- const surviving = state.trackedSourcePaths.filter((entry) => existsSync(path.resolve(entry)));
85
- if (surviving.length === state.trackedSourcePaths.length) return;
86
- state.trackedSourcePaths = surviving;
87
- if (surviving.length === 0) {
88
- state.runtimeActive = false;
89
- state.pendingActivation = false;
90
- }
91
- }
92
-
93
- export function computePendingRuntimeGuidance(
94
- state: LspRuntimeGuidanceState,
95
- manager: LspManager,
96
- inlineSeverity: number,
97
- ): { input: RuntimeGuidanceInput; content: string | null } | null {
98
- if (!state.runtimeActive) return null;
99
-
100
- const diagnosticsSummary = computeTrackedDiagnosticsSummary(
101
- manager,
102
- inlineSeverity,
103
- state.trackedSourcePaths,
104
- );
105
-
106
- const input: RuntimeGuidanceInput = {
107
- pendingActivation: state.pendingActivation,
108
- diagnosticsSummary,
109
- trackedFiles: state.trackedSourcePaths,
110
- };
111
-
112
- return { input, content: buildRuntimeLspGuidance(input) };
113
- }
package/tool-actions.ts DELETED
@@ -1,211 +0,0 @@
1
- // LSP tool action implementations — dispatches agent tool calls to LSP clients.
2
-
3
- import * as fs from "node:fs";
4
- import * as path from "node:path";
5
- import { formatDiagnostics } from "./diagnostics.ts";
6
- import {
7
- formatCodeActions,
8
- formatDocumentSymbols,
9
- formatHover,
10
- formatLocations,
11
- formatSymbolInformation,
12
- formatWorkspaceEdit,
13
- normalizeLocations,
14
- } from "./format.ts";
15
- import type { LspManager } from "./manager.ts";
16
- import type { DocumentSymbol, Range, SymbolInformation } from "./types.ts";
17
-
18
- // ── Types ─────────────────────────────────────────────────────────────
19
-
20
- export type LspAction =
21
- | "hover"
22
- | "definition"
23
- | "references"
24
- | "diagnostics"
25
- | "symbols"
26
- | "rename"
27
- | "code_actions";
28
-
29
- export interface LspToolParams {
30
- action: LspAction;
31
- file?: string;
32
- line?: number;
33
- character?: number;
34
- newName?: string;
35
- }
36
-
37
- // ── Tool Description ──────────────────────────────────────────────────
38
-
39
- export const lspToolDescription = `Language Server Protocol tool — provides type-aware code intelligence.
40
-
41
- Actions:
42
- - hover: Get type info and docs at a position. Params: file, line, character
43
- - definition: Go to definition of a symbol. Params: file, line, character
44
- - references: Find all references to a symbol. Params: file, line, character
45
- - diagnostics: Get type errors and warnings. Params: file (optional — omit for all files)
46
- - symbols: List all symbols in a file. Params: file
47
- - rename: Rename a symbol across the project. Params: file, line, character, newName
48
- - code_actions: Get available fixes/refactors at a position. Params: file, line, character
49
-
50
- Line and character are 1-based. File paths are relative to cwd.`;
51
-
52
- // ── Action Dispatcher ─────────────────────────────────────────────────
53
-
54
- export async function executeAction(manager: LspManager, params: LspToolParams): Promise<string> {
55
- switch (params.action) {
56
- case "hover":
57
- return handleHover(manager, params);
58
- case "definition":
59
- return handleDefinition(manager, params);
60
- case "references":
61
- return handleReferences(manager, params);
62
- case "diagnostics":
63
- return handleDiagnostics(manager, params);
64
- case "symbols":
65
- return handleSymbols(manager, params);
66
- case "rename":
67
- return handleRename(manager, params);
68
- case "code_actions":
69
- return handleCodeActions(manager, params);
70
- default:
71
- return `Unknown action: ${params.action}`;
72
- }
73
- }
74
-
75
- // ── Action Handlers ───────────────────────────────────────────────────
76
-
77
- async function handleHover(manager: LspManager, params: LspToolParams): Promise<string> {
78
- const { file, line, character } = requireFilePosition(params);
79
- const client = await manager.ensureFileOpen(file);
80
- if (!client) return noServerMessage(file);
81
-
82
- const hover = await client.hover(path.resolve(file), toZeroBased(line, character));
83
- if (!hover) return "No hover information available at this position.";
84
- return formatHover(hover);
85
- }
86
-
87
- async function handleDefinition(manager: LspManager, params: LspToolParams): Promise<string> {
88
- const { file, line, character } = requireFilePosition(params);
89
- const client = await manager.ensureFileOpen(file);
90
- if (!client) return noServerMessage(file);
91
-
92
- const result = await client.definition(path.resolve(file), toZeroBased(line, character));
93
- if (!result) return "No definition found.";
94
-
95
- const locations = normalizeLocations(result);
96
- if (locations.length === 0) return "No definition found.";
97
-
98
- return formatLocations("Definition", locations);
99
- }
100
-
101
- async function handleReferences(manager: LspManager, params: LspToolParams): Promise<string> {
102
- const { file, line, character } = requireFilePosition(params);
103
- const client = await manager.ensureFileOpen(file);
104
- if (!client) return noServerMessage(file);
105
-
106
- const locations = await client.references(path.resolve(file), toZeroBased(line, character));
107
- if (!locations || locations.length === 0) return "No references found.";
108
-
109
- return formatLocations("References", locations);
110
- }
111
-
112
- async function handleDiagnostics(manager: LspManager, params: LspToolParams): Promise<string> {
113
- if (params.file) {
114
- const resolvedPath = path.resolve(params.file);
115
- const client = await manager.ensureFileOpen(params.file);
116
- if (!client) return noServerMessage(params.file);
117
-
118
- let content: string;
119
- try {
120
- content = fs.readFileSync(resolvedPath, "utf-8");
121
- } catch {
122
- return `Error: cannot read file ${params.file}`;
123
- }
124
-
125
- const diags = await client.syncAndWaitForDiagnostics(resolvedPath, content);
126
- return formatDiagnostics(params.file, diags);
127
- }
128
-
129
- const summary = manager.getDiagnosticSummary();
130
- if (summary.length === 0) return "No diagnostics across any files.";
131
-
132
- const lines = ["## Diagnostics Summary\n"];
133
- for (const s of summary) {
134
- lines.push(`- **${s.file}**: ${s.errors} error(s), ${s.warnings} warning(s)`);
135
- }
136
- return lines.join("\n");
137
- }
138
-
139
- async function handleSymbols(manager: LspManager, params: LspToolParams): Promise<string> {
140
- if (!params.file) return "Error: 'file' parameter is required for symbols action.";
141
-
142
- const client = await manager.ensureFileOpen(params.file);
143
- if (!client) return noServerMessage(params.file);
144
-
145
- const symbols = await client.documentSymbols(path.resolve(params.file));
146
- if (!symbols || symbols.length === 0) return "No symbols found.";
147
-
148
- if ("children" in symbols[0] || "selectionRange" in symbols[0]) {
149
- return formatDocumentSymbols(symbols as DocumentSymbol[], 0);
150
- }
151
- return formatSymbolInformation(symbols as SymbolInformation[]);
152
- }
153
-
154
- async function handleRename(manager: LspManager, params: LspToolParams): Promise<string> {
155
- const { file, line, character } = requireFilePosition(params);
156
- if (!params.newName) return "Error: 'newName' parameter is required for rename action.";
157
-
158
- const client = await manager.ensureFileOpen(file);
159
- if (!client) return noServerMessage(file);
160
-
161
- const edit = await client.rename(
162
- path.resolve(file),
163
- toZeroBased(line, character),
164
- params.newName,
165
- );
166
- if (!edit) return "Rename not available at this position.";
167
-
168
- return formatWorkspaceEdit(edit);
169
- }
170
-
171
- async function handleCodeActions(manager: LspManager, params: LspToolParams): Promise<string> {
172
- const { file, line, character } = requireFilePosition(params);
173
- const client = await manager.ensureFileOpen(file);
174
- if (!client) return noServerMessage(file);
175
-
176
- const pos = toZeroBased(line, character);
177
- const range: Range = { start: pos, end: pos };
178
- const diags = client.getDiagnostics(path.resolve(file));
179
-
180
- const relevantDiags = diags.filter(
181
- (d) => d.range.start.line <= pos.line && d.range.end.line >= pos.line,
182
- );
183
-
184
- const actions = await client.codeActions(path.resolve(file), range, {
185
- diagnostics: relevantDiags,
186
- });
187
- if (!actions || actions.length === 0) return "No code actions available at this position.";
188
-
189
- return formatCodeActions(actions);
190
- }
191
-
192
- // ── Utility ───────────────────────────────────────────────────────────
193
-
194
- function requireFilePosition(params: LspToolParams): {
195
- file: string;
196
- line: number;
197
- character: number;
198
- } {
199
- if (!params.file) throw new Error("'file' parameter is required.");
200
- if (params.line === undefined) throw new Error("'line' parameter is required.");
201
- if (params.character === undefined) throw new Error("'character' parameter is required.");
202
- return { file: params.file, line: params.line, character: params.character };
203
- }
204
-
205
- function toZeroBased(line: number, character: number): { line: number; character: number } {
206
- return { line: line - 1, character: character - 1 };
207
- }
208
-
209
- function noServerMessage(file: string): string {
210
- return `No LSP server available for this file type (${path.extname(file) || "unknown"})`;
211
- }
package/tsconfig.json DELETED
@@ -1,5 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "include": ["*.ts"],
4
- "exclude": ["__tests__"]
5
- }
package/ui.ts DELETED
@@ -1,303 +0,0 @@
1
- import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { DynamicBorder } from "@mariozechner/pi-coding-agent";
3
- import type { OverlayHandle } from "@mariozechner/pi-tui";
4
- import { Container, Spacer, Text } from "@mariozechner/pi-tui";
5
- import type {
6
- ActiveCoverageSummaryEntry,
7
- LspManager,
8
- OutstandingDiagnosticSummaryEntry,
9
- } from "./manager.ts";
10
-
11
- export interface LspInspectorState {
12
- handle: OverlayHandle | null;
13
- close: (() => void) | null;
14
- }
15
-
16
- export function updateLspUi(
17
- ctx: ExtensionContext,
18
- manager: LspManager,
19
- inlineSeverity: number,
20
- ): void {
21
- const activeCoverage = manager.getActiveCoverageSummary();
22
- const diagnostics = manager.getOutstandingDiagnosticSummary(inlineSeverity);
23
-
24
- ctx.ui.setStatus("lsp", buildLspStatus(ctx, activeCoverage, diagnostics));
25
- ctx.ui.setWidget(
26
- "lsp",
27
- hasWidgetContent(activeCoverage, diagnostics)
28
- ? (_tui, theme) => buildLspWidgetComponent(theme, activeCoverage, diagnostics)
29
- : undefined,
30
- { placement: "belowEditor" },
31
- );
32
- }
33
-
34
- export function toggleLspStatusOverlay(
35
- ctx: ExtensionContext,
36
- manager: LspManager,
37
- inlineSeverity: number,
38
- inspector: LspInspectorState,
39
- ): void {
40
- if (inspector.handle && inspector.close) {
41
- inspector.close();
42
- return;
43
- }
44
-
45
- void ctx.ui
46
- .custom<void>(
47
- (_tui, theme, _kb, done) => {
48
- inspector.close = () => done(undefined);
49
- return createLspInspectorComponent(theme, manager, inlineSeverity);
50
- },
51
- {
52
- overlay: true,
53
- overlayOptions: {
54
- anchor: "right-center",
55
- width: 52,
56
- maxHeight: "75%",
57
- margin: { right: 1, top: 1, bottom: 1 },
58
- nonCapturing: true,
59
- },
60
- onHandle: (handle) => {
61
- inspector.handle = handle;
62
- },
63
- },
64
- )
65
- .finally(() => {
66
- inspector.handle = null;
67
- inspector.close = null;
68
- });
69
- }
70
-
71
- function createLspInspectorComponent(
72
- theme: ExtensionContext["ui"]["theme"],
73
- manager: LspManager,
74
- inlineSeverity: number,
75
- ): { render: (width: number) => string[]; invalidate: () => void } {
76
- return {
77
- render: (width) => buildLspInspectorContainer(theme, manager, inlineSeverity).render(width),
78
- invalidate: () => {},
79
- };
80
- }
81
-
82
- function buildLspInspectorContainer(
83
- theme: ExtensionContext["ui"]["theme"],
84
- manager: LspManager,
85
- inlineSeverity: number,
86
- ): Container {
87
- const activeCoverage = manager.getActiveCoverageSummary();
88
- const diagnostics = manager.getOutstandingDiagnosticSummary(inlineSeverity);
89
- const container = new Container();
90
-
91
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
92
- container.addChild(
93
- new Text(
94
- theme.fg("accent", theme.bold(" λ LSP")) + theme.fg("dim", " inspector /lsp-status toggles"),
95
- 1,
96
- 0,
97
- ),
98
- );
99
-
100
- if (isQuietInspectorState(activeCoverage, diagnostics)) {
101
- container.addChild(
102
- new Text(theme.fg("success", "clean") + theme.fg("dim", " • no active servers"), 1, 0),
103
- );
104
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
105
- return container;
106
- }
107
-
108
- container.addChild(new Text(buildOverlaySummaryLine(theme, activeCoverage, diagnostics), 1, 0));
109
- container.addChild(new Spacer(1));
110
- container.addChild(
111
- buildOverlaySection(theme, "Coverage", buildOverlayCoverageLines(theme, activeCoverage)),
112
- );
113
- container.addChild(new Spacer(1));
114
- container.addChild(
115
- buildOverlaySection(
116
- theme,
117
- diagnostics.length > 0 ? "Problems" : "Diagnostics",
118
- buildOverlayDiagnosticLines(theme, diagnostics),
119
- ),
120
- );
121
- container.addChild(new Spacer(1));
122
- container.addChild(new DynamicBorder((s: string) => theme.fg("accent", s)));
123
-
124
- return container;
125
- }
126
-
127
- function isQuietInspectorState(
128
- activeCoverage: ActiveCoverageSummaryEntry[],
129
- diagnostics: OutstandingDiagnosticSummaryEntry[],
130
- ): boolean {
131
- return activeCoverage.length === 0 && diagnostics.length === 0;
132
- }
133
-
134
- function buildLspStatus(
135
- ctx: ExtensionContext,
136
- activeCoverage: ActiveCoverageSummaryEntry[],
137
- diagnostics: OutstandingDiagnosticSummaryEntry[],
138
- ): string | undefined {
139
- const activeServers = activeCoverage.length;
140
- const openFiles = activeCoverage.reduce((sum, entry) => sum + entry.openFiles.length, 0);
141
- const errors = diagnostics.reduce((sum, entry) => sum + entry.errors, 0);
142
- const warnings = diagnostics.reduce((sum, entry) => sum + entry.warnings, 0);
143
-
144
- if (activeServers === 0 && openFiles === 0 && errors === 0 && warnings === 0) {
145
- return undefined;
146
- }
147
-
148
- const { theme } = ctx.ui;
149
- const parts = [theme.fg("accent", "λ lsp")];
150
- if (activeServers > 0) parts.push(theme.fg("dim", pluralize(activeServers, "server")));
151
- if (openFiles > 0) parts.push(theme.fg("dim", pluralize(openFiles, "open file")));
152
- if (errors > 0) parts.push(theme.fg("error", pluralize(errors, "error")));
153
- if (warnings > 0) parts.push(theme.fg("warning", pluralize(warnings, "warning")));
154
- return parts.join(theme.fg("dim", " • "));
155
- }
156
-
157
- function hasWidgetContent(
158
- _activeCoverage: ActiveCoverageSummaryEntry[],
159
- diagnostics: OutstandingDiagnosticSummaryEntry[],
160
- ): boolean {
161
- return diagnostics.length > 0;
162
- }
163
-
164
- function buildLspWidgetComponent(
165
- theme: ExtensionContext["ui"]["theme"],
166
- _activeCoverage: ActiveCoverageSummaryEntry[],
167
- diagnostics: OutstandingDiagnosticSummaryEntry[],
168
- ): Container {
169
- const container = new Container();
170
-
171
- for (const line of buildWidgetDiagnosticLines(theme, diagnostics)) {
172
- container.addChild(new Text(line, 0, 0));
173
- }
174
-
175
- return container;
176
- }
177
-
178
- function buildWidgetDiagnosticLines(
179
- theme: ExtensionContext["ui"]["theme"],
180
- diagnostics: OutstandingDiagnosticSummaryEntry[],
181
- ): string[] {
182
- if (diagnostics.length === 1) {
183
- const [entry] = diagnostics;
184
- if (!entry) return [];
185
- return [
186
- `${theme.fg("error", "●")} ${entry.file} ${theme.fg("dim", `— ${formatDiagnosticCounts(entry)}`)}`,
187
- ];
188
- }
189
-
190
- const totalErrors = diagnostics.reduce((sum, entry) => sum + entry.errors, 0);
191
- const totalWarnings = diagnostics.reduce((sum, entry) => sum + entry.warnings, 0);
192
- const counts: string[] = [];
193
- if (totalErrors > 0) counts.push(theme.fg("error", pluralize(totalErrors, "error")));
194
- if (totalWarnings > 0) counts.push(theme.fg("warning", pluralize(totalWarnings, "warning")));
195
-
196
- const visibleFiles = diagnostics.slice(0, 2).map((entry) => entry.file);
197
- const remaining = diagnostics.length - visibleFiles.length;
198
- const suffix = remaining > 0 ? `${theme.fg("dim", ` +${remaining} more`)}` : "";
199
-
200
- return [
201
- `${theme.fg("accent", theme.bold("λ LSP diagnostics"))} ${theme.fg("dim", `— ${pluralize(diagnostics.length, "file")}`)} ${counts.join(theme.fg("dim", " • "))}`,
202
- `${theme.fg("error", "↳")} ${visibleFiles.join(", ")}${suffix}`,
203
- ];
204
- }
205
-
206
- function buildOverlaySummaryLine(
207
- theme: ExtensionContext["ui"]["theme"],
208
- activeCoverage: ActiveCoverageSummaryEntry[],
209
- diagnostics: OutstandingDiagnosticSummaryEntry[],
210
- ): string {
211
- const activeServers = activeCoverage.length;
212
- const openFiles = activeCoverage.reduce((sum, entry) => sum + entry.openFiles.length, 0);
213
- const errors = diagnostics.reduce((sum, entry) => sum + entry.errors, 0);
214
- const warnings = diagnostics.reduce((sum, entry) => sum + entry.warnings, 0);
215
-
216
- const parts = [
217
- theme.fg("dim", `${pluralize(activeServers, "server")} • ${pluralize(openFiles, "open file")}`),
218
- ];
219
- if (errors > 0) {
220
- parts.push(theme.fg("error", pluralize(errors, "error")));
221
- } else if (warnings > 0) {
222
- parts.push(theme.fg("warning", pluralize(warnings, "warning")));
223
- } else {
224
- parts.push(theme.fg("success", "clean"));
225
- }
226
-
227
- return parts.join(theme.fg("dim", " "));
228
- }
229
-
230
- function buildOverlaySection(
231
- theme: ExtensionContext["ui"]["theme"],
232
- title: string,
233
- lines: string[],
234
- ): Container {
235
- const container = new Container();
236
- container.addChild(new Text(theme.fg("accent", theme.bold(` ${title}`)), 1, 0));
237
- for (const line of lines) {
238
- container.addChild(new Text(line, 2, 0));
239
- }
240
- return container;
241
- }
242
-
243
- function buildOverlayCoverageLines(
244
- theme: ExtensionContext["ui"]["theme"],
245
- activeCoverage: ActiveCoverageSummaryEntry[],
246
- ): string[] {
247
- if (activeCoverage.length === 0) {
248
- return [theme.fg("dim", "no active LSP servers")];
249
- }
250
-
251
- return activeCoverage.flatMap((entry) => {
252
- const visibleFiles = entry.openFiles.slice(0, 2);
253
- const remainingFiles = entry.openFiles.length - visibleFiles.length;
254
- const fileLine = visibleFiles.length > 0 ? visibleFiles.join(", ") : "none";
255
- const suffix = remainingFiles > 0 ? theme.fg("dim", ` +${remainingFiles} more`) : "";
256
-
257
- return [
258
- `${theme.fg("accent", "◆")} ${entry.name} ${theme.fg("dim", `— ${pluralize(entry.openFiles.length, "file")}`)}`,
259
- `${theme.fg("dim", "↳")} ${fileLine}${suffix}`,
260
- ];
261
- });
262
- }
263
-
264
- function buildOverlayDiagnosticLines(
265
- theme: ExtensionContext["ui"]["theme"],
266
- diagnostics: OutstandingDiagnosticSummaryEntry[],
267
- ): string[] {
268
- if (diagnostics.length === 0) {
269
- return [theme.fg("success", "✓ no outstanding diagnostics")];
270
- }
271
-
272
- const lines = diagnostics
273
- .slice(0, 5)
274
- .map(
275
- (entry) =>
276
- `${theme.fg("error", "●")} ${entry.file} ${theme.fg("dim", `— ${formatDiagnosticCounts(entry)}`)}`,
277
- );
278
-
279
- const remainingDiagnostics = diagnostics.length - Math.min(diagnostics.length, 5);
280
- if (remainingDiagnostics > 0) {
281
- lines.push(
282
- theme.fg(
283
- "dim",
284
- `↳ +${remainingDiagnostics} more diagnostic file${remainingDiagnostics === 1 ? "" : "s"}`,
285
- ),
286
- );
287
- }
288
-
289
- return lines;
290
- }
291
-
292
- function formatDiagnosticCounts(entry: OutstandingDiagnosticSummaryEntry): string {
293
- const counts: string[] = [];
294
- if (entry.errors > 0) counts.push(pluralize(entry.errors, "error"));
295
- if (entry.warnings > 0) counts.push(pluralize(entry.warnings, "warning"));
296
- if (entry.information > 0) counts.push(pluralize(entry.information, "info"));
297
- if (entry.hints > 0) counts.push(pluralize(entry.hints, "hint"));
298
- return counts.join(", ");
299
- }
300
-
301
- function pluralize(count: number, word: string): string {
302
- return `${count} ${word}${count === 1 ? "" : "s"}`;
303
- }