@danypops/pi-lector 0.9.6 → 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 → cross-workspace-search/operations.ts} +1 -1
- package/extension/src/{cross-workspace-search-rendering.ts → cross-workspace-search/rendering.ts} +18 -6
- 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 +72 -34
- 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/{code-intelligence-hints.ts → code-intelligence/hints.ts} +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import type { ContentHash, EditOutcome } 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.applyPatch -- distinct from both the generic edit tool
|
|
7
7
|
* (whole-file replace) and line_edit (per-line hash guards): applies a real unified diff's
|
|
8
8
|
* hunks, guarded by one whole-file expectedHash (a patch inherently describes a
|
|
9
9
|
* whole-file transformation from a known pre-image). `path` is an absolute file path, the
|
|
10
|
-
* same convention edit
|
|
10
|
+
* same convention edit/operations.ts and line-edit/operations.ts already use.
|
|
11
11
|
*/
|
|
12
12
|
export interface ApplyPatchOperations {
|
|
13
13
|
applyPatch(path: string, expectedHash: ContentHash, patchText: string): Promise<EditOutcome>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EditOutcome } from "@danypops/lector";
|
|
2
2
|
import { renderDiffLines, renderTruncatedList, type TextMeasure } from "malevich-tui-components";
|
|
3
|
-
import type { LectorTheme } from "
|
|
3
|
+
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The applied result (EditOutcome) only carries a hash transition, not the
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JobSnapshot, OperationInputs, OperationOutputs, PopulateSymbolGraphResult, SymbolEdgeKind, SymbolNode } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath, workspaceForPathOrDirectory } from "
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath, workspaceForPathOrDirectory } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Thin wrappers over Lector's code-intelligence operations: goToDefinition,
|
|
@@ -39,7 +39,7 @@ export interface CodeIntelligenceOperations {
|
|
|
39
39
|
outgoingCalls(path: string, line: number, character: number): Promise<OperationOutputs["workspace.outgoingCalls"]>;
|
|
40
40
|
/**
|
|
41
41
|
* Not exposed as a standalone Pi tool -- every workspace auto-populates on first touch via
|
|
42
|
-
* monitorWorkspaceCache (workspace-cache
|
|
42
|
+
* monitorWorkspaceCache (workspace-cache/operations.ts). Kept here as an internal capability
|
|
43
43
|
* for tests that need a populated graph. For an explicit/custom-bound population outside
|
|
44
44
|
* Pi entirely, `lector workspace populate-symbol-graph` calls the daemon directly.
|
|
45
45
|
*/
|
|
@@ -12,7 +12,7 @@ import type {
|
|
|
12
12
|
} from "@danypops/lector";
|
|
13
13
|
import { keyHint, type ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { renderTruncatedList } from "malevich-tui-components";
|
|
15
|
-
import { colorForKind, formatLocation, type LectorTheme } from "
|
|
15
|
+
import { colorForKind, formatLocation, type LectorTheme } from "../lector-tui-theme.ts";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* Custom TUI rendering for the code-intelligence tools: go_to_definition,
|
package/extension/src/{cross-workspace-search-operations.ts → cross-workspace-search/operations.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SymbolSearchResult, TextSearchResult, WorkspaceQueryOutcome } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, workspaceForProjectDirectory } from "
|
|
2
|
+
import { lectorClient, workspaceForProjectDirectory } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Fans out across explicitly-named directories only -- never the daemon's own "every registered
|
package/extension/src/{cross-workspace-search-rendering.ts → cross-workspace-search/rendering.ts}
RENAMED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { SymbolSearchResult, TextSearchResult } from "@danypops/lector";
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { renderTruncatedList } from "malevich-tui-components";
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
import type {
|
|
4
|
+
import { describeFindSymbolSources } from "../find-symbols/rendering.ts";
|
|
5
|
+
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
6
|
+
import type { CrossWorkspaceOutcome } from "./operations.ts";
|
|
7
7
|
|
|
8
8
|
const DEFAULT_VISIBLE_PER_WORKSPACE = 10;
|
|
9
9
|
|
|
@@ -17,9 +17,21 @@ function formatOutcomeHeader(entry: CrossWorkspaceOutcome<unknown>, theme: Lecto
|
|
|
17
17
|
const { outcome } = entry;
|
|
18
18
|
const label = theme.fg("accent", entry.directory);
|
|
19
19
|
const lines: string[] = [];
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
switch (outcome.status) {
|
|
21
|
+
case "ready":
|
|
22
|
+
lines.push(label);
|
|
23
|
+
break;
|
|
24
|
+
case "loading":
|
|
25
|
+
lines.push(`${label} ${theme.fg("warning", `-- still loading: ${outcome.message}`)}`);
|
|
26
|
+
break;
|
|
27
|
+
case "error":
|
|
28
|
+
lines.push(`${label} ${theme.fg("error", `-- ${outcome.message}`)}`);
|
|
29
|
+
break;
|
|
30
|
+
default: {
|
|
31
|
+
const exhaustive: never = outcome;
|
|
32
|
+
throw new Error(`unhandled workspace query outcome status: ${JSON.stringify(exhaustive)}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
23
35
|
// Surfaced explicitly, never silently -- two distinct inputs resolving to one workspace means
|
|
24
36
|
// one of their own result payloads below is a real duplicate of the other's, not two independent answers.
|
|
25
37
|
if (entry.collapsedWith.length > 0) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ContentHash, remoteErrorIs } from "@danypops/lector";
|
|
2
2
|
import type { EditOperations } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { lectorClient, withWorkspace, workspaceForPath } from "
|
|
4
|
-
import { toWorkspaceRelativePath } from "
|
|
3
|
+
import { lectorClient, withWorkspace, workspaceForPath } from "../lector-client.ts";
|
|
4
|
+
import { toWorkspaceRelativePath } from "../workspace-relative-path.ts";
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* EditOperations backed by Lector's hash-guarded exactEdit. The workspace
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import { LiveBuffer } from "@danypops/lector";
|
|
2
|
+
|
|
3
|
+
export type EditorMode = "normal" | "insert" | "command";
|
|
4
|
+
|
|
5
|
+
export type EditorAction = { kind: "save" } | { kind: "save-and-quit" } | { kind: "quit" } | { kind: "hover" };
|
|
6
|
+
|
|
7
|
+
const BACKSPACE_KEYS = new Set(["\x7f", "\b"]);
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pure neovim-style modal editing state machine: mode transitions, cursor motion, and buffer
|
|
11
|
+
* edits, with no terminal/ANSI rendering and no I/O -- save/quit/hover requests surface as
|
|
12
|
+
* `pendingAction` for the hosting Component to actually perform (reading/writing through
|
|
13
|
+
* Lector's hash-guarded workspace operations lives outside this class entirely). Kept
|
|
14
|
+
* side-effect-free like this specifically so mode/motion/edit logic is unit-testable without a
|
|
15
|
+
* real terminal, per this environment's own "test behavior, not pixels" TUI-testing standard.
|
|
16
|
+
*/
|
|
17
|
+
export class EditorState {
|
|
18
|
+
buffer: LiveBuffer;
|
|
19
|
+
mode: EditorMode = "normal";
|
|
20
|
+
cursorLine = 1;
|
|
21
|
+
cursorCharacter = 1;
|
|
22
|
+
commandText = "";
|
|
23
|
+
pendingAction: EditorAction | undefined;
|
|
24
|
+
dirty = false;
|
|
25
|
+
|
|
26
|
+
private normalPrefix = "";
|
|
27
|
+
private insertPendingJ = false;
|
|
28
|
+
private yankedLine: string | undefined;
|
|
29
|
+
|
|
30
|
+
constructor(content: string) {
|
|
31
|
+
this.buffer = new LiveBuffer(content);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
get currentLineText(): string {
|
|
35
|
+
return this.buffer.lineText(this.cursorLine);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
handleKey(data: string): void {
|
|
39
|
+
this.pendingAction = undefined;
|
|
40
|
+
if (this.mode === "insert") {
|
|
41
|
+
this.handleInsertKey(data);
|
|
42
|
+
} else if (this.mode === "command") {
|
|
43
|
+
this.handleCommandKey(data);
|
|
44
|
+
} else {
|
|
45
|
+
this.handleNormalKey(data);
|
|
46
|
+
}
|
|
47
|
+
this.clampCursor();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
private clampCursor(): void {
|
|
51
|
+
this.cursorLine = Math.min(Math.max(this.cursorLine, 1), this.buffer.lineCount);
|
|
52
|
+
const lineLength = this.currentLineText.length;
|
|
53
|
+
const maxCharacter = this.mode === "insert" ? lineLength + 1 : Math.max(lineLength, 1);
|
|
54
|
+
this.cursorCharacter = Math.min(Math.max(this.cursorCharacter, 1), maxCharacter);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private enterInsert(): void {
|
|
58
|
+
this.mode = "insert";
|
|
59
|
+
this.insertPendingJ = false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── Normal mode ──────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
private handleNormalKey(data: string): void {
|
|
65
|
+
// Two-key sequences: dd, gg, yy.
|
|
66
|
+
if (this.normalPrefix) {
|
|
67
|
+
const prefix = this.normalPrefix;
|
|
68
|
+
this.normalPrefix = "";
|
|
69
|
+
if (prefix === "d" && data === "d") {
|
|
70
|
+
this.deleteCurrentLine();
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (prefix === "g" && data === "g") {
|
|
74
|
+
this.cursorLine = 1;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (prefix === "y" && data === "y") {
|
|
78
|
+
this.yankedLine = this.currentLineText;
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
// Prefix didn't complete into a known sequence -- fall through and handle `data` fresh.
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
switch (data) {
|
|
85
|
+
case "h":
|
|
86
|
+
this.cursorCharacter -= 1;
|
|
87
|
+
return;
|
|
88
|
+
case "l":
|
|
89
|
+
this.cursorCharacter += 1;
|
|
90
|
+
return;
|
|
91
|
+
case "k":
|
|
92
|
+
this.cursorLine -= 1;
|
|
93
|
+
return;
|
|
94
|
+
case "j":
|
|
95
|
+
this.cursorLine += 1;
|
|
96
|
+
return;
|
|
97
|
+
case "0":
|
|
98
|
+
this.cursorCharacter = 1;
|
|
99
|
+
return;
|
|
100
|
+
case "$":
|
|
101
|
+
this.cursorCharacter = Math.max(this.currentLineText.length, 1);
|
|
102
|
+
return;
|
|
103
|
+
case "G":
|
|
104
|
+
this.cursorLine = this.buffer.lineCount;
|
|
105
|
+
return;
|
|
106
|
+
case "d":
|
|
107
|
+
case "g":
|
|
108
|
+
case "y":
|
|
109
|
+
this.normalPrefix = data;
|
|
110
|
+
return;
|
|
111
|
+
case "i":
|
|
112
|
+
this.enterInsert();
|
|
113
|
+
return;
|
|
114
|
+
case "a":
|
|
115
|
+
this.cursorCharacter += 1;
|
|
116
|
+
this.enterInsert();
|
|
117
|
+
return;
|
|
118
|
+
case "I":
|
|
119
|
+
this.cursorCharacter = 1;
|
|
120
|
+
this.enterInsert();
|
|
121
|
+
return;
|
|
122
|
+
case "A":
|
|
123
|
+
this.cursorCharacter = this.currentLineText.length + 1;
|
|
124
|
+
this.enterInsert();
|
|
125
|
+
return;
|
|
126
|
+
case "o":
|
|
127
|
+
this.openLine(this.cursorLine);
|
|
128
|
+
return;
|
|
129
|
+
case "O":
|
|
130
|
+
this.openLine(this.cursorLine - 1);
|
|
131
|
+
return;
|
|
132
|
+
case "x":
|
|
133
|
+
this.deleteCharUnderCursor();
|
|
134
|
+
return;
|
|
135
|
+
case "p":
|
|
136
|
+
this.pasteYankedLine();
|
|
137
|
+
return;
|
|
138
|
+
case "u":
|
|
139
|
+
this.buffer.undo();
|
|
140
|
+
this.dirty = true;
|
|
141
|
+
return;
|
|
142
|
+
case "\x12": // Ctrl+R
|
|
143
|
+
this.buffer.redo();
|
|
144
|
+
this.dirty = true;
|
|
145
|
+
return;
|
|
146
|
+
case ":":
|
|
147
|
+
this.mode = "command";
|
|
148
|
+
this.commandText = "";
|
|
149
|
+
return;
|
|
150
|
+
case "\x13": // Ctrl+S
|
|
151
|
+
this.pendingAction = { kind: "save" };
|
|
152
|
+
return;
|
|
153
|
+
case "K":
|
|
154
|
+
this.pendingAction = { kind: "hover" };
|
|
155
|
+
return;
|
|
156
|
+
default:
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
private openLine(afterLine: number): void {
|
|
162
|
+
const offset = this.buffer.offsetAt({ line: Math.max(afterLine, 1), character: this.buffer.lineText(Math.max(afterLine, 1)).length + 1 });
|
|
163
|
+
this.buffer.insert(offset, "\n");
|
|
164
|
+
this.cursorLine = Math.max(afterLine, 1) + 1;
|
|
165
|
+
this.cursorCharacter = 1;
|
|
166
|
+
this.enterInsert();
|
|
167
|
+
this.dirty = true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private deleteCharUnderCursor(): void {
|
|
171
|
+
const line = this.currentLineText;
|
|
172
|
+
if (this.cursorCharacter > line.length) return;
|
|
173
|
+
const from = this.buffer.offsetAt({ line: this.cursorLine, character: this.cursorCharacter });
|
|
174
|
+
this.buffer.delete(from, from + 1);
|
|
175
|
+
this.dirty = true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private deleteCurrentLine(): void {
|
|
179
|
+
const isLastLine = this.cursorLine === this.buffer.lineCount;
|
|
180
|
+
const from = this.buffer.offsetAt({ line: this.cursorLine, character: 1 });
|
|
181
|
+
const to = isLastLine
|
|
182
|
+
? this.buffer.offsetAt({ line: this.cursorLine, character: this.currentLineText.length + 1 })
|
|
183
|
+
: this.buffer.offsetAt({ line: this.cursorLine + 1, character: 1 });
|
|
184
|
+
this.yankedLine = this.currentLineText;
|
|
185
|
+
this.buffer.delete(from, to);
|
|
186
|
+
if (isLastLine && this.cursorLine > 1) this.cursorLine -= 1;
|
|
187
|
+
this.cursorCharacter = 1;
|
|
188
|
+
this.dirty = true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private pasteYankedLine(): void {
|
|
192
|
+
if (this.yankedLine === undefined) return;
|
|
193
|
+
const offset = this.buffer.offsetAt({ line: this.cursorLine, character: this.currentLineText.length + 1 });
|
|
194
|
+
this.buffer.insert(offset, `\n${this.yankedLine}`);
|
|
195
|
+
this.cursorLine += 1;
|
|
196
|
+
this.cursorCharacter = 1;
|
|
197
|
+
this.dirty = true;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── Insert mode ──────────────────────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
private handleInsertKey(data: string): void {
|
|
203
|
+
if (data === "\x1b") {
|
|
204
|
+
this.insertPendingJ = false;
|
|
205
|
+
this.mode = "normal";
|
|
206
|
+
this.cursorCharacter = Math.max(this.cursorCharacter - 1, 1);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// "jk" chord exits insert without inserting either character -- mirrors the user's own
|
|
211
|
+
// real Neovim muscle memory (`map("i", "jk", "<ESC>")`).
|
|
212
|
+
if (this.insertPendingJ) {
|
|
213
|
+
this.insertPendingJ = false;
|
|
214
|
+
if (data === "k") {
|
|
215
|
+
this.mode = "normal";
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
this.insertCharacter("j");
|
|
219
|
+
// Fall through and handle `data` normally (it wasn't the "k" half of the chord).
|
|
220
|
+
}
|
|
221
|
+
if (data === "j" && data.length === 1) {
|
|
222
|
+
this.insertPendingJ = true;
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (BACKSPACE_KEYS.has(data)) {
|
|
227
|
+
this.backspace();
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (data === "\r" || data === "\n") {
|
|
231
|
+
this.insertNewline();
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
235
|
+
this.insertCharacter(data);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
// Unhandled control sequence (arrow keys, etc.) -- ignored in this v1 walking skeleton.
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
private insertCharacter(char: string): void {
|
|
242
|
+
const offset = this.buffer.offsetAt({ line: this.cursorLine, character: this.cursorCharacter });
|
|
243
|
+
this.buffer.insert(offset, char);
|
|
244
|
+
this.cursorCharacter += char.length;
|
|
245
|
+
this.dirty = true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private insertNewline(): void {
|
|
249
|
+
const offset = this.buffer.offsetAt({ line: this.cursorLine, character: this.cursorCharacter });
|
|
250
|
+
this.buffer.insert(offset, "\n");
|
|
251
|
+
this.cursorLine += 1;
|
|
252
|
+
this.cursorCharacter = 1;
|
|
253
|
+
this.dirty = true;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
private backspace(): void {
|
|
257
|
+
if (this.cursorCharacter > 1) {
|
|
258
|
+
const offset = this.buffer.offsetAt({ line: this.cursorLine, character: this.cursorCharacter });
|
|
259
|
+
this.buffer.delete(offset - 1, offset);
|
|
260
|
+
this.cursorCharacter -= 1;
|
|
261
|
+
this.dirty = true;
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
if (this.cursorLine > 1) {
|
|
265
|
+
const previousLineLength = this.buffer.lineText(this.cursorLine - 1).length;
|
|
266
|
+
const offset = this.buffer.offsetAt({ line: this.cursorLine, character: 1 });
|
|
267
|
+
this.buffer.delete(offset - 1, offset);
|
|
268
|
+
this.cursorLine -= 1;
|
|
269
|
+
this.cursorCharacter = previousLineLength + 1;
|
|
270
|
+
this.dirty = true;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// ── Command mode ─────────────────────────────────────────────────────────
|
|
275
|
+
|
|
276
|
+
private handleCommandKey(data: string): void {
|
|
277
|
+
if (data === "\x1b") {
|
|
278
|
+
this.mode = "normal";
|
|
279
|
+
this.commandText = "";
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
if (data === "\r" || data === "\n") {
|
|
283
|
+
this.executeCommand(this.commandText);
|
|
284
|
+
this.mode = "normal";
|
|
285
|
+
this.commandText = "";
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
if (BACKSPACE_KEYS.has(data)) {
|
|
289
|
+
if (this.commandText.length === 0) {
|
|
290
|
+
this.mode = "normal";
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
this.commandText = this.commandText.slice(0, -1);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
if (data.length === 1 && data.charCodeAt(0) >= 32) {
|
|
297
|
+
this.commandText += data;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
private executeCommand(command: string): void {
|
|
302
|
+
switch (command) {
|
|
303
|
+
case "w":
|
|
304
|
+
this.pendingAction = { kind: "save" };
|
|
305
|
+
return;
|
|
306
|
+
case "q":
|
|
307
|
+
this.pendingAction = { kind: "quit" };
|
|
308
|
+
return;
|
|
309
|
+
case "wq":
|
|
310
|
+
case "x":
|
|
311
|
+
this.pendingAction = { kind: "save-and-quit" };
|
|
312
|
+
return;
|
|
313
|
+
default:
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
@@ -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
|
@@ -43,9 +43,9 @@ import { Type } from "typebox";
|
|
|
43
43
|
/** Real ANSI-aware measurement for Table -- Malevich's own default is ASCII-only, unsafe against theme-styled cell/header text. */
|
|
44
44
|
const tableMeasure: TextMeasure = { visibleWidth, truncateToWidth };
|
|
45
45
|
|
|
46
|
-
import { createLectorApplyPatchOperations } from "./apply-patch
|
|
47
|
-
import { formatApplyPatchCall, formatApplyPatchResult } from "./apply-patch
|
|
48
|
-
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";
|
|
49
49
|
import {
|
|
50
50
|
type CallHierarchyToolDetails,
|
|
51
51
|
formatCallHierarchyCall,
|
|
@@ -66,29 +66,31 @@ import {
|
|
|
66
66
|
formatReachableFromResult,
|
|
67
67
|
formatWorkspaceMapCall,
|
|
68
68
|
formatWorkspaceMapResult,
|
|
69
|
-
} from "./code-intelligence
|
|
70
|
-
import { type CrossWorkspaceOutcome, createLectorCrossWorkspaceSearchOperations } from "./cross-workspace-search
|
|
71
|
-
import { formatCrossWorkspaceCall, formatFindSymbolsAcrossProjectsResult, formatSearchTextAcrossProjectsResult } from "./cross-workspace-search
|
|
72
|
-
import { createLectorEditOperations } from "./edit
|
|
73
|
-
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";
|
|
74
76
|
import {
|
|
75
77
|
formatExternalSearchCall,
|
|
76
78
|
formatGithubRepoSearchResult,
|
|
77
79
|
formatNpmPackageSearchResult,
|
|
78
80
|
formatSourcegraphCodeSearchResult,
|
|
79
|
-
} from "./external-search
|
|
80
|
-
import { createLectorFindFilesOperations } from "./find-files
|
|
81
|
-
import { formatFindFilesCall, formatFindFilesResult } from "./find-files
|
|
82
|
-
import { createLectorFindSymbolsOperations } from "./find-symbols
|
|
83
|
-
import { describeFindSymbolSources, formatFindSymbolsCall, formatFindSymbolsResult } from "./find-symbols
|
|
84
|
-
import { createLectorGitOperations } from "./git
|
|
85
|
-
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";
|
|
86
88
|
import { setNewWorkspaceObserver } from "./lector-client.ts";
|
|
87
|
-
import { createLectorLineEditOperations } from "./line-edit
|
|
88
|
-
import { formatLineEditCall, formatLineEditResult } from "./line-edit
|
|
89
|
-
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";
|
|
90
92
|
import { isFilesystemRoot, nearestGitRoot } from "./nearest-workspace-root.ts";
|
|
91
|
-
import { createLectorPackageSourceOperations, type PackageSourceListPage } from "./package-source
|
|
93
|
+
import { createLectorPackageSourceOperations, type PackageSourceListPage } from "./package-source/operations.ts";
|
|
92
94
|
import {
|
|
93
95
|
buildPackageSourceListTableRows,
|
|
94
96
|
formatPackageSourceCall,
|
|
@@ -99,12 +101,12 @@ import {
|
|
|
99
101
|
PACKAGE_SOURCE_LIST_TABLE_COLUMNS,
|
|
100
102
|
PACKAGE_SOURCE_LIST_VISIBLE_ROWS,
|
|
101
103
|
packageSourceListMoreLine,
|
|
102
|
-
} from "./package-source
|
|
103
|
-
import { createLectorReadOperations } from "./read
|
|
104
|
-
import { createReferenceBasedRenameOperations } from "./reference-based-rename
|
|
105
|
-
import { createRenameOperations } from "./rename
|
|
106
|
-
import { createRepoCacheEvictOperations } from "./repo-cache
|
|
107
|
-
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";
|
|
108
110
|
import {
|
|
109
111
|
buildRepoCacheTableRows,
|
|
110
112
|
formatRepoCacheCall,
|
|
@@ -114,21 +116,21 @@ import {
|
|
|
114
116
|
REPO_CACHE_TABLE_COLUMNS,
|
|
115
117
|
REPO_CACHE_VISIBLE_ROWS,
|
|
116
118
|
repoCacheMoreLine,
|
|
117
|
-
} from "./repo-cache
|
|
118
|
-
import { createLectorRepoFetchOperations } from "./repo-fetch
|
|
119
|
-
import { createLectorSearchOperations } from "./search
|
|
120
|
-
import { formatSearchCall, formatSearchResult } from "./search
|
|
121
|
-
import { type AnnotationAnchorInput, createLectorSymbolAnnotationOperations } from "./symbol-annotation
|
|
122
|
-
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";
|
|
123
125
|
import {
|
|
124
126
|
type CachePresentationState,
|
|
125
127
|
cacheContextMessage,
|
|
126
128
|
createWorkspaceCacheOperations,
|
|
127
129
|
describeCacheState,
|
|
128
130
|
monitorWorkspaceCache,
|
|
129
|
-
} from "./workspace-cache
|
|
130
|
-
import { formatJobSnapshotResult, formatWorkspaceCacheCall, formatWorkspaceCacheStatusResult } from "./workspace-cache
|
|
131
|
-
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";
|
|
132
134
|
|
|
133
135
|
function describeIntelligenceSource(provenance: IntelligenceProvenance): string {
|
|
134
136
|
return `${provenance.fidelity} via ${provenance.backend}`;
|
|
@@ -344,6 +346,42 @@ export default function (pi: ExtensionAPI) {
|
|
|
344
346
|
});
|
|
345
347
|
|
|
346
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
|
+
|
|
347
385
|
const referenceBasedRenameOperations = createReferenceBasedRenameOperations();
|
|
348
386
|
const renameOperations = createRenameOperations();
|
|
349
387
|
const positionParameters = {
|
|
@@ -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[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { PackageSourceListEntry, PackageSourceOperationResult } from "@danypops/lector";
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { renderTruncatedList, type TableColumn } 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_CANDIDATES = 5;
|
|
7
7
|
|
|
@@ -36,33 +36,40 @@ export function formatPackageSourceCall(
|
|
|
36
36
|
export function formatPackageSourceResult(result: PackageSourceOperationResult | undefined, expanded: boolean, theme: LectorTheme): string {
|
|
37
37
|
if (!result) return theme.fg("dim", "No package-source result.");
|
|
38
38
|
const { outcome } = result;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
switch (outcome.status) {
|
|
40
|
+
case "verified":
|
|
41
|
+
return [
|
|
42
|
+
`${theme.fg("accent", result.workspaceId ?? "unregistered")} ${theme.fg("success", `${outcome.coordinate.name}@${outcome.coordinate.resolvedVersion}`)}`,
|
|
43
|
+
`${outcome.workspace.cachePath}`,
|
|
44
|
+
`${outcome.repository.url ?? "local source"}@${outcome.repository.resolvedRef ?? "local"} ${outcome.repository.commit ?? outcome.verification.integrity}`,
|
|
45
|
+
].join("\n");
|
|
46
|
+
case "ambiguous": {
|
|
47
|
+
const lines = [
|
|
48
|
+
theme.fg("warning", `Ambiguous package source (${outcome.code})`),
|
|
49
|
+
...renderTruncatedList({
|
|
50
|
+
items: outcome.candidates,
|
|
51
|
+
expanded,
|
|
52
|
+
visibleCount: DEFAULT_VISIBLE_CANDIDATES,
|
|
53
|
+
formatItem: (candidate) => `${candidate.version} -- ${candidate.source}`,
|
|
54
|
+
moreLine: (hidden) => theme.fg("dim", `… ${hidden} more`),
|
|
55
|
+
truncationWarning: outcome.truncated ? theme.fg("dim", "More candidates were truncated by the daemon.") : undefined,
|
|
56
|
+
}),
|
|
57
|
+
];
|
|
58
|
+
return lines.join("\n");
|
|
59
|
+
}
|
|
60
|
+
case "unauthenticated":
|
|
61
|
+
return theme.fg("warning", `Authentication required (${outcome.code}): configure ${outcome.requiredCredentialNames.join(", ")}`);
|
|
62
|
+
case "oversized":
|
|
63
|
+
return theme.fg("warning", `Source resolution exceeded ${outcome.resource} limit ${outcome.limit}.`);
|
|
64
|
+
case "mismatched":
|
|
65
|
+
return theme.fg("error", `Source mismatch (${outcome.code}): expected ${outcome.expected}, got ${outcome.actual}.`);
|
|
66
|
+
case "unavailable":
|
|
67
|
+
return theme.fg("warning", `Source unavailable (${outcome.code}).`);
|
|
68
|
+
default: {
|
|
69
|
+
const exhaustive: never = outcome;
|
|
70
|
+
throw new Error(`unhandled package source outcome status: ${JSON.stringify(exhaustive)}`);
|
|
71
|
+
}
|
|
45
72
|
}
|
|
46
|
-
if (outcome.status === "ambiguous") {
|
|
47
|
-
const lines = [
|
|
48
|
-
theme.fg("warning", `Ambiguous package source (${outcome.code})`),
|
|
49
|
-
...renderTruncatedList({
|
|
50
|
-
items: outcome.candidates,
|
|
51
|
-
expanded,
|
|
52
|
-
visibleCount: DEFAULT_VISIBLE_CANDIDATES,
|
|
53
|
-
formatItem: (candidate) => `${candidate.version} -- ${candidate.source}`,
|
|
54
|
-
moreLine: (hidden) => theme.fg("dim", `… ${hidden} more`),
|
|
55
|
-
truncationWarning: outcome.truncated ? theme.fg("dim", "More candidates were truncated by the daemon.") : undefined,
|
|
56
|
-
}),
|
|
57
|
-
];
|
|
58
|
-
return lines.join("\n");
|
|
59
|
-
}
|
|
60
|
-
if (outcome.status === "unauthenticated") {
|
|
61
|
-
return theme.fg("warning", `Authentication required (${outcome.code}): configure ${outcome.requiredCredentialNames.join(", ")}`);
|
|
62
|
-
}
|
|
63
|
-
if (outcome.status === "oversized") return theme.fg("warning", `Source resolution exceeded ${outcome.resource} limit ${outcome.limit}.`);
|
|
64
|
-
if (outcome.status === "mismatched") return theme.fg("error", `Source mismatch (${outcome.code}): expected ${outcome.expected}, got ${outcome.actual}.`);
|
|
65
|
-
return theme.fg("warning", `Source unavailable (${outcome.code}).`);
|
|
66
73
|
}
|
|
67
74
|
|
|
68
75
|
/** Empty-state fallback only -- a non-empty page renders as a real Table (see buildPackageSourceListTableRows) so the human channel actually shows what's resolved, not just a bare count. Mirrors formatRepoCacheListResult. */
|
|
@@ -74,7 +81,7 @@ export function formatPackageSourceListResult(
|
|
|
74
81
|
return count === 0 ? theme.fg("dim", "no resolved package sources") : theme.fg("success", `${count} resolved package source${count === 1 ? "" : "s"}`);
|
|
75
82
|
}
|
|
76
83
|
|
|
77
|
-
/** Powers of 1024, one decimal past the first. Mirrors repo-cache
|
|
84
|
+
/** Powers of 1024, one decimal past the first. Mirrors repo-cache/rendering.ts's own formatCacheSize -- kept as a small local duplicate rather than a shared export, matching that file's own precedent. */
|
|
78
85
|
function formatCacheSize(bytes: number): string {
|
|
79
86
|
const units = ["B", "KB", "MB", "GB", "TB"] as const;
|
|
80
87
|
let value = bytes;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { constants } from "node:fs";
|
|
2
2
|
import { access as fsAccess, readFile as fsReadFile } from "node:fs/promises";
|
|
3
3
|
import type { ReadOperations } from "@earendil-works/pi-coding-agent";
|
|
4
|
-
import { lectorClient, withWorkspace, workspaceForPath } from "
|
|
5
|
-
import { toWorkspaceRelativePath } from "
|
|
4
|
+
import { lectorClient, withWorkspace, workspaceForPath } from "../lector-client.ts";
|
|
5
|
+
import { toWorkspaceRelativePath } from "../workspace-relative-path.ts";
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* Lector's core domain (RawRead/ExpectedHashEdit) is deliberately text-only
|
package/extension/src/{reference-based-rename-operations.ts → reference-based-rename/operations.ts}
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OperationOutputs } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath } from "
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Thin wrapper over Lector's non-LSP reference-based rename: moves a file and rewrites every
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OperationOutputs } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath } from "
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Thin wrappers over Lector's LSP-driven prepareRename/rename -- position-based (path + 1-indexed
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { lectorClient } from "
|
|
1
|
+
import { lectorClient } from "../lector-client.ts";
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Thin wrapper over repo.evictCache -- no `directory`/workspaceForDirectory resolution (matching
|
|
5
|
-
* repo-fetch
|
|
5
|
+
* repo-fetch/operations.ts and repo-cache/list-operations.ts: this targets the daemon-wide fetch
|
|
6
6
|
* cache, not a workspace-scoped concept).
|
|
7
7
|
*/
|
|
8
8
|
export interface RepoCacheEvictOperations {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import type { CachedRepositoryPage } from "@danypops/lector";
|
|
2
|
-
import { lectorClient } from "
|
|
2
|
+
import { lectorClient } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Thin wrapper over repo.listCache -- no network, no cache mutation, no `directory`/
|
|
6
|
-
* workspaceForDirectory resolution (matching repo-fetch
|
|
6
|
+
* workspaceForDirectory resolution (matching repo-fetch/operations.ts: this queries the
|
|
7
7
|
* daemon-wide fetch cache, not a workspace-scoped concept).
|
|
8
8
|
*/
|
|
9
9
|
export interface RepoCacheListOperations {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CachedRepositoryEntry, CachedRepositoryPage, RepoFetchResult } from "@danypops/lector";
|
|
2
2
|
import { keyHint } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import type { TableColumn } from "malevich-tui-components";
|
|
4
|
-
import type { LectorTheme } from "
|
|
4
|
+
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
5
5
|
|
|
6
6
|
/** Table has no row-count bound of its own; a cache can grow arbitrarily large even though repo_cache's own `maxResults` bounds any one page, so the display itself still needs a cap independent of that. */
|
|
7
7
|
export const REPO_CACHE_VISIBLE_ROWS = 20;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { TextSearchResult } 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.searchText. `directory` is required, same convention as
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { TextSearchResult } 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_MATCHES = 20;
|
|
7
7
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { OperationInputs, OperationOutputs } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath } from "
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
/** A bare symbol position an anchor is given as -- symbolNodeId and the anchor's baseline file hash are derived server-side, never supplied by the caller. */
|
|
5
5
|
export interface AnnotationAnchorInput {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SymbolAnnotation } from "@danypops/lector";
|
|
2
|
-
import type { LectorTheme } from "
|
|
2
|
+
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
3
3
|
|
|
4
4
|
const STATUS_COLOR: Record<SymbolAnnotation["status"], "success" | "warning" | "dim"> = {
|
|
5
5
|
fresh: "success",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JobSnapshot, PopulateSymbolGraphResult, WorkspaceCacheStatus } from "@danypops/lector";
|
|
2
|
-
import { lectorClient, withWorkspace, workspaceForDirectory } from "
|
|
2
|
+
import { lectorClient, withWorkspace, workspaceForDirectory } from "../lector-client.ts";
|
|
3
3
|
|
|
4
4
|
export interface WorkspaceCacheOperations {
|
|
5
5
|
status(directory: string, maxFiles: number, maxSymbolsPerFile: number): Promise<WorkspaceCacheStatus>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JobSnapshot, PopulateSymbolGraphResult, WorkspaceCacheStatus } from "@danypops/lector";
|
|
2
|
-
import type { LectorTheme } from "
|
|
2
|
+
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
3
3
|
|
|
4
4
|
type WorkspaceCacheAction = "status" | "populate" | "job_status";
|
|
5
5
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ContentHash, remoteErrorIs } from "@danypops/lector";
|
|
2
2
|
import type { WriteOperations } from "@earendil-works/pi-coding-agent";
|
|
3
|
-
import { lectorClient, withWorkspace, workspaceForPath } from "
|
|
4
|
-
import { toWorkspaceRelativePath } from "
|
|
3
|
+
import { lectorClient, withWorkspace, workspaceForPath } from "../lector-client.ts";
|
|
4
|
+
import { toWorkspaceRelativePath } from "../workspace-relative-path.ts";
|
|
5
5
|
|
|
6
6
|
const MAX_STALE_HASH_RETRIES = 3;
|
|
7
7
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "Pi host adapter for Lector: overrides read/write/edit with a daemon-backed, hash-guarded filesystem",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@danypops/vehicle-client": "^0.2.0",
|
|
22
|
-
"@danypops/lector": "^0.
|
|
22
|
+
"@danypops/lector": "^0.14.0",
|
|
23
23
|
"malevich-tui-components": "^0.19.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
File without changes
|