@smoose/pi-beautify 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.ts +62 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoose/pi-beautify",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Small visual polish extensions for pi coding agent",
5
5
  "type": "module",
6
6
  "files": [
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { CustomEditor, type ExtensionAPI, type KeybindingsManager, type Theme } from "@earendil-works/pi-coding-agent";
2
2
  import type { ImageContent } from "@earendil-works/pi-ai";
3
- import { truncateToWidth, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
3
+ import { getKeybindings, matchesKey, truncateToWidth, type EditorTheme, type TUI } from "@earendil-works/pi-tui";
4
4
  import { existsSync, readFileSync } from "node:fs";
5
5
  import { extname } from "node:path";
6
6
 
@@ -12,6 +12,7 @@ interface Attachment {
12
12
 
13
13
  const CLIPBOARD_PATH_RE = /(?:[^\s"'`<>]+[\\/])?pi-clipboard-[0-9a-f-]+\.(?:png|jpe?g|webp|gif)/gi;
14
14
  const TOKEN_RE = /\[image(\d+)\]/g;
15
+ const TOKEN_LINE_RE = /\[image\d+\]/g;
15
16
 
16
17
  function mimeTypeForPath(path: string): string {
17
18
  const ext = extname(path).toLowerCase();
@@ -45,10 +46,15 @@ class BeautifyEditor extends CustomEditor {
45
46
 
46
47
  handleInput(data: string): void {
47
48
  const isImagePaste = this.appKeybindings.matches(data, "app.clipboard.pasteImage");
49
+ if (this.deleteImageTokenAtCursor(data)) return;
48
50
  super.handleInput(data);
49
51
  if (isImagePaste) this.scheduleClipboardPathScan();
50
52
  }
51
53
 
54
+ insertTextAtCursor(text: string): void {
55
+ super.insertTextAtCursor(this.replaceClipboardPathsInText(text));
56
+ }
57
+
52
58
  render(width: number): string[] {
53
59
  let lines = super.render(width);
54
60
  const currentTheme = this.getTheme();
@@ -67,20 +73,72 @@ class BeautifyEditor extends CustomEditor {
67
73
  );
68
74
  }
69
75
 
76
+ private deleteImageTokenAtCursor(data: string): boolean {
77
+ const keybindings = getKeybindings();
78
+ const backward = keybindings.matches(data, "tui.editor.deleteCharBackward") || matchesKey(data, "shift+backspace");
79
+ const forward = keybindings.matches(data, "tui.editor.deleteCharForward") || matchesKey(data, "shift+delete");
80
+ if (!backward && !forward) return false;
81
+
82
+ const editor = this as unknown as {
83
+ state: { lines: string[]; cursorLine: number; cursorCol: number };
84
+ historyIndex: number;
85
+ lastAction: string | null;
86
+ pushUndoSnapshot: () => void;
87
+ setCursorCol: (col: number) => void;
88
+ };
89
+ const line = editor.state.lines[editor.state.cursorLine] || "";
90
+ const range = this.findImageTokenDeleteRange(line, editor.state.cursorCol, backward);
91
+ if (!range) return false;
92
+
93
+ editor.historyIndex = -1;
94
+ editor.lastAction = null;
95
+ editor.pushUndoSnapshot();
96
+ editor.state.lines[editor.state.cursorLine] = line.slice(0, range.start) + line.slice(range.end);
97
+ editor.setCursorCol(range.start);
98
+ this.attachments.delete(range.token);
99
+ if (this.onChange) this.onChange(this.getText());
100
+ this.tui.requestRender();
101
+ return true;
102
+ }
103
+
104
+ private findImageTokenDeleteRange(line: string, cursorCol: number, backward: boolean): { start: number; end: number; token: string } | undefined {
105
+ for (const match of line.matchAll(TOKEN_LINE_RE)) {
106
+ const token = match[0];
107
+ const start = match.index;
108
+ let end = start + token.length;
109
+ if (backward) {
110
+ if (start < cursorCol && cursorCol <= end) return { start, end, token };
111
+ if (cursorCol === end + 1 && line[end] === " ") return { start, end: end + 1, token };
112
+ } else if (start <= cursorCol && cursorCol < end) {
113
+ if (line[end] === " ") end += 1;
114
+ return { start, end, token };
115
+ }
116
+ }
117
+ return undefined;
118
+ }
119
+
70
120
  private replaceClipboardPaths(): void {
71
121
  const current = this.getText();
72
122
  let changed = false;
73
123
  const next = current.replace(CLIPBOARD_PATH_RE, (path) => {
74
- const token = imageChip(this.nextId++);
75
- this.attachments.set(token, { token, path, mimeType: mimeTypeForPath(path) });
76
124
  changed = true;
77
- return `${token} `;
125
+ return this.createImageToken(path);
78
126
  });
79
127
  if (changed) {
80
128
  this.setText(next);
81
129
  this.tui.requestRender();
82
130
  }
83
131
  }
132
+
133
+ private replaceClipboardPathsInText(text: string): string {
134
+ return text.replace(CLIPBOARD_PATH_RE, (path) => this.createImageToken(path));
135
+ }
136
+
137
+ private createImageToken(path: string): string {
138
+ const token = imageChip(this.nextId++);
139
+ this.attachments.set(token, { token, path, mimeType: mimeTypeForPath(path) });
140
+ return `${token} `;
141
+ }
84
142
  }
85
143
 
86
144
  function collectImageAttachments(text: string, attachments: Map<string, Attachment>): Attachment[] {