@xynogen/pix-search 0.2.1 → 0.3.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/README.md +6 -5
- package/package.json +1 -1
- package/src/editor.ts +38 -47
- package/src/extension.ts +23 -4
- package/src/picker.ts +3 -0
- package/src/rank.ts +0 -5
package/README.md
CHANGED
|
@@ -18,8 +18,9 @@ file picker with its own query input:
|
|
|
18
18
|
- **Git recency** — recently modified files (from `git log`) rank higher.
|
|
19
19
|
- **Depth penalty** — shallower entries win ties.
|
|
20
20
|
|
|
21
|
-
Picking a result inserts
|
|
22
|
-
|
|
21
|
+
Picking a result inserts `<path>…</path>` into the prompt; the model receives
|
|
22
|
+
that span verbatim, and `@xynogen/pix-display` renders it as an `@name` chip
|
|
23
|
+
when installed. Keys: `↑`/`↓` move, `⏎` insert, `Esc` cancel (a cancelled
|
|
23
24
|
pick inserts a literal `@`, so the keystroke is never swallowed).
|
|
24
25
|
|
|
25
26
|
`@` mid-token (e.g. an email `user@host`) is left as a literal `@` — only a
|
|
@@ -31,6 +32,6 @@ boundary `@` opens the picker.
|
|
|
31
32
|
pi install npm:@xynogen/pix-search
|
|
32
33
|
```
|
|
33
34
|
|
|
34
|
-
Standalone — **not** bundled by `@xynogen/pix-core`. It
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
Standalone — **not** bundled by `@xynogen/pix-core`. It wraps whatever editor
|
|
36
|
+
is already installed (via `getEditorComponent`/`setEditorComponent`), so it
|
|
37
|
+
composes with `pix-display` in either load order.
|
package/package.json
CHANGED
package/src/editor.ts
CHANGED
|
@@ -1,60 +1,51 @@
|
|
|
1
|
-
/**
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
* `@` and relying on the inline autocomplete dropdown.
|
|
5
|
-
*
|
|
6
|
-
* The editor stays dumb about UI plumbing: it calls the injected `openPicker`
|
|
7
|
-
* callback (wired to `ctx.ui.custom` in the extension) and inserts whatever
|
|
8
|
-
* token comes back. A cancelled pick inserts a literal `@` so the keystroke is
|
|
9
|
-
* never silently swallowed.
|
|
10
|
-
*/
|
|
1
|
+
/** Add the @ picker to an editor without replacing its paste state or rendering. */
|
|
2
|
+
import type { CustomEditor } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
11
4
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
// The CustomEditor constructor wants pi-coding-agent's KeybindingsManager,
|
|
17
|
-
// which is a structural superset of pi-tui's. The factory hands us a value the
|
|
18
|
-
// host already typed correctly, so we take it as the constructor's own param
|
|
19
|
-
// type rather than re-importing a non-exported type.
|
|
20
|
-
type KbManager = ConstructorParameters<typeof CustomEditor>[2];
|
|
5
|
+
/** Picked paths are marked as `<path>…</path>`; pix-display renders the span as a chip. */
|
|
6
|
+
export function pathToken(path: string): string {
|
|
7
|
+
return `<path>${path}</path> `;
|
|
8
|
+
}
|
|
21
9
|
|
|
22
10
|
export type OpenPicker = () => Promise<string | null>;
|
|
23
11
|
|
|
24
|
-
/**
|
|
25
|
-
* is at line start or the preceding character is whitespace. */
|
|
12
|
+
/** A mention starts at the beginning of a line or after whitespace. */
|
|
26
13
|
export function atStartsMention(before: string): boolean {
|
|
27
14
|
return before === "" || /\s$/.test(before);
|
|
28
15
|
}
|
|
29
16
|
|
|
30
|
-
export
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
17
|
+
export function attachPicker(
|
|
18
|
+
editor: CustomEditor,
|
|
19
|
+
tui: TUI,
|
|
20
|
+
openPicker: OpenPicker,
|
|
21
|
+
onError: (message: string) => void,
|
|
22
|
+
): void {
|
|
23
|
+
const handleInput = editor.handleInput.bind(editor);
|
|
24
|
+
let inPaste = false;
|
|
25
|
+
const runPicker = async () => {
|
|
26
|
+
try {
|
|
27
|
+
const path = await openPicker();
|
|
28
|
+
editor.insertTextAtCursor(path ? pathToken(path) : "@");
|
|
29
|
+
} catch (error) {
|
|
30
|
+
editor.insertTextAtCursor("@");
|
|
31
|
+
onError(`File picker failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
32
|
+
} finally {
|
|
33
|
+
tui.requestRender();
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
editor.handleInput = (data: string) => {
|
|
37
|
+
// Bracketed paste can arrive in separate chunks; a pasted @ is not a shortcut.
|
|
38
|
+
for (const marker of data.matchAll(/\x1b\[(200|201)~/g)) {
|
|
39
|
+
inPaste = marker[1] === "200";
|
|
40
|
+
}
|
|
41
|
+
if (!inPaste && data === "@") {
|
|
42
|
+
const cursor = editor.getCursor();
|
|
43
|
+
const line = editor.getLines()[cursor.line] ?? "";
|
|
44
44
|
if (atStartsMention(line.slice(0, cursor.col))) {
|
|
45
|
-
void
|
|
45
|
+
void runPicker();
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
}
|
|
49
|
-
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
private async runPicker(): Promise<void> {
|
|
53
|
-
const path = await this.openPicker();
|
|
54
|
-
this.insertTextAtCursor(path ? `${atToken(path)} ` : "@");
|
|
55
|
-
// The insert resolves outside the TUI input loop (after the overlay
|
|
56
|
-
// promise), so nothing schedules a frame — request one or the inserted
|
|
57
|
-
// text stays invisible until the next keystroke.
|
|
58
|
-
this.tui.requestRender();
|
|
59
|
-
}
|
|
49
|
+
handleInput(data);
|
|
50
|
+
};
|
|
60
51
|
}
|
package/src/extension.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* pix-search —
|
|
2
|
+
* pix-search — adds input handling that opens a file-picker
|
|
3
3
|
* overlay when you type `@` at a token boundary. The picker owns its own query
|
|
4
4
|
* input, so spaces in the query work natively (no `@"…"` quoting). It lists
|
|
5
5
|
* both files and their containing folders (so you can @-mention a directory),
|
|
6
6
|
* ranked by fuzzy score + git recency, with a live preview of the selection.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type
|
|
10
|
-
import {
|
|
9
|
+
import { CustomEditor, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
10
|
+
import { showTransientMessage } from "@xynogen/pix-pretty/transient-error";
|
|
11
|
+
import { attachPicker } from "./editor.ts";
|
|
11
12
|
import { FilePicker } from "./picker.ts";
|
|
12
13
|
import { withDirectories } from "./rank.ts";
|
|
13
14
|
import { loadRecency, type RecencyMap } from "./recency.ts";
|
|
@@ -15,6 +16,7 @@ import { rgFiles } from "./rg.ts";
|
|
|
15
16
|
|
|
16
17
|
export default function (pi: ExtensionAPI): void {
|
|
17
18
|
pi.on("session_start", async (_event, ctx) => {
|
|
19
|
+
if (ctx.mode !== "tui") return;
|
|
18
20
|
const cwd = ctx.cwd;
|
|
19
21
|
|
|
20
22
|
// Warm the git-recency cache in the background; picker reads whatever is
|
|
@@ -51,6 +53,23 @@ export default function (pi: ExtensionAPI): void {
|
|
|
51
53
|
{ overlay: true, overlayOptions: { anchor: "center", width: "60%", maxHeight: "60%" } },
|
|
52
54
|
);
|
|
53
55
|
|
|
54
|
-
ctx.ui.
|
|
56
|
+
const previous = ctx.ui.getEditorComponent();
|
|
57
|
+
ctx.ui.setEditorComponent((tui, theme, kb) => {
|
|
58
|
+
const editor = previous?.(tui, theme, kb) ?? new CustomEditor(tui, theme, kb);
|
|
59
|
+
// ponytail: cursor access requires CustomEditor. Keep other editors intact;
|
|
60
|
+
// add a public cursor adapter if a non-CustomEditor integration is needed.
|
|
61
|
+
if (editor instanceof CustomEditor) {
|
|
62
|
+
attachPicker(editor, tui, openPicker, (message) =>
|
|
63
|
+
showTransientMessage(ctx.ui, message, "error"),
|
|
64
|
+
);
|
|
65
|
+
} else {
|
|
66
|
+
showTransientMessage(
|
|
67
|
+
ctx.ui,
|
|
68
|
+
"pix-search: @ picker requires a CustomEditor; existing editor kept.",
|
|
69
|
+
"warning",
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
return editor;
|
|
73
|
+
});
|
|
55
74
|
});
|
|
56
75
|
}
|
package/src/picker.ts
CHANGED
|
@@ -233,6 +233,9 @@ export class FilePicker {
|
|
|
233
233
|
lines: rows,
|
|
234
234
|
color: (s) => theme.fg("accent", s),
|
|
235
235
|
fg: (s) => theme.fg("text", s),
|
|
236
|
+
// Solid modal fill so the terminal doesn't bleed through, matching the
|
|
237
|
+
// gate/confirm/ask overlays. Skipped when the theme lacks bg (mock themes).
|
|
238
|
+
bg: theme.bg ? (s) => theme.bg?.("customMessageBg", s) ?? s : undefined,
|
|
236
239
|
});
|
|
237
240
|
this.cachedWidth = width;
|
|
238
241
|
return this.cachedLines;
|
package/src/rank.ts
CHANGED
|
@@ -50,11 +50,6 @@ export function rankFiles(
|
|
|
50
50
|
return ranked.slice(0, limit);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/** Quote a path for insertion into the prompt only when it contains a space. */
|
|
54
|
-
export function atToken(path: string): string {
|
|
55
|
-
return path.includes(" ") ? `@"${path}"` : `@${path}`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
53
|
/**
|
|
59
54
|
* Expand a flat file list into files + the directories that contain them, so
|
|
60
55
|
* the picker can offer folders as selectable targets. Directories keep a
|