|
16 | 16 | (require 'eca-util) |
17 | 17 | (require 'eca-api) |
18 | 18 | (require 'eca-chat-expandable) |
| 19 | +(require 'eca-chat-image) |
19 | 20 |
|
20 | 21 | ;; Forward declarations for eca-chat.el core |
21 | 22 | (defvar eca-chat-mode-map) |
@@ -323,36 +324,63 @@ If STATIC? return strs with no dynamic values." |
323 | 324 | (format ":L%d-L%d" start end))) |
324 | 325 | 'font-lock-face 'eca-chat-context-buffer-face))) |
325 | 326 | (_ (concat eca-chat-context-prefix "unknown:" type))))) |
326 | | - (propertize context-str |
327 | | - 'eca-chat-item-type 'context |
328 | | - 'eca-chat-item-str-length (length context-str) |
329 | | - 'eca-chat-context-item context))) |
| 327 | + (eca-chat--propertize-image-mention |
| 328 | + (propertize context-str |
| 329 | + 'eca-chat-item-type 'context |
| 330 | + 'eca-chat-item-str-length (length context-str) |
| 331 | + 'eca-chat-context-item context) |
| 332 | + (and (equal type "file") (plist-get context :path))))) |
330 | 333 |
|
331 | 334 | (defun eca-chat--filepath->str (filepath lines-range) |
332 | 335 | "Convert FILEPATH and LINES-RANGE to a presentable str in buffer." |
333 | 336 | (let* ((item-str (concat eca-chat-filepath-prefix |
334 | 337 | (eca-chat--context-presentable-path filepath) |
335 | 338 | (-when-let ((&plist :start start :end end) lines-range) |
336 | 339 | (format "(%d-%d)" start end))))) |
337 | | - (propertize item-str |
338 | | - 'eca-chat-item-type 'filepath |
339 | | - 'eca-chat-item-str-length (length item-str) |
340 | | - 'eca-chat-expanded-item-str (concat eca-chat-filepath-prefix |
341 | | - filepath |
342 | | - (-when-let ((&plist :start start :end end) lines-range) |
343 | | - (format ":L%d-L%d" start end))) |
344 | | - 'font-lock-face 'eca-chat-context-file-face))) |
| 340 | + (eca-chat--propertize-image-mention |
| 341 | + (propertize item-str |
| 342 | + 'eca-chat-item-type 'filepath |
| 343 | + 'eca-chat-item-str-length (length item-str) |
| 344 | + 'eca-chat-expanded-item-str (concat eca-chat-filepath-prefix |
| 345 | + filepath |
| 346 | + (-when-let ((&plist :start start :end end) lines-range) |
| 347 | + (format ":L%d-L%d" start end))) |
| 348 | + 'font-lock-face 'eca-chat-context-file-face) |
| 349 | + filepath))) |
| 350 | + |
| 351 | +(defun eca-chat--context-chips (start end) |
| 352 | + "Return (CONTEXT . IMAGE) for each context chip between START and END. |
| 353 | +IMAGE is the thumbnail the chip shows, or nil." |
| 354 | + (let ((chips '()) |
| 355 | + (pos start)) |
| 356 | + (while (< pos end) |
| 357 | + (let ((next (next-single-property-change pos 'eca-chat-context-item nil end))) |
| 358 | + (when-let* ((context (get-text-property pos 'eca-chat-context-item))) |
| 359 | + (push (cons context (-some-> (eca-chat--image-thumbnail-at pos next) |
| 360 | + (overlay-get 'display))) |
| 361 | + chips)) |
| 362 | + (setq pos next))) |
| 363 | + chips)) |
345 | 364 |
|
346 | 365 | (defun eca-chat--refresh-context () |
347 | | - "Refresh chat context." |
| 366 | + "Refresh chat context. |
| 367 | +Redrawn chips keep their thumbnail, or its absence when hidden with |
| 368 | +RET, while new ones follow `eca-chat-image-show-thumbnails'." |
348 | 369 | (save-excursion |
349 | 370 | (-some-> (eca-chat--prompt-context-field-ov) |
350 | 371 | (overlay-start) |
351 | 372 | (goto-char)) |
352 | | - (delete-region (point) (line-end-position)) |
353 | | - (seq-doseq (context eca-chat--context) |
354 | | - (eca-chat--insert (eca-chat--context->str context)) |
355 | | - (eca-chat--insert " ")) |
| 373 | + (let ((chips (eca-chat--context-chips (point) (line-end-position)))) |
| 374 | + (delete-region (point) (line-end-position)) |
| 375 | + (seq-doseq (context eca-chat--context) |
| 376 | + (let ((start (point)) |
| 377 | + (chip (assoc context chips))) |
| 378 | + (let ((eca-chat--inhibit-auto-thumbnails t)) |
| 379 | + (eca-chat--insert (eca-chat--context->str context))) |
| 380 | + (cond |
| 381 | + ((cdr chip) (eca-chat--show-image-thumbnail start (point) (cdr chip))) |
| 382 | + ((not chip) (eca-chat--auto-show-image-thumbnails start (point))))) |
| 383 | + (eca-chat--insert " "))) |
356 | 384 | (eca-chat--insert (propertize eca-chat-context-prefix 'font-lock-face 'eca-chat-context-unlinked-face)))) |
357 | 385 |
|
358 | 386 | (defun eca-chat--add-context (context) |
@@ -476,9 +504,8 @@ DATA is the binary image data as a string." |
476 | 504 | (eca-chat--select-window) |
477 | 505 | (if (eq 'system eca-chat-yank-image-context-location) |
478 | 506 | (eca-chat--add-context context) |
479 | | - (progn |
480 | | - (eca-chat--insert-prompt (concat (eca-chat--context->str context 'static) " ")) |
481 | | - (goto-char (+ (point) (+ 2 (length output-path)))))) |
| 507 | + (goto-char (eca-chat--insert-prompt |
| 508 | + (concat (eca-chat--context->str context 'static) " ")))) |
482 | 509 | (eca-info "Image added, size: %s" file-size))))) |
483 | 510 |
|
484 | 511 | (defun eca-chat--clipboard-image-p () |
@@ -602,6 +629,34 @@ file or directory." |
602 | 629 | (when (eq last-command-event ?\s) |
603 | 630 | (eca-chat--maybe-finalize-context-token))) |
604 | 631 |
|
| 632 | +(defconst eca-chat--item-properties |
| 633 | + '(eca-chat-item-type eca-chat-item-str-length eca-chat-context-item |
| 634 | + eca-chat-expanded-item-str eca-chat-image-path) |
| 635 | + "Text properties making a prompt item, like a context chip.") |
| 636 | + |
| 637 | +(defun eca-chat--drop-inherited-item-face () |
| 638 | + "Drop the item face a char typed right after an item inherited. |
| 639 | +Item properties don't stick to typed text, see |
| 640 | +`eca-chat--setup-item-stickiness', but faces do, since the query |
| 641 | +typed after the context line `@' takes its face." |
| 642 | + (let ((pos (1- (point)))) |
| 643 | + (when (and (> pos (point-min)) |
| 644 | + (get-text-property (1- pos) 'eca-chat-item-type) |
| 645 | + (not (get-text-property pos 'eca-chat-item-type))) |
| 646 | + (with-silent-modifications |
| 647 | + (remove-text-properties pos (point) '(font-lock-face nil)))))) |
| 648 | + |
| 649 | +(defun eca-chat--setup-item-stickiness () |
| 650 | + "Keep the text typed right after a prompt item out of it. |
| 651 | +Otherwise the typed text inherits the item properties: it looks |
| 652 | +glued to the item, gets deleted along with it and is replaced by |
| 653 | +the item when the prompt is sent." |
| 654 | + (setq-local text-property-default-nonsticky |
| 655 | + (append (mapcar (lambda (prop) (cons prop t)) |
| 656 | + eca-chat--item-properties) |
| 657 | + text-property-default-nonsticky)) |
| 658 | + (add-hook 'post-self-insert-hook #'eca-chat--drop-inherited-item-face nil t)) |
| 659 | + |
605 | 660 | (declare-function dired-get-marked-files "dired") |
606 | 661 | (declare-function treemacs-node-at-point "treemacs") |
607 | 662 | (declare-function treemacs-button-get "treemacs") |
@@ -917,12 +972,19 @@ Calls CB with the resulting message." |
917 | 972 | ((when-let ((item-type (get-text-property (point) 'eca-chat-item-type))) |
918 | 973 | (when-let ((item-str (get-text-property (point) 'eca-chat-expanded-item-str))) |
919 | 974 | (when-let ((face (get-text-property (point) 'font-lock-face))) |
920 | | - (funcall cb (format "%s: %s" |
921 | | - (pcase item-type |
922 | | - ('context "Context") |
923 | | - ('filepath "Filepath")) |
924 | | - (propertize item-str 'face face))) |
925 | | - t)))))) |
| 975 | + (funcall cb (concat (format "%s: %s" |
| 976 | + (pcase item-type |
| 977 | + ('context "Context") |
| 978 | + ('filepath "Filepath")) |
| 979 | + (propertize item-str 'face face)) |
| 980 | + (when (get-text-property (point) 'eca-chat-image-path) |
| 981 | + " (RET: toggle thumbnail)"))) |
| 982 | + t)))) |
| 983 | + ;; Image mentions of sent messages |
| 984 | + ((when-let* ((path (get-text-property (point) 'eca-chat-image-path))) |
| 985 | + (funcall cb (concat "Image: " (abbreviate-file-name path) |
| 986 | + " (RET: toggle thumbnail)")) |
| 987 | + t)))) |
926 | 988 |
|
927 | 989 | ;;;; Completion-at-point function |
928 | 990 |
|
|
0 commit comments