@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
@@ -0,0 +1,98 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+
4
+ export interface GrepMatch {
5
+ file: string;
6
+ line: number;
7
+ text: string;
8
+ }
9
+
10
+ const IGNORE_DIRS = new Set([
11
+ "node_modules",
12
+ ".git",
13
+ "dist",
14
+ "build",
15
+ ".next",
16
+ "coverage",
17
+ "tmp",
18
+ ".pnpm",
19
+ ]);
20
+
21
+ const SOURCE_EXTENSIONS = new Set([
22
+ ".ts",
23
+ ".tsx",
24
+ ".js",
25
+ ".jsx",
26
+ ".mjs",
27
+ ".cjs",
28
+ ".py",
29
+ ".rs",
30
+ ".go",
31
+ ".java",
32
+ ".kt",
33
+ ".swift",
34
+ ".rb",
35
+ ".c",
36
+ ".cpp",
37
+ ".h",
38
+ ".hpp",
39
+ ]);
40
+
41
+ /** Simple recursive text search in project source files. */
42
+ export function fallbackGrep(projectRoot: string, query: string): GrepMatch[] {
43
+ const results: GrepMatch[] = [];
44
+ walk(projectRoot, projectRoot, query, results);
45
+ return results;
46
+ }
47
+
48
+ function walk(dir: string, projectRoot: string, query: string, results: GrepMatch[]): void {
49
+ let entries: fs.Dirent[];
50
+ try {
51
+ entries = fs.readdirSync(dir, { withFileTypes: true });
52
+ } catch {
53
+ return;
54
+ }
55
+
56
+ for (const entry of entries) {
57
+ if (entry.isDirectory()) {
58
+ if (!IGNORE_DIRS.has(entry.name)) {
59
+ walk(path.join(dir, entry.name), projectRoot, query, results);
60
+ }
61
+ continue;
62
+ }
63
+
64
+ if (!entry.isFile() || !SOURCE_EXTENSIONS.has(path.extname(entry.name))) {
65
+ continue;
66
+ }
67
+
68
+ const filePath = path.join(dir, entry.name);
69
+ searchFile(filePath, projectRoot, query, results);
70
+ if (results.length >= 20) return;
71
+ }
72
+ }
73
+
74
+ function searchFile(
75
+ filePath: string,
76
+ projectRoot: string,
77
+ query: string,
78
+ results: GrepMatch[],
79
+ ): void {
80
+ let content: string;
81
+ try {
82
+ content = fs.readFileSync(filePath, "utf-8");
83
+ } catch {
84
+ return;
85
+ }
86
+
87
+ const lines = content.split("\n");
88
+ for (let i = 0; i < lines.length; i++) {
89
+ if (lines[i].includes(query)) {
90
+ results.push({
91
+ file: path.relative(projectRoot, filePath),
92
+ line: i + 1,
93
+ text: lines[i].trim(),
94
+ });
95
+ if (results.length >= 20) return;
96
+ }
97
+ }
98
+ }
@@ -0,0 +1,153 @@
1
+ // Shared session-scoped LSP service registry.
2
+ // Peer extensions can import `getSessionLspService` from the package root
3
+ // to reuse the active LSP runtime without starting duplicate servers.
4
+
5
+ import * as path from "node:path";
6
+ import type { LspManager } from "./manager/manager.ts";
7
+ import type {
8
+ Diagnostic,
9
+ DocumentSymbol,
10
+ Hover,
11
+ Location,
12
+ LocationLink,
13
+ Position,
14
+ ProjectServerInfo,
15
+ SymbolInformation,
16
+ WorkspaceSymbol,
17
+ } from "./types.ts";
18
+
19
+ export type SessionLspServiceState =
20
+ | { kind: "ready"; service: SessionLspService }
21
+ | { kind: "pending" }
22
+ | { kind: "disabled" }
23
+ | { kind: "unavailable"; reason: string };
24
+
25
+ /**
26
+ * Public wrapper around {@link LspManager} that exposes stable semantic operations.
27
+ * File path inputs may be absolute or session-cwd-relative; a leading `@` is stripped
28
+ * to match pi's built-in path-tool convention.
29
+ */
30
+ export class SessionLspService {
31
+ constructor(private readonly manager: LspManager) {}
32
+
33
+ // ── Semantic lookups ────────────────────────────────────────────────
34
+
35
+ async hover(filePath: string, position: Position): Promise<Hover | null> {
36
+ const resolvedPath = this.resolveFilePath(filePath);
37
+ const client = await this.manager.ensureFileOpen(resolvedPath);
38
+ if (!client) return null;
39
+ return client.hover(resolvedPath, position);
40
+ }
41
+
42
+ async definition(
43
+ filePath: string,
44
+ position: Position,
45
+ ): Promise<Location | Location[] | LocationLink[] | null> {
46
+ const resolvedPath = this.resolveFilePath(filePath);
47
+ const client = await this.manager.ensureFileOpen(resolvedPath);
48
+ if (!client) return null;
49
+ return client.definition(resolvedPath, position);
50
+ }
51
+
52
+ async references(filePath: string, position: Position): Promise<Location[] | null> {
53
+ const resolvedPath = this.resolveFilePath(filePath);
54
+ const client = await this.manager.ensureFileOpen(resolvedPath);
55
+ if (!client) return null;
56
+ return client.references(resolvedPath, position);
57
+ }
58
+
59
+ async implementation(
60
+ filePath: string,
61
+ position: Position,
62
+ ): Promise<Location | Location[] | LocationLink[] | null> {
63
+ const resolvedPath = this.resolveFilePath(filePath);
64
+ const client = await this.manager.ensureFileOpen(resolvedPath);
65
+ if (!client) return null;
66
+ return client.implementation(resolvedPath, position);
67
+ }
68
+
69
+ async documentSymbols(filePath: string): Promise<DocumentSymbol[] | SymbolInformation[] | null> {
70
+ const resolvedPath = this.resolveFilePath(filePath);
71
+ const client = await this.manager.ensureFileOpen(resolvedPath);
72
+ if (!client) return null;
73
+ return client.documentSymbols(resolvedPath);
74
+ }
75
+
76
+ async workspaceSymbol(query: string): Promise<SymbolInformation[] | WorkspaceSymbol[] | null> {
77
+ return this.manager.workspaceSymbol(query);
78
+ }
79
+
80
+ // ── Project / runtime awareness ─────────────────────────────────────
81
+
82
+ getProjectServers(): ProjectServerInfo[] {
83
+ return this.manager.getKnownProjectServers([]);
84
+ }
85
+
86
+ isSupportedSourceFile(filePath: string): boolean {
87
+ return this.manager.isSupportedSourceFile(this.resolveFilePath(filePath));
88
+ }
89
+
90
+ // ── Diagnostics ─────────────────────────────────────────────────────
91
+
92
+ getOutstandingDiagnostics(
93
+ maxSeverity: number = 1,
94
+ ): Array<{ file: string; diagnostics: Diagnostic[] }> {
95
+ return this.manager.getOutstandingDiagnostics(maxSeverity);
96
+ }
97
+
98
+ getOutstandingDiagnosticSummary(
99
+ maxSeverity: number = 1,
100
+ ): import("./manager/manager-types.ts").OutstandingDiagnosticSummaryEntry[] {
101
+ return this.manager.getOutstandingDiagnosticSummary(maxSeverity);
102
+ }
103
+
104
+ /** Access the underlying manager for advanced use cases (discouraged). */
105
+ getManager(): LspManager {
106
+ return this.manager;
107
+ }
108
+
109
+ private resolveFilePath(filePath: string): string {
110
+ const normalizedPath = filePath.startsWith("@") ? filePath.slice(1) : filePath;
111
+ return path.resolve(this.manager.getCwd(), normalizedPath);
112
+ }
113
+ }
114
+
115
+ // ── Registry ──────────────────────────────────────────────────────────
116
+
117
+ const REGISTRY_KEY = Symbol.for("@mrclrchtr/supi-lsp/session-registry");
118
+
119
+ function getRegistry(): Map<string, SessionLspServiceState> {
120
+ const globalScope = globalThis as typeof globalThis & Record<symbol, unknown>;
121
+ const existing = globalScope[REGISTRY_KEY];
122
+ if (existing instanceof Map) return existing as Map<string, SessionLspServiceState>;
123
+
124
+ const registry = new Map<string, SessionLspServiceState>();
125
+ globalScope[REGISTRY_KEY] = registry;
126
+ return registry;
127
+ }
128
+
129
+ function normalizeCwd(cwd: string): string {
130
+ return path.resolve(cwd);
131
+ }
132
+
133
+ const registry = getRegistry();
134
+
135
+ /** Publish the LSP service state for a session cwd. */
136
+ export function setSessionLspServiceState(cwd: string, state: SessionLspServiceState): void {
137
+ registry.set(normalizeCwd(cwd), state);
138
+ }
139
+
140
+ /** Acquire the LSP service state for a session cwd. */
141
+ export function getSessionLspService(cwd: string): SessionLspServiceState {
142
+ return (
143
+ registry.get(normalizeCwd(cwd)) ?? {
144
+ kind: "unavailable",
145
+ reason: "No LSP session initialized for this workspace",
146
+ }
147
+ );
148
+ }
149
+
150
+ /** Remove the LSP service state for a session cwd. */
151
+ export function clearSessionLspService(cwd: string): void {
152
+ registry.delete(normalizeCwd(cwd));
153
+ }
@@ -0,0 +1,292 @@
1
+ // LSP settings registration for the supi settings registry.
2
+
3
+ import { getSettingsListTheme } from "@earendil-works/pi-coding-agent";
4
+ import type { SettingItem } from "@earendil-works/pi-tui";
5
+ import { Container, Key, matchesKey, SettingsList, Text } from "@earendil-works/pi-tui";
6
+ import {
7
+ loadSupiConfig,
8
+ loadSupiConfigForScope,
9
+ registerConfigSettings,
10
+ } from "@mrclrchtr/supi-core";
11
+ import { loadConfig } from "./config.ts";
12
+
13
+ // ── Types ────────────────────────────────────────────────────
14
+
15
+ export interface LspSettings {
16
+ enabled: boolean;
17
+ severity: number;
18
+ active: string[];
19
+ exclude: string[];
20
+ }
21
+
22
+ const LSP_DEFAULTS: LspSettings = {
23
+ enabled: true,
24
+ severity: 1,
25
+ active: [],
26
+ exclude: [],
27
+ };
28
+
29
+ // ── Config helpers ───────────────────────────────────────────
30
+
31
+ export function loadLspSettings(cwd: string, homeDir?: string): LspSettings {
32
+ return loadSupiConfig("lsp", cwd, LSP_DEFAULTS, { homeDir });
33
+ }
34
+
35
+ /**
36
+ * Return a user-facing message that indicates which config scope disabled LSP.
37
+ */
38
+ export function getLspDisabledMessage(cwd: string, homeDir?: string): string {
39
+ const global = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "global", homeDir });
40
+ const project = loadSupiConfigForScope("lsp", cwd, LSP_DEFAULTS, { scope: "project", homeDir });
41
+
42
+ if (project.enabled === false) {
43
+ return "LSP is disabled in project settings (.pi/supi/config.json)";
44
+ }
45
+ if (global.enabled === false) {
46
+ return "LSP is disabled in global settings (~/.pi/agent/supi/config.json)";
47
+ }
48
+ return "LSP is disabled in settings";
49
+ }
50
+
51
+ function severityLabel(severity: number): string {
52
+ switch (severity) {
53
+ case 1:
54
+ return "errors";
55
+ case 2:
56
+ return "warnings";
57
+ case 3:
58
+ return "info";
59
+ case 4:
60
+ return "hints";
61
+ default:
62
+ return "errors";
63
+ }
64
+ }
65
+
66
+ // ── Settings registration ────────────────────────────────────
67
+
68
+ export function registerLspSettings(): void {
69
+ registerConfigSettings({
70
+ id: "lsp",
71
+ label: "LSP",
72
+ section: "lsp",
73
+ defaults: LSP_DEFAULTS,
74
+ buildItems: (settings, scope, cwd) => buildLspSettingItems(settings, scope, cwd),
75
+ // biome-ignore lint/complexity/useMaxParams: ConfigSettingsOptions interface callback
76
+ persistChange: (_scope, _cwd, settingId, value, helpers) => {
77
+ handlePersistChange(settingId, value, helpers);
78
+ },
79
+ });
80
+ }
81
+
82
+ function handlePersistChange(
83
+ settingId: string,
84
+ value: string,
85
+ helpers: { set: (key: string, value: unknown) => void; unset: (key: string) => void },
86
+ ): void {
87
+ switch (settingId) {
88
+ case "enabled":
89
+ helpers.set("enabled", value === "on");
90
+ break;
91
+ case "severity": {
92
+ const num = Number.parseInt(value.split(" ")[0] ?? "1", 10);
93
+ helpers.set("severity", Number.isNaN(num) ? 1 : num);
94
+ break;
95
+ }
96
+ case "active": {
97
+ const active = value
98
+ .split(",")
99
+ .map((s) => s.trim())
100
+ .filter((s) => s.length > 0);
101
+ if (active.length > 0) {
102
+ helpers.set("active", active);
103
+ } else {
104
+ helpers.unset("active");
105
+ }
106
+ break;
107
+ }
108
+ case "exclude": {
109
+ const patterns = value
110
+ .split(",")
111
+ .map((s) => s.trim())
112
+ .filter((s) => s.length > 0);
113
+ if (patterns.length > 0) {
114
+ helpers.set("exclude", patterns);
115
+ } else {
116
+ helpers.unset("exclude");
117
+ }
118
+ break;
119
+ }
120
+ }
121
+ }
122
+
123
+ function buildLspSettingItems(
124
+ settings: LspSettings,
125
+ scope: "project" | "global",
126
+ cwd: string,
127
+ ): SettingItem[] {
128
+ return [
129
+ {
130
+ id: "enabled",
131
+ label: "Enable LSP",
132
+ description: "Enable or disable all LSP functionality",
133
+ currentValue: settings.enabled ? "on" : "off",
134
+ values: ["on", "off"],
135
+ },
136
+ {
137
+ id: "severity",
138
+ label: "Inline Severity",
139
+ description: "Minimum diagnostic severity to show inline (1=errors, 4=hints)",
140
+ currentValue: `${settings.severity} (${severityLabel(settings.severity)})`,
141
+ values: ["1 (errors)", "2 (warnings)", "3 (info)", "4 (hints)"],
142
+ },
143
+ {
144
+ id: "active",
145
+ label: "Active Servers",
146
+ description: "Press Enter to configure which language servers are active",
147
+ currentValue: settings.active.length > 0 ? settings.active.join(", ") : "all",
148
+ submenu: (_currentValue, done) => createServerSubmenu(scope, cwd, settings, done),
149
+ },
150
+ {
151
+ id: "exclude",
152
+ label: "Exclude Patterns",
153
+ description:
154
+ "Gitignore patterns to suppress LSP diagnostics. Edit .pi/supi/config.json → lsp.exclude (or ~/.pi/agent/supi/config.json for global). Patterns like __tests__/ exclude a directory, *.test.ts wildcards match at any depth, /dist anchors to root.",
155
+ currentValue: settings.exclude.length > 0 ? settings.exclude.join(", ") : "none",
156
+ submenu: (_currentValue, done) => createExcludeSubmenu(scope, cwd, settings, done),
157
+ },
158
+ ];
159
+ }
160
+
161
+ // ── Server submenu ───────────────────────────────────────────
162
+
163
+ function createServerSubmenu(
164
+ _scope: "project" | "global",
165
+ cwd: string,
166
+ settings: LspSettings,
167
+ done: (selectedValue?: string) => void,
168
+ ): {
169
+ render: (width: number) => string[];
170
+ invalidate: () => void;
171
+ handleInput: (data: string) => boolean;
172
+ } {
173
+ const config = loadConfig(cwd);
174
+ const allServers = Object.keys(config.servers);
175
+ const allEnabled = settings.active.length === 0;
176
+ const enabledServers = new Set(settings.active);
177
+
178
+ const items: SettingItem[] = allServers.map((name) => ({
179
+ id: name,
180
+ label: name,
181
+ currentValue: allEnabled || enabledServers.has(name) ? "enabled" : "disabled",
182
+ values: ["enabled", "disabled"],
183
+ }));
184
+
185
+ let dirty = false;
186
+
187
+ const container = new Container();
188
+ const header = new Text("Active Servers — all enabled by default", 0, 0);
189
+ container.addChild(header);
190
+
191
+ const settingsList = new SettingsList(
192
+ items,
193
+ Math.min(items.length + 2, 15),
194
+ getSettingsListTheme(),
195
+ (id, newValue) => {
196
+ const idx = items.findIndex((i) => i.id === id);
197
+ if (idx >= 0 && items[idx].currentValue !== newValue) {
198
+ dirty = true;
199
+ items[idx].currentValue = newValue;
200
+ }
201
+ },
202
+ () => {
203
+ // Escape on inner SettingsList — no-op, handled by submenu wrapper
204
+ },
205
+ { enableSearch: true },
206
+ );
207
+
208
+ container.addChild(settingsList);
209
+
210
+ return {
211
+ render: (width: number) => container.render(width),
212
+ invalidate: () => container.invalidate(),
213
+ handleInput: (data: string) => {
214
+ if (matchesKey(data, Key.escape)) {
215
+ if (!dirty) {
216
+ done();
217
+ return true;
218
+ }
219
+ const enabled = items.filter((i) => i.currentValue === "enabled").map((i) => i.id);
220
+ done(enabled.join(", ") || undefined);
221
+ return true;
222
+ }
223
+ settingsList.handleInput?.(data);
224
+ return true;
225
+ },
226
+ };
227
+ }
228
+
229
+ // ── Exclude patterns submenu ─────────────────────────────────
230
+
231
+ function createExcludeSubmenu(
232
+ _scope: "project" | "global",
233
+ _cwd: string,
234
+ settings: LspSettings,
235
+ done: (selectedValue?: string) => void,
236
+ ): {
237
+ render: (width: number) => string[];
238
+ invalidate: () => void;
239
+ handleInput: (data: string) => boolean;
240
+ } {
241
+ const items: SettingItem[] = settings.exclude.map((pattern) => ({
242
+ id: pattern,
243
+ label: pattern,
244
+ currentValue: "enabled",
245
+ values: ["enabled", "disabled"],
246
+ }));
247
+
248
+ let dirty = false;
249
+
250
+ const container = new Container();
251
+ const header = new Text("Exclude Patterns — toggle off to remove", 0, 0);
252
+ container.addChild(header);
253
+
254
+ const footer = new Text("Add new patterns in .pi/supi/config.json under lsp.exclude", 0, 0);
255
+ container.addChild(footer);
256
+
257
+ const settingsList = new SettingsList(
258
+ items,
259
+ Math.min(items.length + 3, 15),
260
+ getSettingsListTheme(),
261
+ (id, newValue) => {
262
+ const idx = items.findIndex((i) => i.id === id);
263
+ if (idx >= 0 && items[idx].currentValue !== newValue) {
264
+ dirty = true;
265
+ items[idx].currentValue = newValue;
266
+ }
267
+ },
268
+ () => {
269
+ // Escape on inner SettingsList — no-op, handled by submenu wrapper
270
+ },
271
+ );
272
+
273
+ container.addChild(settingsList);
274
+
275
+ return {
276
+ render: (width: number) => container.render(width),
277
+ invalidate: () => container.invalidate(),
278
+ handleInput: (data: string) => {
279
+ if (matchesKey(data, Key.escape)) {
280
+ if (!dirty) {
281
+ done();
282
+ return true;
283
+ }
284
+ const enabled = items.filter((i) => i.currentValue === "enabled").map((i) => i.id);
285
+ done(enabled.join(", ") || undefined);
286
+ return true;
287
+ }
288
+ settingsList.handleInput?.(data);
289
+ return true;
290
+ },
291
+ };
292
+ }
@@ -1,5 +1,8 @@
1
1
  import * as path from "node:path";
2
- import type { ActiveCoverageSummaryEntry, OutstandingDiagnosticSummaryEntry } from "./manager.ts";
2
+ import type {
3
+ ActiveCoverageSummaryEntry,
4
+ OutstandingDiagnosticSummaryEntry,
5
+ } from "./manager/manager-types.ts";
3
6
 
4
7
  /**
5
8
  * Display form for a file path used both for human-readable LSP output and as
@@ -9,9 +12,9 @@ import type { ActiveCoverageSummaryEntry, OutstandingDiagnosticSummaryEntry } fr
9
12
  * unrelated files with the same name appear interchangeable in relevance
10
13
  * matching, and it broke diagnostic correlation for tracked external paths.
11
14
  */
12
- export function displayRelativeFilePath(filePath: string): string {
13
- const absolutePath = path.resolve(filePath);
14
- const relativePath = path.relative(process.cwd(), absolutePath);
15
+ export function displayRelativeFilePath(filePath: string, cwd: string): string {
16
+ const absolutePath = path.resolve(cwd, filePath);
17
+ const relativePath = path.relative(cwd, absolutePath);
15
18
  if (relativePath === "") return path.basename(absolutePath);
16
19
  if (relativePath.startsWith(`..${path.sep}`) || relativePath === "..") {
17
20
  return absolutePath;
@@ -64,9 +67,9 @@ export function normalizeRelevantPaths(relevantPaths: string[]): string[] {
64
67
  * - candidate has no "/" and no ".": treat as a directory name anywhere in the path
65
68
  * - otherwise: treat as a filename and match the basename
66
69
  */
67
- export function isPathRelevant(filePath: string, relevantPaths: string[]): boolean {
70
+ export function isPathRelevant(filePath: string, relevantPaths: string[], cwd: string): boolean {
68
71
  const normalizedFilePath = normalizeRelevantPath(filePath);
69
- if (shouldIgnoreLspPath(normalizedFilePath)) return false;
72
+ if (shouldIgnoreLspPath(normalizedFilePath, cwd)) return false;
70
73
 
71
74
  return relevantPaths.some((candidate) => {
72
75
  if (normalizedFilePath === candidate) return true;
@@ -81,16 +84,48 @@ export function isPathRelevant(filePath: string, relevantPaths: string[]): boole
81
84
  });
82
85
  }
83
86
 
84
- export function shouldIgnoreLspPath(filePath: string): boolean {
87
+ import { isFileExcludedByTsconfig } from "./tsconfig-scope.ts";
88
+
89
+ /** Check whether a file path is inside the project tree (within cwd, not node_modules/.pnpm/out-of-tree).
90
+ * Does NOT check tsconfig exclusion — use `shouldIgnoreLspPath` for diagnostics/guidance filtering. */
91
+ export function isInProjectTree(filePath: string, cwd: string): boolean {
85
92
  const normalized = normalizeRelevantPath(filePath);
86
- return (
93
+ if (
87
94
  normalized === "node_modules" ||
88
95
  normalized.startsWith("node_modules/") ||
89
96
  normalized.includes("/node_modules/") ||
90
97
  normalized === ".pnpm" ||
91
98
  normalized.startsWith(".pnpm/") ||
92
99
  normalized.includes("/.pnpm/")
93
- );
100
+ ) {
101
+ return false;
102
+ }
103
+
104
+ const absolutePath = path.resolve(cwd, filePath);
105
+ const relativePath = path.relative(cwd, absolutePath);
106
+ return !(relativePath.startsWith(`..${path.sep}`) || relativePath === "..");
107
+ }
108
+
109
+ /** Check whether a file path is inside the current project (not ignored and within cwd). */
110
+ export function isProjectSource(filePath: string, cwd: string): boolean {
111
+ return isInProjectTree(filePath, cwd);
112
+ }
113
+
114
+ export function shouldIgnoreLspPath(filePath: string, cwd: string): boolean {
115
+ const normalized = normalizeRelevantPath(filePath);
116
+ if (
117
+ normalized === "node_modules" ||
118
+ normalized.startsWith("node_modules/") ||
119
+ normalized.includes("/node_modules/") ||
120
+ normalized === ".pnpm" ||
121
+ normalized.startsWith(".pnpm/") ||
122
+ normalized.includes("/.pnpm/") ||
123
+ isFileExcludedByTsconfig(normalized, cwd)
124
+ ) {
125
+ return true;
126
+ }
127
+
128
+ return false;
94
129
  }
95
130
 
96
131
  function normalizeRelevantPath(filePath: string): string {