@mrclrchtr/supi-lsp 0.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/bash-guard.ts +58 -0
- package/capabilities.ts +54 -0
- package/client.ts +397 -0
- package/config.ts +99 -0
- package/defaults.json +40 -0
- package/diagnostic-summary.ts +69 -0
- package/diagnostics.ts +93 -0
- package/format.ts +190 -0
- package/guidance.ts +140 -0
- package/lsp.ts +375 -0
- package/manager.ts +396 -0
- package/overrides.ts +95 -0
- package/package.json +36 -0
- package/recent-paths.ts +126 -0
- package/runtime-state.ts +113 -0
- package/summary.ts +118 -0
- package/tool-actions.ts +211 -0
- package/transport.ts +188 -0
- package/tsconfig.json +5 -0
- package/types.ts +286 -0
- package/ui.ts +303 -0
- package/utils.ts +139 -0
package/types.ts
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
// LSP protocol types — minimal subset needed for our client.
|
|
2
|
+
// Based on the Language Server Protocol specification.
|
|
3
|
+
|
|
4
|
+
// ── Positions & Ranges ────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
/** 0-based line and character offset. */
|
|
7
|
+
export interface Position {
|
|
8
|
+
line: number;
|
|
9
|
+
character: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface Range {
|
|
13
|
+
start: Position;
|
|
14
|
+
end: Position;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Location {
|
|
18
|
+
uri: string;
|
|
19
|
+
range: Range;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface LocationLink {
|
|
23
|
+
originSelectionRange?: Range;
|
|
24
|
+
targetUri: string;
|
|
25
|
+
targetRange: Range;
|
|
26
|
+
targetSelectionRange: Range;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Text Edits ────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
export interface TextEdit {
|
|
32
|
+
range: Range;
|
|
33
|
+
newText: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TextDocumentEdit {
|
|
37
|
+
textDocument: { uri: string; version?: number | null };
|
|
38
|
+
edits: TextEdit[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface WorkspaceEdit {
|
|
42
|
+
changes?: Record<string, TextEdit[]>;
|
|
43
|
+
documentChanges?: TextDocumentEdit[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── Diagnostics ───────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
export const DiagnosticSeverity = {
|
|
49
|
+
Error: 1,
|
|
50
|
+
Warning: 2,
|
|
51
|
+
Information: 3,
|
|
52
|
+
Hint: 4,
|
|
53
|
+
} as const;
|
|
54
|
+
export type DiagnosticSeverity = (typeof DiagnosticSeverity)[keyof typeof DiagnosticSeverity];
|
|
55
|
+
|
|
56
|
+
export interface DiagnosticRelatedInformation {
|
|
57
|
+
location: Location;
|
|
58
|
+
message: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface Diagnostic {
|
|
62
|
+
range: Range;
|
|
63
|
+
severity?: DiagnosticSeverity;
|
|
64
|
+
code?: number | string;
|
|
65
|
+
codeDescription?: { href: string };
|
|
66
|
+
source?: string;
|
|
67
|
+
message: string;
|
|
68
|
+
relatedInformation?: DiagnosticRelatedInformation[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// ── Hover ─────────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
export interface MarkupContent {
|
|
74
|
+
kind: "plaintext" | "markdown";
|
|
75
|
+
value: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export type MarkedString = string | { language: string; value: string };
|
|
79
|
+
|
|
80
|
+
export interface Hover {
|
|
81
|
+
contents: MarkupContent | MarkedString | MarkedString[];
|
|
82
|
+
range?: Range;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Symbols ───────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
export const SymbolKind = {
|
|
88
|
+
File: 1,
|
|
89
|
+
Module: 2,
|
|
90
|
+
Namespace: 3,
|
|
91
|
+
Package: 4,
|
|
92
|
+
Class: 5,
|
|
93
|
+
Method: 6,
|
|
94
|
+
Property: 7,
|
|
95
|
+
Field: 8,
|
|
96
|
+
Constructor: 9,
|
|
97
|
+
Enum: 10,
|
|
98
|
+
Interface: 11,
|
|
99
|
+
Function: 12,
|
|
100
|
+
Variable: 13,
|
|
101
|
+
Constant: 14,
|
|
102
|
+
String: 15,
|
|
103
|
+
Number: 16,
|
|
104
|
+
Boolean: 17,
|
|
105
|
+
Array: 18,
|
|
106
|
+
Object: 19,
|
|
107
|
+
Key: 20,
|
|
108
|
+
Null: 21,
|
|
109
|
+
EnumMember: 22,
|
|
110
|
+
Struct: 23,
|
|
111
|
+
Event: 24,
|
|
112
|
+
Operator: 25,
|
|
113
|
+
TypeParameter: 26,
|
|
114
|
+
} as const;
|
|
115
|
+
export type SymbolKind = (typeof SymbolKind)[keyof typeof SymbolKind];
|
|
116
|
+
|
|
117
|
+
export interface DocumentSymbol {
|
|
118
|
+
name: string;
|
|
119
|
+
detail?: string;
|
|
120
|
+
kind: SymbolKind;
|
|
121
|
+
range: Range;
|
|
122
|
+
selectionRange: Range;
|
|
123
|
+
children?: DocumentSymbol[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export interface SymbolInformation {
|
|
127
|
+
name: string;
|
|
128
|
+
kind: SymbolKind;
|
|
129
|
+
location: Location;
|
|
130
|
+
containerName?: string;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ── Code Actions ──────────────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
export interface CodeActionContext {
|
|
136
|
+
diagnostics: Diagnostic[];
|
|
137
|
+
only?: string[];
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface Command {
|
|
141
|
+
title: string;
|
|
142
|
+
command: string;
|
|
143
|
+
arguments?: unknown[];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface CodeAction {
|
|
147
|
+
title: string;
|
|
148
|
+
kind?: string;
|
|
149
|
+
diagnostics?: Diagnostic[];
|
|
150
|
+
isPreferred?: boolean;
|
|
151
|
+
edit?: WorkspaceEdit;
|
|
152
|
+
command?: Command;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ── Publish Diagnostics ───────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
export interface PublishDiagnosticsParams {
|
|
158
|
+
uri: string;
|
|
159
|
+
version?: number;
|
|
160
|
+
diagnostics: Diagnostic[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ── Initialize ────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
export interface InitializeParams {
|
|
166
|
+
processId: number | null;
|
|
167
|
+
rootUri: string | null;
|
|
168
|
+
capabilities: ClientCapabilities;
|
|
169
|
+
initializationOptions?: unknown;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface ClientCapabilities {
|
|
173
|
+
textDocument?: {
|
|
174
|
+
synchronization?: {
|
|
175
|
+
didSave?: boolean;
|
|
176
|
+
dynamicRegistration?: boolean;
|
|
177
|
+
};
|
|
178
|
+
hover?: {
|
|
179
|
+
contentFormat?: string[];
|
|
180
|
+
dynamicRegistration?: boolean;
|
|
181
|
+
};
|
|
182
|
+
definition?: {
|
|
183
|
+
dynamicRegistration?: boolean;
|
|
184
|
+
linkSupport?: boolean;
|
|
185
|
+
};
|
|
186
|
+
references?: {
|
|
187
|
+
dynamicRegistration?: boolean;
|
|
188
|
+
};
|
|
189
|
+
documentSymbol?: {
|
|
190
|
+
dynamicRegistration?: boolean;
|
|
191
|
+
hierarchicalDocumentSymbolSupport?: boolean;
|
|
192
|
+
};
|
|
193
|
+
rename?: {
|
|
194
|
+
dynamicRegistration?: boolean;
|
|
195
|
+
prepareSupport?: boolean;
|
|
196
|
+
};
|
|
197
|
+
codeAction?: {
|
|
198
|
+
dynamicRegistration?: boolean;
|
|
199
|
+
codeActionLiteralSupport?: {
|
|
200
|
+
codeActionKind: { valueSet: string[] };
|
|
201
|
+
};
|
|
202
|
+
};
|
|
203
|
+
publishDiagnostics?: {
|
|
204
|
+
relatedInformation?: boolean;
|
|
205
|
+
};
|
|
206
|
+
};
|
|
207
|
+
workspace?: {
|
|
208
|
+
workspaceFolders?: boolean;
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export interface InitializeResult {
|
|
213
|
+
capabilities: ServerCapabilities;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface ServerCapabilities {
|
|
217
|
+
textDocumentSync?: number | { openClose?: boolean; change?: number };
|
|
218
|
+
hoverProvider?: boolean;
|
|
219
|
+
definitionProvider?: boolean;
|
|
220
|
+
referencesProvider?: boolean;
|
|
221
|
+
documentSymbolProvider?: boolean;
|
|
222
|
+
renameProvider?: boolean | { prepareProvider?: boolean };
|
|
223
|
+
codeActionProvider?: boolean | { codeActionKinds?: string[] };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── Text Document Items ───────────────────────────────────────────────
|
|
227
|
+
|
|
228
|
+
export interface TextDocumentIdentifier {
|
|
229
|
+
uri: string;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export interface TextDocumentItem {
|
|
233
|
+
uri: string;
|
|
234
|
+
languageId: string;
|
|
235
|
+
version: number;
|
|
236
|
+
text: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface VersionedTextDocumentIdentifier {
|
|
240
|
+
uri: string;
|
|
241
|
+
version: number;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
export interface TextDocumentPositionParams {
|
|
245
|
+
textDocument: TextDocumentIdentifier;
|
|
246
|
+
position: Position;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ── JSON-RPC ──────────────────────────────────────────────────────────
|
|
250
|
+
|
|
251
|
+
export interface JsonRpcRequest {
|
|
252
|
+
jsonrpc: "2.0";
|
|
253
|
+
id: number;
|
|
254
|
+
method: string;
|
|
255
|
+
params?: unknown;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface JsonRpcResponse {
|
|
259
|
+
jsonrpc: "2.0";
|
|
260
|
+
id: number;
|
|
261
|
+
result?: unknown;
|
|
262
|
+
error?: { code: number; message: string; data?: unknown };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export interface JsonRpcNotification {
|
|
266
|
+
jsonrpc: "2.0";
|
|
267
|
+
method: string;
|
|
268
|
+
params?: unknown;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
|
|
272
|
+
|
|
273
|
+
// ── Server Configuration ──────────────────────────────────────────────
|
|
274
|
+
|
|
275
|
+
export interface ServerConfig {
|
|
276
|
+
command: string;
|
|
277
|
+
args?: string[];
|
|
278
|
+
fileTypes: string[];
|
|
279
|
+
rootMarkers: string[];
|
|
280
|
+
enabled?: boolean;
|
|
281
|
+
initializationOptions?: unknown;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export interface LspConfig {
|
|
285
|
+
servers: Record<string, ServerConfig>;
|
|
286
|
+
}
|
package/ui.ts
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
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
|
+
}
|