@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.
- package/README.md +112 -0
- package/node_modules/@mrclrchtr/supi-core/README.md +90 -0
- package/node_modules/@mrclrchtr/supi-core/package.json +26 -0
- package/node_modules/@mrclrchtr/supi-core/src/config-settings.ts +76 -0
- package/node_modules/@mrclrchtr/supi-core/src/config.ts +186 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-messages.ts +119 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-provider-registry.ts +36 -0
- package/node_modules/@mrclrchtr/supi-core/src/context-tag.ts +31 -0
- package/node_modules/@mrclrchtr/supi-core/src/debug-registry.ts +255 -0
- package/node_modules/@mrclrchtr/supi-core/src/index.ts +83 -0
- package/node_modules/@mrclrchtr/supi-core/src/project-roots.ts +170 -0
- package/node_modules/@mrclrchtr/supi-core/src/registry-utils.ts +54 -0
- package/node_modules/@mrclrchtr/supi-core/src/session-utils.ts +29 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-command.ts +15 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-registry.ts +41 -0
- package/node_modules/@mrclrchtr/supi-core/src/settings-ui.ts +226 -0
- package/node_modules/@mrclrchtr/supi-core/src/terminal.ts +60 -0
- package/package.json +16 -11
- package/{capabilities.ts → src/capabilities.ts} +8 -0
- package/src/client/client-refresh.ts +229 -0
- package/{client.ts → src/client/client.ts} +178 -30
- package/{transport.ts → src/client/transport.ts} +10 -6
- package/src/config.ts +143 -0
- package/src/defaults.json +82 -0
- package/src/diagnostics/diagnostic-augmentation.ts +82 -0
- package/src/diagnostics/diagnostic-display.ts +68 -0
- package/{diagnostic-summary.ts → src/diagnostics/diagnostic-summary.ts} +11 -7
- package/{diagnostics.ts → src/diagnostics/diagnostics.ts} +9 -4
- package/src/diagnostics/stale-diagnostics.ts +47 -0
- package/src/diagnostics/suppression-diagnostics.ts +58 -0
- package/src/format.ts +359 -0
- package/src/guidance.ts +163 -0
- package/src/index.ts +17 -0
- package/src/lsp-state.ts +82 -0
- package/src/lsp.ts +481 -0
- package/src/manager/manager-client-state.ts +34 -0
- package/src/manager/manager-diagnostics.ts +139 -0
- package/src/manager/manager-helpers.ts +39 -0
- package/src/manager/manager-project-info.ts +46 -0
- package/src/manager/manager-stale-resync.ts +47 -0
- package/src/manager/manager-types.ts +39 -0
- package/src/manager/manager-workspace-recovery.ts +83 -0
- package/src/manager/manager-workspace-symbol.ts +18 -0
- package/src/manager/manager.ts +550 -0
- package/src/overrides.ts +173 -0
- package/src/pattern-matcher.ts +197 -0
- package/src/renderer.ts +120 -0
- package/src/scanner.ts +153 -0
- package/src/search-fallback.ts +98 -0
- package/src/service-registry.ts +153 -0
- package/src/settings-registration.ts +292 -0
- package/{summary.ts → src/summary.ts} +44 -9
- package/src/tool-actions.ts +430 -0
- package/src/tree-persist.ts +48 -0
- package/src/tsconfig-scope.ts +156 -0
- package/{types.ts → src/types.ts} +123 -0
- package/src/ui.ts +358 -0
- package/{utils.ts → src/utils.ts} +8 -25
- package/src/workspace-sentinels.ts +114 -0
- package/bash-guard.ts +0 -58
- package/config.ts +0 -99
- package/defaults.json +0 -40
- package/format.ts +0 -190
- package/guidance.ts +0 -140
- package/lsp.ts +0 -375
- package/manager.ts +0 -396
- package/overrides.ts +0 -95
- package/recent-paths.ts +0 -126
- package/runtime-state.ts +0 -113
- package/tool-actions.ts +0 -211
- package/tsconfig.json +0 -5
- 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
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
|
-
}
|