Skip to content

Commit cd5eb26

Browse files
authored
Fix: Slowness of ai coding session given clickable session link feature (#267)
* Skip linkify for plain recent output * Reuse project file cache and avoid relinkify churn
1 parent aeb8028 commit cd5eb26

2 files changed

Lines changed: 163 additions & 24 deletions

File tree

ai-code-session-link.el

Lines changed: 93 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,26 @@ terminal output redraw."
104104
"\\)")
105105
"Regexp matching conservative symbol candidates near a file link.")
106106

107+
(defconst ai-code-session-link--recent-output-candidate-regexp
108+
(concat
109+
"\\(?:https?://"
110+
"\\|"
111+
ai-code-session-link--path-base-regexp
112+
"\\(?:[#:(][[:alnum:],L-]+\\)?"
113+
"\\|"
114+
ai-code-session-link--symbol-identifier-regexp "()"
115+
"\\|"
116+
ai-code-session-link--symbol-identifier-regexp
117+
"\\(?:\\.\\|::\\|#\\)"
118+
ai-code-session-link--symbol-identifier-regexp
119+
"\\|"
120+
ai-code-session-link--snake-case-symbol-regexp
121+
"\\|"
122+
"[[:alpha:]_][[:alnum:]_*!?]*--[[:alpha:]_*!?-]+"
123+
"\\|"
124+
"[[:alpha:]_][[:alnum:]_*!?]*-\\(?:mode\\|hook\\|command\\|function\\|local\\|p\\)\\)")
125+
"Regexp matching recent output that may contain session links.")
126+
107127
(defun ai-code-session-link--path-pattern (suffix)
108128
"Return a session link regexp for `ai-code-session-link--path-base-regexp' plus SUFFIX."
109129
(concat "\\(" ai-code-session-link--path-base-regexp "\\)" suffix))
@@ -135,6 +155,15 @@ terminal output redraw."
135155
(defvar-local ai-code-session-link--pending-tail-width 0
136156
"Pending tail width to rescan when delayed session linkification runs.")
137157

158+
(defvar-local ai-code-session-link--buffer-project-files-cache nil
159+
"Buffer-local project file cache reused across session relinkify passes.")
160+
161+
(defvar-local ai-code-session-link--last-region-bounds nil
162+
"Last relinkified region bounds used to skip unchanged property churn.")
163+
164+
(defvar-local ai-code-session-link--last-region-text nil
165+
"Last relinkified region text used to skip unchanged property churn.")
166+
138167
(defvar ai-code-session-link--project-files-cache nil
139168
"Dynamic cache of project file lists used during one linkify pass.")
140169

@@ -164,6 +193,17 @@ terminal output redraw."
164193
cached))
165194
(funcall compute)))
166195

196+
(defun ai-code-session-link--buffer-project-files-cache ()
197+
"Return the buffer-local cache of enumerated project files."
198+
(or ai-code-session-link--buffer-project-files-cache
199+
(setq ai-code-session-link--buffer-project-files-cache
200+
(make-hash-table :test 'equal))))
201+
202+
(defun ai-code-session-link--unchanged-region-p (bounds region-text)
203+
"Return non-nil when BOUNDS and REGION-TEXT match the last relinkified region."
204+
(and (equal ai-code-session-link--last-region-bounds bounds)
205+
(equal ai-code-session-link--last-region-text region-text)))
206+
167207
(defun ai-code-session-link--project-files (root)
168208
"Return absolute project files for ROOT."
169209
(when (file-directory-p root)
@@ -448,7 +488,8 @@ terminal output redraw."
448488

449489
(defun ai-code-session-link--linkify-file-region (start end)
450490
"Apply file session links between START and END."
451-
(let ((ai-code-session-link--project-files-cache (make-hash-table :test 'equal))
491+
(let ((ai-code-session-link--project-files-cache
492+
(ai-code-session-link--buffer-project-files-cache))
452493
(ai-code-session-link--resolved-path-cache (make-hash-table :test 'equal)))
453494
(let ((file-links (ai-code-session-link--collect-file-links start end)))
454495
(while file-links
@@ -566,32 +607,59 @@ terminal output redraw."
566607
(widen)
567608
(setq start (max (point-min) start)
568609
end (min (point-max) end))
569-
(let ((pos start))
570-
(while (< pos end)
571-
(let ((next (or (next-single-property-change
572-
pos 'ai-code-session-link nil end)
573-
end)))
574-
(when (get-text-property pos 'ai-code-session-link)
575-
(remove-text-properties
576-
pos next
577-
'(ai-code-session-link nil
578-
ai-code-session-symbol-link nil
579-
ai-code-session-symbol-file nil
580-
mouse-face nil
581-
help-echo nil
582-
keymap nil
583-
follow-link nil
584-
font-lock-face nil
585-
face nil)))
586-
(setq pos next))))
587-
(ai-code-session-link--linkify-url-region start end)
588-
(ai-code-session-link--linkify-file-region start end))))))
610+
(let ((bounds (cons start end))
611+
(region-text (buffer-substring-no-properties start end)))
612+
(unless (ai-code-session-link--unchanged-region-p bounds region-text)
613+
(let ((pos start))
614+
(while (< pos end)
615+
(let ((next (or (next-single-property-change
616+
pos 'ai-code-session-link nil end)
617+
end)))
618+
(when (get-text-property pos 'ai-code-session-link)
619+
(remove-text-properties
620+
pos next
621+
'(ai-code-session-link nil
622+
ai-code-session-symbol-link nil
623+
ai-code-session-symbol-file nil
624+
mouse-face nil
625+
help-echo nil
626+
keymap nil
627+
follow-link nil
628+
font-lock-face nil
629+
face nil)))
630+
(setq pos next))))
631+
(ai-code-session-link--linkify-url-region start end)
632+
(ai-code-session-link--linkify-file-region start end)
633+
(setq ai-code-session-link--last-region-bounds bounds
634+
ai-code-session-link--last-region-text region-text))))))))
589635

590636
(defun ai-code-session-link--recent-output-tail-width (output)
591637
"Return the tail width to rescan after OUTPUT."
592638
(max ai-code-session-link--linkify-min-tail-width
593639
(* 2 (length (or output "")))))
594640

641+
(defun ai-code-session-link--recent-output-plain-text (output)
642+
"Return OUTPUT with terminal control sequences removed."
643+
(let* ((text (or output ""))
644+
(text (replace-regexp-in-string
645+
"\x1b\\][^\x07\x1b]*\\(?:\x07\\|\x1b\\\\\\)" "" text))
646+
(text (replace-regexp-in-string
647+
"\x1b\\[[0-9;?]*[ -/]*[@-~]" "" text))
648+
(text (replace-regexp-in-string "[\x00-\x1f\x7f]" "" text)))
649+
text))
650+
651+
(defun ai-code-session-link--recent-output-may-contain-links-p (output)
652+
"Return non-nil when OUTPUT may introduce session links worth rescanning."
653+
(let ((text (ai-code-session-link--recent-output-plain-text output)))
654+
(and (not (string-empty-p text))
655+
(string-match-p ai-code-session-link--recent-output-candidate-regexp text))))
656+
657+
(defun ai-code-session-link--should-linkify-recent-output-p (buffer output)
658+
"Return non-nil when BUFFER and OUTPUT should trigger hot-path relinkification."
659+
(and ai-code-session-link-enabled
660+
(buffer-live-p buffer)
661+
(ai-code-session-link--recent-output-may-contain-links-p output)))
662+
595663
(defun ai-code-session-link--flush-scheduled-linkify ()
596664
"Apply any delayed session linkification pending in the current buffer."
597665
(let ((tail-width ai-code-session-link--pending-tail-width))
@@ -605,8 +673,7 @@ terminal output redraw."
605673

606674
(defun ai-code-session-link--schedule-linkify-recent-output (buffer output)
607675
"Linkify recent OUTPUT in BUFFER after terminal redraw settles."
608-
(when (and ai-code-session-link-enabled
609-
(buffer-live-p buffer))
676+
(when (ai-code-session-link--should-linkify-recent-output-p buffer output)
610677
(with-current-buffer buffer
611678
(setq ai-code-session-link--pending-tail-width
612679
(max ai-code-session-link--pending-tail-width
@@ -623,7 +690,9 @@ terminal output redraw."
623690

624691
(defun ai-code-session-link--linkify-recent-output (output)
625692
"Linkify the recent tail of the current session buffer after OUTPUT."
626-
(when ai-code-session-link-enabled
693+
(when (ai-code-session-link--should-linkify-recent-output-p
694+
(current-buffer)
695+
output)
627696
(let* ((visible-width (ai-code-session-link--recent-output-tail-width output))
628697
(end (point-max))
629698
(start (max (point-min) (- end visible-width))))

test/test_ai-code-session-link.el

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,64 @@
479479
(when (file-directory-p root)
480480
(delete-directory root t)))))
481481

482+
(ert-deftest ai-code-session-link-test-linkify-session-region-reuses-project-files-across-passes ()
483+
"Repeated relinkify passes should reuse project file enumeration."
484+
(let* ((root (make-temp-file "ai-code-session-links-project-cache-passes-" t))
485+
(project-files-count 0)
486+
(ai-code-session-link-enabled t))
487+
(unwind-protect
488+
(cl-letf (((symbol-function 'project-current)
489+
(lambda (&optional _maybe-prompt _dir)
490+
'mock-project))
491+
((symbol-function 'project-root)
492+
(lambda (_project)
493+
root))
494+
((symbol-function 'project-files)
495+
(lambda (_project &optional _dirs)
496+
(cl-incf project-files-count)
497+
'("src/UserService.java"))))
498+
(with-temp-buffer
499+
(setq-local ai-code-backends-infra--session-directory root)
500+
(insert "UserService.java:1\n")
501+
(ai-code-session-link--linkify-session-region (point-min) (point-max))
502+
(ai-code-session-link--linkify-session-region (point-min) (point-max))
503+
(should (= project-files-count 1))))
504+
(when (file-directory-p root)
505+
(delete-directory root t)))))
506+
507+
(ert-deftest ai-code-session-link-test-linkify-session-region-skips-unchanged-property-churn ()
508+
"Repeated linkify should not churn properties for unchanged session text."
509+
(let* ((root (make-temp-file "ai-code-session-links-stable-region-" t))
510+
(src-dir (expand-file-name "src" root))
511+
(file (expand-file-name "FileABC.java" src-dir))
512+
(ai-code-session-link-enabled t))
513+
(unwind-protect
514+
(progn
515+
(make-directory src-dir t)
516+
(with-temp-file file
517+
(insert "class FileABC {}\n"))
518+
(with-temp-buffer
519+
(setq-local ai-code-backends-infra--session-directory root)
520+
(insert "src/FileABC.java:42\n")
521+
(ai-code-session-link--linkify-session-region (point-min) (point-max))
522+
(let ((add-count 0)
523+
(remove-count 0)
524+
(orig-add (symbol-function 'add-text-properties))
525+
(orig-remove (symbol-function 'remove-text-properties)))
526+
(cl-letf (((symbol-function 'add-text-properties)
527+
(lambda (start end props &optional object)
528+
(cl-incf add-count)
529+
(funcall orig-add start end props object)))
530+
((symbol-function 'remove-text-properties)
531+
(lambda (start end props &optional object)
532+
(cl-incf remove-count)
533+
(funcall orig-remove start end props object))))
534+
(ai-code-session-link--linkify-session-region (point-min) (point-max)))
535+
(should (zerop add-count))
536+
(should (zerop remove-count)))))
537+
(when (file-directory-p root)
538+
(delete-directory root t)))))
539+
482540
(ert-deftest ai-code-session-link-test-navigate-symbol-at-point-falls-back-to-associated-file ()
483541
"Symbol navigation should fall back to the nearby file and move to the symbol."
484542
(let* ((root (make-temp-file "ai-code-session-links-symbol-nav-" t))
@@ -687,6 +745,18 @@
687745
(when (file-directory-p root)
688746
(delete-directory root t)))))
689747

748+
(ert-deftest ai-code-session-link-test-schedule-linkify-recent-output-skips-plain-prose ()
749+
"Plain prose output should not schedule hot-path session relinkification."
750+
(let ((ai-code-session-link-enabled t))
751+
(with-temp-buffer
752+
(setq ai-code-session-link--pending-tail-width 0
753+
ai-code-session-link--linkify-timer nil)
754+
(ai-code-session-link--schedule-linkify-recent-output
755+
(current-buffer)
756+
"Working on the next step now.\n")
757+
(should-not ai-code-session-link--linkify-timer)
758+
(should (zerop ai-code-session-link--pending-tail-width)))))
759+
690760
(provide 'test_ai-code-session-link)
691761

692762
;;; test_ai-code-session-link.el ends here

0 commit comments

Comments
 (0)