@danypops/pi-lector 0.9.5 → 0.10.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/extension/src/{apply-patch-operations.ts → apply-patch/operations.ts} +3 -3
- package/extension/src/{apply-patch-rendering.ts → apply-patch/rendering.ts} +1 -1
- package/extension/src/{code-intelligence-operations.ts → code-intelligence/operations.ts} +2 -2
- package/extension/src/{code-intelligence-rendering.ts → code-intelligence/rendering.ts} +1 -1
- package/extension/src/cross-workspace-search/operations.ts +88 -0
- package/extension/src/{cross-workspace-search-rendering.ts → cross-workspace-search/rendering.ts} +37 -13
- package/extension/src/{edit-operations.ts → edit/operations.ts} +2 -2
- package/extension/src/editor/editor-state.ts +317 -0
- package/extension/src/editor/neovim-editor-component.ts +199 -0
- package/extension/src/editor/operations.ts +36 -0
- package/extension/src/{external-search-operations.ts → external-search/operations.ts} +1 -1
- package/extension/src/{external-search-rendering.ts → external-search/rendering.ts} +1 -1
- package/extension/src/{find-files-operations.ts → find-files/operations.ts} +1 -1
- package/extension/src/{find-files-rendering.ts → find-files/rendering.ts} +1 -1
- package/extension/src/{find-symbols-operations.ts → find-symbols/operations.ts} +1 -1
- package/extension/src/{find-symbols-rendering.ts → find-symbols/rendering.ts} +1 -1
- package/extension/src/{git-operations.ts → git/operations.ts} +1 -1
- package/extension/src/{git-rendering.ts → git/rendering.ts} +1 -1
- package/extension/src/index.ts +76 -39
- package/extension/src/lector-client.ts +25 -0
- package/extension/src/{line-edit-operations.ts → line-edit/operations.ts} +4 -4
- package/extension/src/{line-edit-rendering.ts → line-edit/rendering.ts} +1 -1
- package/extension/src/{mutation-history-operations.ts → mutation-history/operations.ts} +2 -2
- package/extension/src/{package-source-operations.ts → package-source/operations.ts} +1 -1
- package/extension/src/{package-source-rendering.ts → package-source/rendering.ts} +35 -28
- package/extension/src/{read-operations.ts → read/operations.ts} +2 -2
- package/extension/src/{reference-based-rename-operations.ts → reference-based-rename/operations.ts} +1 -1
- package/extension/src/{rename-operations.ts → rename/operations.ts} +1 -1
- package/extension/src/{repo-cache-evict-operations.ts → repo-cache/evict-operations.ts} +2 -2
- package/extension/src/{repo-cache-list-operations.ts → repo-cache/list-operations.ts} +2 -2
- package/extension/src/{repo-cache-rendering.ts → repo-cache/rendering.ts} +1 -1
- package/extension/src/{repo-fetch-operations.ts → repo-fetch/operations.ts} +1 -1
- package/extension/src/{search-operations.ts → search/operations.ts} +1 -1
- package/extension/src/{search-rendering.ts → search/rendering.ts} +1 -1
- package/extension/src/{symbol-annotation-operations.ts → symbol-annotation/operations.ts} +1 -1
- package/extension/src/{symbol-annotation-rendering.ts → symbol-annotation/rendering.ts} +1 -1
- package/extension/src/{workspace-cache-operations.ts → workspace-cache/operations.ts} +1 -1
- package/extension/src/{workspace-cache-rendering.ts → workspace-cache/rendering.ts} +1 -1
- package/extension/src/{write-operations.ts → write/operations.ts} +2 -2
- package/package.json +2 -2
- package/extension/src/cross-workspace-search-operations.ts +0 -43
- /package/extension/src/{code-intelligence-hints.ts → code-intelligence/hints.ts} +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
import type { HighlightSpan } from "@danypops/lector";
|
|
3
|
+
import { highlightSpans } from "@danypops/lector";
|
|
4
|
+
import type { ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
5
|
+
import type { Component, TUI } from "@earendil-works/pi-tui";
|
|
6
|
+
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
7
|
+
import type { EditorAction } from "./editor-state.ts";
|
|
8
|
+
import { EditorState } from "./editor-state.ts";
|
|
9
|
+
|
|
10
|
+
/** Real theme, narrowed to exactly what this component needs -- avoids depending on pi-coding-agent's full internal Theme shape. */
|
|
11
|
+
export interface EditorTheme {
|
|
12
|
+
fg(color: ThemeColor, text: string): string;
|
|
13
|
+
bg(color: "selectedBg", text: string): string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface NeovimEditorHost {
|
|
17
|
+
filePath: string;
|
|
18
|
+
/** Saves the buffer's current text through Lector's hash-guarded write. Throws (surfaced as a status message, not a crash) on a genuinely concurrent external change. */
|
|
19
|
+
save(text: string): Promise<void>;
|
|
20
|
+
/** Real hover info from Lector's existing code-intelligence operation, or undefined when there is none at this position. */
|
|
21
|
+
hover(line: number, character: number): Promise<{ contents: string } | undefined>;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const CAPTURE_COLOR: Record<string, ThemeColor> = {
|
|
25
|
+
keyword: "syntaxKeyword",
|
|
26
|
+
comment: "syntaxComment",
|
|
27
|
+
string: "syntaxString",
|
|
28
|
+
number: "syntaxNumber",
|
|
29
|
+
function: "syntaxFunction",
|
|
30
|
+
type: "syntaxType",
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A real, full-file, neovim-style modal code editor Component -- not a CustomEditor subclass
|
|
35
|
+
* (that API replaces Pi's own chat input, not a full-file view; confirmed against
|
|
36
|
+
* docs/tui.md's Pattern 7 and examples/extensions/modal-editor.ts). Renders as a `ctx.ui.custom`
|
|
37
|
+
* overlay. Owns no authoritative state of its own past the open edit session: `EditorState`'s
|
|
38
|
+
* LiveBuffer is the only in-memory copy, and every save round-trips through the host's
|
|
39
|
+
* hash-guarded write -- never a second source of truth for the file's real disk content.
|
|
40
|
+
*/
|
|
41
|
+
export class NeovimEditorComponent implements Component {
|
|
42
|
+
private readonly state: EditorState;
|
|
43
|
+
private readonly host: NeovimEditorHost;
|
|
44
|
+
private readonly tui: TUI;
|
|
45
|
+
private readonly theme: EditorTheme;
|
|
46
|
+
private readonly done: () => void;
|
|
47
|
+
private readonly extension: string;
|
|
48
|
+
|
|
49
|
+
private scrollTop = 1;
|
|
50
|
+
private statusMessage = "";
|
|
51
|
+
private highlightCache: { text: string; spans: readonly HighlightSpan[] } | undefined;
|
|
52
|
+
|
|
53
|
+
constructor(tui: TUI, theme: EditorTheme, host: NeovimEditorHost, content: string, done: () => void) {
|
|
54
|
+
this.tui = tui;
|
|
55
|
+
this.theme = theme;
|
|
56
|
+
this.host = host;
|
|
57
|
+
this.done = done;
|
|
58
|
+
this.extension = extname(host.filePath);
|
|
59
|
+
this.state = new EditorState(content);
|
|
60
|
+
void this.refreshHighlights();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
invalidate(): void {
|
|
64
|
+
this.highlightCache = undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
handleInput(data: string): void {
|
|
68
|
+
this.state.handleKey(data);
|
|
69
|
+
this.scrollToKeepCursorVisible();
|
|
70
|
+
const action = this.state.pendingAction;
|
|
71
|
+
if (action) void this.performAction(action);
|
|
72
|
+
this.tui.requestRender();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
private async performAction(action: EditorAction): Promise<void> {
|
|
76
|
+
switch (action.kind) {
|
|
77
|
+
case "save":
|
|
78
|
+
case "save-and-quit": {
|
|
79
|
+
try {
|
|
80
|
+
await this.host.save(this.state.buffer.text);
|
|
81
|
+
this.state.dirty = false;
|
|
82
|
+
this.statusMessage = `"${this.host.filePath}" written`;
|
|
83
|
+
} catch (error) {
|
|
84
|
+
this.statusMessage = `save failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
85
|
+
this.tui.requestRender();
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (action.kind === "save-and-quit") {
|
|
89
|
+
this.done();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
case "quit":
|
|
95
|
+
this.done();
|
|
96
|
+
return;
|
|
97
|
+
case "hover": {
|
|
98
|
+
const hover = await this.host.hover(this.state.cursorLine, this.state.cursorCharacter);
|
|
99
|
+
const firstLine = hover?.contents.split("\n")[0];
|
|
100
|
+
this.statusMessage = firstLine ?? "no hover info at this position";
|
|
101
|
+
break;
|
|
102
|
+
}
|
|
103
|
+
default: {
|
|
104
|
+
const exhaustive: never = action;
|
|
105
|
+
throw new Error(`Unhandled editor action: ${JSON.stringify(exhaustive)}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
this.tui.requestRender();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private async refreshHighlights(): Promise<void> {
|
|
112
|
+
const text = this.state.buffer.text;
|
|
113
|
+
const spans = await highlightSpans(text, this.extension);
|
|
114
|
+
this.highlightCache = { text, spans };
|
|
115
|
+
this.tui.requestRender();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
private scrollToKeepCursorVisible(): void {
|
|
119
|
+
const viewportHeight = Math.max(1, this.tui.terminal.rows - 2);
|
|
120
|
+
if (this.state.cursorLine < this.scrollTop) this.scrollTop = this.state.cursorLine;
|
|
121
|
+
else if (this.state.cursorLine >= this.scrollTop + viewportHeight) this.scrollTop = this.state.cursorLine - viewportHeight + 1;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
render(width: number): string[] {
|
|
125
|
+
if (this.highlightCache?.text !== this.state.buffer.text) void this.refreshHighlights();
|
|
126
|
+
|
|
127
|
+
const viewportHeight = Math.max(1, this.tui.terminal.rows - 2);
|
|
128
|
+
const gutterWidth = Math.max(3, String(this.state.buffer.lineCount).length) + 1;
|
|
129
|
+
const lastLine = Math.min(this.state.buffer.lineCount, this.scrollTop + viewportHeight - 1);
|
|
130
|
+
|
|
131
|
+
const lines: string[] = [];
|
|
132
|
+
for (let line = this.scrollTop; line <= lastLine; line++) {
|
|
133
|
+
lines.push(this.renderLine(line, gutterWidth, width - gutterWidth));
|
|
134
|
+
}
|
|
135
|
+
while (lines.length < viewportHeight) lines.push("");
|
|
136
|
+
|
|
137
|
+
lines.push(this.renderStatusLine(width));
|
|
138
|
+
return lines;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
private renderLine(line: number, gutterWidth: number, contentWidth: number): string {
|
|
142
|
+
const isCursorLine = line === this.state.cursorLine;
|
|
143
|
+
const gutterNumber = isCursorLine ? String(line) : String(Math.abs(line - this.state.cursorLine));
|
|
144
|
+
const gutter = `${this.theme.fg(isCursorLine ? "text" : "muted", gutterNumber.padStart(gutterWidth - 1))} `;
|
|
145
|
+
|
|
146
|
+
const lineText = this.state.buffer.lineText(line);
|
|
147
|
+
const rendered = isCursorLine ? this.renderLineWithCursor(line, lineText) : this.renderHighlightedLine(line, lineText);
|
|
148
|
+
return gutter + truncateToWidth(rendered, contentWidth, "");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private spansForLine(line: number): readonly HighlightSpan[] {
|
|
152
|
+
if (!this.highlightCache) return [];
|
|
153
|
+
const from = this.state.buffer.offsetAt({ line, character: 1 });
|
|
154
|
+
const to = from + this.state.buffer.lineText(line).length;
|
|
155
|
+
return this.highlightCache.spans.filter((span) => span.startIndex < to && span.endIndex > from);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
private renderHighlightedLine(line: number, lineText: string): string {
|
|
159
|
+
const from = this.state.buffer.offsetAt({ line, character: 1 });
|
|
160
|
+
const spans = this.spansForLine(line);
|
|
161
|
+
let result = "";
|
|
162
|
+
let cursor = 0;
|
|
163
|
+
for (const span of spans) {
|
|
164
|
+
const start = Math.max(span.startIndex - from, 0);
|
|
165
|
+
const end = Math.min(span.endIndex - from, lineText.length);
|
|
166
|
+
if (start < cursor) continue; // overlapping captures -- keep the earliest, simplest for v1
|
|
167
|
+
result += lineText.slice(cursor, start);
|
|
168
|
+
const color = CAPTURE_COLOR[span.capture];
|
|
169
|
+
result += color ? this.theme.fg(color, lineText.slice(start, end)) : lineText.slice(start, end);
|
|
170
|
+
cursor = end;
|
|
171
|
+
}
|
|
172
|
+
result += lineText.slice(cursor);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** The cursor's own line: highlighted text plus an inverse-video cursor cell, matching pi-tui's own Editor cursor convention. */
|
|
177
|
+
private renderLineWithCursor(line: number, lineText: string): string {
|
|
178
|
+
const highlighted = this.renderHighlightedLine(line, lineText);
|
|
179
|
+
// Re-slicing a pre-highlighted (ANSI-embedded) string by character index would misplace the
|
|
180
|
+
// cursor inside escape codes -- render the cursor against the plain text, forgoing this
|
|
181
|
+
// line's syntax highlighting. Acceptable for v1: only one line is ever affected at a time.
|
|
182
|
+
void highlighted;
|
|
183
|
+
const col = this.state.cursorCharacter - 1;
|
|
184
|
+
const before = lineText.slice(0, col);
|
|
185
|
+
const atCursor = col < lineText.length ? lineText[col] : " ";
|
|
186
|
+
const after = col < lineText.length ? lineText.slice(col + 1) : "";
|
|
187
|
+
return `${before}\x1b[7m${atCursor}\x1b[0m${after}`;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private renderStatusLine(width: number): string {
|
|
191
|
+
const modeLabel = { normal: " NORMAL ", insert: " INSERT ", command: " COMMAND " }[this.state.mode];
|
|
192
|
+
const dirtyMarker = this.state.dirty ? " [+]" : "";
|
|
193
|
+
const position = `${this.state.cursorLine}:${this.state.cursorCharacter}`;
|
|
194
|
+
const left = this.state.mode === "command" ? `:${this.state.commandText}` : `${this.theme.fg("accent", modeLabel)} ${this.host.filePath}${dirtyMarker}`;
|
|
195
|
+
const right = this.statusMessage || position;
|
|
196
|
+
const gap = Math.max(1, width - visibleWidth(left) - visibleWidth(right));
|
|
197
|
+
return truncateToWidth(`${left}${" ".repeat(gap)}${right}`, width, "");
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ContentHash } from "@danypops/lector";
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForPath } from "../lector-client.ts";
|
|
3
|
+
import { toWorkspaceRelativePath } from "../workspace-relative-path.ts";
|
|
4
|
+
|
|
5
|
+
export interface EditorFileSession {
|
|
6
|
+
readonly content: string;
|
|
7
|
+
/**
|
|
8
|
+
* Saves through Lector's hash-guarded `workspace.exactEdit` -- deliberately NOT the
|
|
9
|
+
* transparent stale-hash-retry behavior `createLectorWriteOperations` uses for pi's
|
|
10
|
+
* unconditional-overwrite write tool. `/editor`'s `:w` is a human editing a file they can see
|
|
11
|
+
* on screen; a genuinely concurrent external change must surface as a real, visible error
|
|
12
|
+
* (StaleExpectedHash), never be silently overwritten just because the model's write-tool
|
|
13
|
+
* contract happens to prefer that elsewhere.
|
|
14
|
+
*/
|
|
15
|
+
save(text: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Opens `absolutePath` for `/editor`: one hash-guarded read, then a save() closure that tracks the hash across saves within the same session. */
|
|
19
|
+
export function openEditorFile(absolutePath: string): Promise<EditorFileSession> {
|
|
20
|
+
return withWorkspace(
|
|
21
|
+
() => workspaceForPath(absolutePath),
|
|
22
|
+
async ({ workspaceId, root }) => {
|
|
23
|
+
const client = await lectorClient();
|
|
24
|
+
const relativePath = toWorkspaceRelativePath(root, absolutePath);
|
|
25
|
+
const { content, hash } = await client.call("workspace.rawRead", { workspaceId, path: relativePath });
|
|
26
|
+
let expectedHash: ContentHash | null = hash;
|
|
27
|
+
return {
|
|
28
|
+
content,
|
|
29
|
+
async save(text: string): Promise<void> {
|
|
30
|
+
const outcome = await client.callOnce("workspace.exactEdit", { workspaceId, path: relativePath, expectedHash, content: text });
|
|
31
|
+
expectedHash = outcome.newHash;
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GithubRepoSearchResult, NpmPackageCandidate, SourcegraphCodeCandidate } from "@danypops/lector";
|
|
2
|
-
import { lectorClient } from "
|
|
2
|
+
import { lectorClient } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
/** Thin wrapper over search.githubRepos/search.npmPackages/search.sourcegraphCode -- explicit-query discovery inputs shaped for repo_cache/package_source, never open-ended discovery/trending. */
|
|
5
5
|
export interface ExternalSearchOperations {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GithubRepoSearchResult, NpmPackageCandidate, SourcegraphCodeCandidate } from "@danypops/lector";
|
|
2
|
-
import type { LectorTheme } from "
|
|
2
|
+
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
3
3
|
|
|
4
4
|
type ExternalSearchAction = "github_repos" | "npm_packages" | "sourcegraph_code";
|
|
5
5
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { FindFilesResult } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, withWorkspace, workspaceForDirectory } from "
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForDirectory } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Thin wrapper over workspace.findFiles -- the `find`-shaped half of the classic grep+find pair,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { FindFilesResult } from "@danypops/lector";
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { renderTruncatedList } from "malevich-tui-components";
|
|
4
|
-
import type { LectorTheme } from "
|
|
4
|
+
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
5
5
|
|
|
6
6
|
const DEFAULT_VISIBLE_PATHS = 40;
|
|
7
7
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OperationInputs, SymbolSearchResult } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, withWorkspace, workspaceForDirectory } from "
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForDirectory } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Thin wrapper over workspace.findSymbols. No seedFile parameter here or
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { SymbolSearchResult, WorkspaceSymbol } from "@danypops/lector";
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { renderTruncatedList } from "malevich-tui-components";
|
|
4
|
-
import { colorForKind, formatLocation, type LectorTheme } from "
|
|
4
|
+
import { colorForKind, formatLocation, type LectorTheme } from "../lector-tui-theme.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Custom TUI rendering for find_symbols -- the one Lector-backed tool with
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { GitDiffResult, GitLogEntry, GitStatusSummary, OperationOutputs } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, withWorkspace, workspaceForDirectory } from "
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForDirectory } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
type SymbolComparison = OperationOutputs["workspace.compareSymbolAcrossVersions"];
|
|
5
5
|
|
|
@@ -2,7 +2,7 @@ import type { GitDiffResult, GitLogEntry, GitStatusSummary, OperationOutputs } f
|
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
|
|
4
4
|
import { renderDiffLines, renderTruncatedList, type TextMeasure } from "malevich-tui-components";
|
|
5
|
-
import type { LectorTheme } from "
|
|
5
|
+
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
6
6
|
|
|
7
7
|
/** Real ANSI-aware measurement, not Malevich's own ASCII-only default -- every diff line renderDiffLines receives is already theme.fg-styled. */
|
|
8
8
|
const measure: TextMeasure = { visibleWidth, truncateToWidth };
|
package/extension/src/index.ts
CHANGED
|
@@ -27,7 +27,6 @@ import type {
|
|
|
27
27
|
WorkspaceCacheStatus,
|
|
28
28
|
WorkspaceLocation,
|
|
29
29
|
WorkspaceMapResult,
|
|
30
|
-
WorkspaceQueryOutcome,
|
|
31
30
|
} from "@danypops/lector";
|
|
32
31
|
import { DEFAULT_EXTERNAL_SEARCH_MAX_RESULTS, PACKAGE_ECOSYSTEMS } from "@danypops/lector";
|
|
33
32
|
import {
|
|
@@ -44,9 +43,9 @@ import { Type } from "typebox";
|
|
|
44
43
|
/** Real ANSI-aware measurement for Table -- Malevich's own default is ASCII-only, unsafe against theme-styled cell/header text. */
|
|
45
44
|
const tableMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
46
45
|
|
|
47
|
-
import { createLectorApplyPatchOperations } from "./apply-patch
|
|
48
|
-
import { formatApplyPatchCall, formatApplyPatchResult } from "./apply-patch
|
|
49
|
-
import { createLectorCodeIntelligenceOperations } from "./code-intelligence
|
|
46
|
+
import { createLectorApplyPatchOperations } from "./apply-patch/operations.ts";
|
|
47
|
+
import { formatApplyPatchCall, formatApplyPatchResult } from "./apply-patch/rendering.ts";
|
|
48
|
+
import { createLectorCodeIntelligenceOperations } from "./code-intelligence/operations.ts";
|
|
50
49
|
import {
|
|
51
50
|
type CallHierarchyToolDetails,
|
|
52
51
|
formatCallHierarchyCall,
|
|
@@ -67,29 +66,31 @@ import {
|
|
|
67
66
|
formatReachableFromResult,
|
|
68
67
|
formatWorkspaceMapCall,
|
|
69
68
|
formatWorkspaceMapResult,
|
|
70
|
-
} from "./code-intelligence
|
|
71
|
-
import { createLectorCrossWorkspaceSearchOperations } from "./cross-workspace-search
|
|
72
|
-
import { formatCrossWorkspaceCall, formatFindSymbolsAcrossProjectsResult, formatSearchTextAcrossProjectsResult } from "./cross-workspace-search
|
|
73
|
-
import { createLectorEditOperations } from "./edit
|
|
74
|
-
import {
|
|
69
|
+
} from "./code-intelligence/rendering.ts";
|
|
70
|
+
import { type CrossWorkspaceOutcome, createLectorCrossWorkspaceSearchOperations } from "./cross-workspace-search/operations.ts";
|
|
71
|
+
import { formatCrossWorkspaceCall, formatFindSymbolsAcrossProjectsResult, formatSearchTextAcrossProjectsResult } from "./cross-workspace-search/rendering.ts";
|
|
72
|
+
import { createLectorEditOperations } from "./edit/operations.ts";
|
|
73
|
+
import { NeovimEditorComponent, type NeovimEditorHost } from "./editor/neovim-editor-component.ts";
|
|
74
|
+
import { openEditorFile } from "./editor/operations.ts";
|
|
75
|
+
import { createExternalSearchOperations } from "./external-search/operations.ts";
|
|
75
76
|
import {
|
|
76
77
|
formatExternalSearchCall,
|
|
77
78
|
formatGithubRepoSearchResult,
|
|
78
79
|
formatNpmPackageSearchResult,
|
|
79
80
|
formatSourcegraphCodeSearchResult,
|
|
80
|
-
} from "./external-search
|
|
81
|
-
import { createLectorFindFilesOperations } from "./find-files
|
|
82
|
-
import { formatFindFilesCall, formatFindFilesResult } from "./find-files
|
|
83
|
-
import { createLectorFindSymbolsOperations } from "./find-symbols
|
|
84
|
-
import { describeFindSymbolSources, formatFindSymbolsCall, formatFindSymbolsResult } from "./find-symbols
|
|
85
|
-
import { createLectorGitOperations } from "./git
|
|
86
|
-
import { formatGitCall, formatGitResult, type GitToolDetails } from "./git
|
|
81
|
+
} from "./external-search/rendering.ts";
|
|
82
|
+
import { createLectorFindFilesOperations } from "./find-files/operations.ts";
|
|
83
|
+
import { formatFindFilesCall, formatFindFilesResult } from "./find-files/rendering.ts";
|
|
84
|
+
import { createLectorFindSymbolsOperations } from "./find-symbols/operations.ts";
|
|
85
|
+
import { describeFindSymbolSources, formatFindSymbolsCall, formatFindSymbolsResult } from "./find-symbols/rendering.ts";
|
|
86
|
+
import { createLectorGitOperations } from "./git/operations.ts";
|
|
87
|
+
import { formatGitCall, formatGitResult, type GitToolDetails } from "./git/rendering.ts";
|
|
87
88
|
import { setNewWorkspaceObserver } from "./lector-client.ts";
|
|
88
|
-
import { createLectorLineEditOperations } from "./line-edit
|
|
89
|
-
import { formatLineEditCall, formatLineEditResult } from "./line-edit
|
|
90
|
-
import { createMutationHistoryOperations } from "./mutation-history
|
|
89
|
+
import { createLectorLineEditOperations } from "./line-edit/operations.ts";
|
|
90
|
+
import { formatLineEditCall, formatLineEditResult } from "./line-edit/rendering.ts";
|
|
91
|
+
import { createMutationHistoryOperations } from "./mutation-history/operations.ts";
|
|
91
92
|
import { isFilesystemRoot, nearestGitRoot } from "./nearest-workspace-root.ts";
|
|
92
|
-
import { createLectorPackageSourceOperations, type PackageSourceListPage } from "./package-source
|
|
93
|
+
import { createLectorPackageSourceOperations, type PackageSourceListPage } from "./package-source/operations.ts";
|
|
93
94
|
import {
|
|
94
95
|
buildPackageSourceListTableRows,
|
|
95
96
|
formatPackageSourceCall,
|
|
@@ -100,12 +101,12 @@ import {
|
|
|
100
101
|
PACKAGE_SOURCE_LIST_TABLE_COLUMNS,
|
|
101
102
|
PACKAGE_SOURCE_LIST_VISIBLE_ROWS,
|
|
102
103
|
packageSourceListMoreLine,
|
|
103
|
-
} from "./package-source
|
|
104
|
-
import { createLectorReadOperations } from "./read
|
|
105
|
-
import { createReferenceBasedRenameOperations } from "./reference-based-rename
|
|
106
|
-
import { createRenameOperations } from "./rename
|
|
107
|
-
import { createRepoCacheEvictOperations } from "./repo-cache
|
|
108
|
-
import { createRepoCacheListOperations } from "./repo-cache
|
|
104
|
+
} from "./package-source/rendering.ts";
|
|
105
|
+
import { createLectorReadOperations } from "./read/operations.ts";
|
|
106
|
+
import { createReferenceBasedRenameOperations } from "./reference-based-rename/operations.ts";
|
|
107
|
+
import { createRenameOperations } from "./rename/operations.ts";
|
|
108
|
+
import { createRepoCacheEvictOperations } from "./repo-cache/evict-operations.ts";
|
|
109
|
+
import { createRepoCacheListOperations } from "./repo-cache/list-operations.ts";
|
|
109
110
|
import {
|
|
110
111
|
buildRepoCacheTableRows,
|
|
111
112
|
formatRepoCacheCall,
|
|
@@ -115,21 +116,21 @@ import {
|
|
|
115
116
|
REPO_CACHE_TABLE_COLUMNS,
|
|
116
117
|
REPO_CACHE_VISIBLE_ROWS,
|
|
117
118
|
repoCacheMoreLine,
|
|
118
|
-
} from "./repo-cache
|
|
119
|
-
import { createLectorRepoFetchOperations } from "./repo-fetch
|
|
120
|
-
import { createLectorSearchOperations } from "./search
|
|
121
|
-
import { formatSearchCall, formatSearchResult } from "./search
|
|
122
|
-
import { type AnnotationAnchorInput, createLectorSymbolAnnotationOperations } from "./symbol-annotation
|
|
123
|
-
import { formatAnnotationDetail, formatAnnotationListSummary, formatAnnotationSummary } from "./symbol-annotation
|
|
119
|
+
} from "./repo-cache/rendering.ts";
|
|
120
|
+
import { createLectorRepoFetchOperations } from "./repo-fetch/operations.ts";
|
|
121
|
+
import { createLectorSearchOperations } from "./search/operations.ts";
|
|
122
|
+
import { formatSearchCall, formatSearchResult } from "./search/rendering.ts";
|
|
123
|
+
import { type AnnotationAnchorInput, createLectorSymbolAnnotationOperations } from "./symbol-annotation/operations.ts";
|
|
124
|
+
import { formatAnnotationDetail, formatAnnotationListSummary, formatAnnotationSummary } from "./symbol-annotation/rendering.ts";
|
|
124
125
|
import {
|
|
125
126
|
type CachePresentationState,
|
|
126
127
|
cacheContextMessage,
|
|
127
128
|
createWorkspaceCacheOperations,
|
|
128
129
|
describeCacheState,
|
|
129
130
|
monitorWorkspaceCache,
|
|
130
|
-
} from "./workspace-cache
|
|
131
|
-
import { formatJobSnapshotResult, formatWorkspaceCacheCall, formatWorkspaceCacheStatusResult } from "./workspace-cache
|
|
132
|
-
import { createLectorWriteOperations } from "./write
|
|
131
|
+
} from "./workspace-cache/operations.ts";
|
|
132
|
+
import { formatJobSnapshotResult, formatWorkspaceCacheCall, formatWorkspaceCacheStatusResult } from "./workspace-cache/rendering.ts";
|
|
133
|
+
import { createLectorWriteOperations } from "./write/operations.ts";
|
|
133
134
|
|
|
134
135
|
function describeIntelligenceSource(provenance: IntelligenceProvenance): string {
|
|
135
136
|
return `${provenance.fidelity} via ${provenance.backend}`;
|
|
@@ -345,6 +346,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
345
346
|
});
|
|
346
347
|
|
|
347
348
|
const codeIntelligenceOperations = createLectorCodeIntelligenceOperations();
|
|
349
|
+
|
|
350
|
+
pi.registerCommand("editor", {
|
|
351
|
+
description: "Open a file in a neovim-style modal code editor",
|
|
352
|
+
handler: async (args, commandCtx) => {
|
|
353
|
+
const target = args.trim();
|
|
354
|
+
if (!target) {
|
|
355
|
+
commandCtx.ui.notify("Usage: /editor <path>", "error");
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
const absolutePath = resolve(commandCtx.cwd, target);
|
|
359
|
+
|
|
360
|
+
let session: Awaited<ReturnType<typeof openEditorFile>>;
|
|
361
|
+
try {
|
|
362
|
+
session = await openEditorFile(absolutePath);
|
|
363
|
+
} catch (error) {
|
|
364
|
+
commandCtx.ui.notify(`Could not open ${absolutePath}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
await commandCtx.ui.custom<void>(
|
|
369
|
+
(tui, theme, _keybindings, done) => {
|
|
370
|
+
const host: NeovimEditorHost = {
|
|
371
|
+
filePath: absolutePath,
|
|
372
|
+
save: (text) => session.save(text),
|
|
373
|
+
hover: async (line, character) => {
|
|
374
|
+
const result = await codeIntelligenceOperations.hover(absolutePath, line, character);
|
|
375
|
+
return result.hover;
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
return new NeovimEditorComponent(tui, theme, host, session.content, () => done(undefined));
|
|
379
|
+
},
|
|
380
|
+
{ overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", anchor: "center" } },
|
|
381
|
+
);
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
|
|
348
385
|
const referenceBasedRenameOperations = createReferenceBasedRenameOperations();
|
|
349
386
|
const renameOperations = createRenameOperations();
|
|
350
387
|
const positionParameters = {
|
|
@@ -1725,7 +1762,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1725
1762
|
name: "find_symbols_across_projects",
|
|
1726
1763
|
label: "Find Symbols Across Projects",
|
|
1727
1764
|
description:
|
|
1728
|
-
"Fans out a symbol-name search across several explicitly-named project directories at once (e.g. several fetched repos, or a handful of related local projects) and reports one outcome per project -- ready with real results, loading (a project's language server is still cold-starting; retry shortly), or error. Directories are required and explicit -- never every project this daemon happens to have registered, which can include unrelated projects from other concurrent sessions.",
|
|
1765
|
+
"Fans out a symbol-name search across several explicitly-named project directories at once (e.g. several fetched repos, or a handful of related local projects) and reports one outcome per project -- ready with real results, loading (a project's language server is still cold-starting; retry shortly), or error. Directories are required and explicit -- never every project this daemon happens to have registered, which can include unrelated projects from other concurrent sessions. Each directory resolves to its OWN nearest project root (package.json/tsconfig.json/go.mod/Cargo.toml/...), not the outer repo's git root -- sibling packages under one monorepo stay distinct scopes rather than collapsing into one. A result's collapsedWith lists any other requested directories that genuinely did resolve to the same workspace; empty means it got its own.",
|
|
1729
1766
|
promptSnippet: "Search for a symbol name across several projects at once",
|
|
1730
1767
|
parameters: Type.Object({
|
|
1731
1768
|
directories: Type.Array(Type.String(), { description: "Project directories to search, each absolute or relative to the current working directory" }),
|
|
@@ -1751,7 +1788,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1751
1788
|
.join("\n");
|
|
1752
1789
|
return new Text(theme.fg("error", errorText || "find_symbols_across_projects failed"), 0, 0);
|
|
1753
1790
|
}
|
|
1754
|
-
const details = result.details as { results?: readonly
|
|
1791
|
+
const details = result.details as { results?: readonly CrossWorkspaceOutcome<SymbolSearchResult>[] } | undefined;
|
|
1755
1792
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1756
1793
|
text.setText(formatFindSymbolsAcrossProjectsResult(details?.results, expanded, theme));
|
|
1757
1794
|
return text;
|
|
@@ -1762,7 +1799,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1762
1799
|
name: "search_code_across_projects",
|
|
1763
1800
|
label: "Search Code Across Projects",
|
|
1764
1801
|
description:
|
|
1765
|
-
"Fans out a ripgrep-backed text/regex search across several explicitly-named project directories at once and reports one outcome per project. Directories are required and explicit -- never every project this daemon happens to have registered, which can include unrelated projects from other concurrent sessions.",
|
|
1802
|
+
"Fans out a ripgrep-backed text/regex search across several explicitly-named project directories at once and reports one outcome per project. Directories are required and explicit -- never every project this daemon happens to have registered, which can include unrelated projects from other concurrent sessions. Each directory resolves to its OWN nearest project root (package.json/tsconfig.json/go.mod/Cargo.toml/...), not the outer repo's git root -- sibling packages under one monorepo stay distinct scopes rather than collapsing into one. A result's collapsedWith lists any other requested directories that genuinely did resolve to the same workspace; empty means it got its own.",
|
|
1766
1803
|
promptSnippet: "Search for a pattern across several projects at once",
|
|
1767
1804
|
parameters: Type.Object({
|
|
1768
1805
|
directories: Type.Array(Type.String(), { description: "Project directories to search, each absolute or relative to the current working directory" }),
|
|
@@ -1790,7 +1827,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1790
1827
|
.join("\n");
|
|
1791
1828
|
return new Text(theme.fg("error", errorText || "search_code_across_projects failed"), 0, 0);
|
|
1792
1829
|
}
|
|
1793
|
-
const details = result.details as { results?: readonly
|
|
1830
|
+
const details = result.details as { results?: readonly CrossWorkspaceOutcome<TextSearchResult>[] } | undefined;
|
|
1794
1831
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1795
1832
|
text.setText(formatSearchTextAcrossProjectsResult(details?.results, expanded, theme));
|
|
1796
1833
|
return text;
|
|
@@ -3,6 +3,7 @@ import { dirname, extname, parse } from "node:path";
|
|
|
3
3
|
import {
|
|
4
4
|
connectLectorClient,
|
|
5
5
|
descriptorForExtension,
|
|
6
|
+
LANGUAGE_SERVER_DESCRIPTORS,
|
|
6
7
|
type LectorClient,
|
|
7
8
|
type OperationInputs,
|
|
8
9
|
type OperationName,
|
|
@@ -142,6 +143,30 @@ export function workspaceForCodeIntelligencePath(absolutePath: string): Promise<
|
|
|
142
143
|
return workspaceForRoot(root);
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
/** Every known language's own rootMarkers, deduplicated -- see workspaceForProjectDirectory. */
|
|
147
|
+
const ALL_PROJECT_ROOT_MARKERS: readonly string[] = [...new Set(LANGUAGE_SERVER_DESCRIPTORS.flatMap((descriptor) => descriptor.rootMarkers))];
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Resolves a caller-supplied directory to its OWN nearest project root -- never the outer repo's
|
|
151
|
+
* git root -- so distinct sibling packages under one monorepo stay distinct workspaces. Unlike
|
|
152
|
+
* workspaceForDirectory (used by find_symbols/read/write, where one canonical workspaceId per
|
|
153
|
+
* repo is exactly the point), this is for a tool whose entire premise is comparing *different*
|
|
154
|
+
* scopes (find_symbols_across_projects, search_code_across_projects): collapsing two sibling
|
|
155
|
+
* packages into the same workspaceId there silently duplicates one package's own results under
|
|
156
|
+
* the other's name, with no error at all -- confirmed live against this monorepo
|
|
157
|
+
* (packages/lector and packages/pi-lector both resolved to the same workspaceId).
|
|
158
|
+
*
|
|
159
|
+
* Unlike workspaceForCodeIntelligencePath, there is no single file (and therefore no known
|
|
160
|
+
* extension) to pick one specific language's markers from -- a caller-supplied directory could
|
|
161
|
+
* be any language, so this checks the union of every known language's rootMarkers. Falls back to
|
|
162
|
+
* the nearest git root, then the directory itself, exactly as nearestProjectRoot already does
|
|
163
|
+
* internally (it appends ".git" to whatever marker list it's given).
|
|
164
|
+
*/
|
|
165
|
+
export function workspaceForProjectDirectory(directory: string): Promise<ResolvedWorkspace> {
|
|
166
|
+
const root = nearestProjectRoot(directory, ALL_PROJECT_ROOT_MARKERS) ?? directory;
|
|
167
|
+
return workspaceForRoot(root);
|
|
168
|
+
}
|
|
169
|
+
|
|
145
170
|
/**
|
|
146
171
|
* For an operation whose `path` genuinely means "the project/workspace itself"
|
|
147
172
|
* (populateSymbolGraph, workspaceMap, hasWarmIndex) rather than one specific file
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { type LineEdit, type LineEditOutcome, type LineHash, lineHashOf } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, withWorkspace, workspaceForPath } from "
|
|
3
|
-
import { toWorkspaceRelativePath } from "
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForPath } from "../lector-client.ts";
|
|
3
|
+
import { toWorkspaceRelativePath } from "../workspace-relative-path.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Thin wrapper over workspace.lineEdit -- distinct from the generic edit tool (backed by
|
|
7
|
-
* exactEdit's whole-file hash guard, see edit
|
|
7
|
+
* exactEdit's whole-file hash guard, see edit/operations.ts): every edit here is guarded by
|
|
8
8
|
* its own referenced line(s)' hash, so a concurrent change to a line no edit references never
|
|
9
|
-
* invalidates this one. `path` is an absolute file path, the same convention edit
|
|
9
|
+
* invalidates this one. `path` is an absolute file path, the same convention edit/operations.ts
|
|
10
10
|
* already uses -- the workspace is resolved from the file itself, not a separate directory arg.
|
|
11
11
|
*/
|
|
12
12
|
export interface LineEditOperations {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { LineEditOutcome } from "@danypops/lector";
|
|
2
|
-
import type { LectorTheme } from "
|
|
2
|
+
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
3
3
|
|
|
4
4
|
export function formatLineEditCall(args: { path?: unknown; edits?: unknown }, theme: LectorTheme): string {
|
|
5
5
|
const path = typeof args.path === "string" ? args.path : "";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { MutationHistoryEntry } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, withWorkspace, workspaceForPath } from "
|
|
3
|
-
import { toWorkspaceRelativePath } from "
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForPath } from "../lector-client.ts";
|
|
3
|
+
import { toWorkspaceRelativePath } from "../workspace-relative-path.ts";
|
|
4
4
|
|
|
5
5
|
/** Thin wrapper over Lector's mutation history: every successful edit is recorded, and any entry can be reverted -- guarded the same way every other Lector write is. */
|
|
6
6
|
export interface MutationHistoryOperations {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { DEFAULT_PACKAGE_SOURCE_BOUNDS, type PackageEcosystem, type PackageSourceListEntry, type PackageSourceOperationResult } from "@danypops/lector";
|
|
2
|
-
import { lectorClient } from "
|
|
2
|
+
import { lectorClient } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
export interface PackageSourceListPage {
|
|
5
5
|
readonly entries: readonly PackageSourceListEntry[];
|