@@ -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+ " \x 1b\\ ][^\x 07\x 1b]*\\ (?:\x 07\\ |\x 1b\\\\\\ )" " " text))
646+ (text (replace-regexp-in-string
647+ " \x 1b\\ [[0-9;?]*[ -/]*[@-~]" " " text))
648+ (text (replace-regexp-in-string " [\x 00-\x 1f\x 7f]" " " 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))))
0 commit comments