ansi-osc.el: Use marker (bug#78184)

* lisp/ansi-osc.el (ansi-osc-apply-on-region)
(ansi-osc-filter-region): Use marker to properly handle
unfinished escape sequence.

* test/lisp/ansi-osc-tests.el (ansi-osc-tests--strings)
(ansi-osc-tests-apply-region-no-handlers)
(ansi-osc-tests-apply-region-no-handlers-multiple-calls)
(ansi-osc-tests-filter-region)
(ansi-osc-tests-filter-region-with-multiple-calls): Cover
bug#78184.
This commit is contained in:
Matthias Meulien 2025-05-08 16:51:46 +02:00 committed by Eli Zaretskii
parent 32d911cddf
commit 6f8cee0331
2 changed files with 97 additions and 40 deletions

View file

@ -35,18 +35,32 @@
;;; Code: ;;; Code:
(defconst ansi-osc-control-seq-regexp ;; According to ECMA 48, section 8.3.89 "OSC - OPERATING SYSTEM COMMAND"
;; See ECMA 48, section 8.3.89 "OSC - OPERATING SYSTEM COMMAND". ;; OSC control sequences match:
"\e\\][\x08-\x0D]*[\x20-\x7E]*\\(\a\\|\e\\\\\\)" ;; "\e\\][\x08-\x0D]*[\x20-\x7E]*\\(\a\\|\e\\\\\\)"
"Regexp matching an OSC control sequence.")
(defvar-local ansi-osc--marker nil
"Marker pointing to the start of an escape sequence.
Used by `ansi-osc-filter-region' and `ansi-osc-apply-on-region' to store
position of an unfinished escape sequence, for the complete sequence to
be handled in next call.")
(defun ansi-osc-filter-region (begin end) (defun ansi-osc-filter-region (begin end)
"Filter out all OSC control sequences from region between BEGIN and END." "Filter out all OSC control sequences from region between BEGIN and END.
(save-excursion When an unfinished escape sequence is found, the start position is saved
(goto-char begin) to `ansi-osc--marker'. Later call will override BEGIN with the position
;; Delete escape sequences. pointed by `ansi-osc--marker'."
(while (re-search-forward ansi-osc-control-seq-regexp end t) (let ((end-marker (copy-marker end)))
(delete-region (match-beginning 0) (match-end 0))))) (save-excursion
(goto-char (or ansi-osc--marker begin))
(when (eq (char-before) ?\e) (backward-char))
(while (re-search-forward "\e]" end-marker t)
(let ((pos0 (match-beginning 0)))
(if (re-search-forward
"\\=[\x08-\x0D]*[\x20-\x7E]*\\(\a\\|\e\\\\\\)"
end-marker t)
(delete-region pos0 (point))
(setq ansi-osc--marker (copy-marker pos0))))))))
(defvar-local ansi-osc-handlers '(("2" . ansi-osc-window-title-handler) (defvar-local ansi-osc-handlers '(("2" . ansi-osc-window-title-handler)
("7" . ansi-osc-directory-tracker) ("7" . ansi-osc-directory-tracker)
@ -54,10 +68,6 @@
"Alist of handlers for OSC escape sequences. "Alist of handlers for OSC escape sequences.
See `ansi-osc-apply-on-region' for details.") See `ansi-osc-apply-on-region' for details.")
(defvar-local ansi-osc--marker nil)
;; The function `ansi-osc-apply-on-region' can set `ansi-osc--marker'
;; to the start position of an escape sequence without termination.
(defun ansi-osc-apply-on-region (begin end) (defun ansi-osc-apply-on-region (begin end)
"Interpret OSC escape sequences in region between BEGIN and END. "Interpret OSC escape sequences in region between BEGIN and END.
This function searches for escape sequences of the forms This function searches for escape sequences of the forms
@ -65,29 +75,33 @@ This function searches for escape sequences of the forms
ESC ] command ; text BEL ESC ] command ; text BEL
ESC ] command ; text ESC \\ ESC ] command ; text ESC \\
Every occurrence of such escape sequences is removed from the Every occurrence of such escape sequences is removed from the buffer.
buffer. Then, if `command' is a key in the alist that is the Then, if `command' is a key in the alist that is the value of the local
value of the local variable `ansi-osc-handlers', that key's variable `ansi-osc-handlers', that key's value, which should be a
value, which should be a function, is called with `command' and function, is called with `command' and `text' as arguments, with point
`text' as arguments, with point where the escape sequence was where the escape sequence was located. When an unfinished escape
located." sequence is identified, it's hidden and the start position is saved to
(save-excursion `ansi-osc--marker'. Later call will override BEGIN with the position
(goto-char (or ansi-osc--marker begin)) pointed by `ansi-osc--marker'."
(when (eq (char-before) ?\e) (backward-char)) (let ((end-marker (copy-marker end)))
(while (re-search-forward "\e]" end t) (save-excursion
(let ((pos0 (match-beginning 0)) (goto-char (or ansi-osc--marker begin))
(code (and (re-search-forward "\\=\\([0-9A-Za-z]*\\);" end t) (when (eq (char-before) ?\e) (backward-char))
(match-string 1))) (while (re-search-forward "\e]" end-marker t)
(pos1 (point))) (let ((pos0 (match-beginning 0))
(if (re-search-forward "\a\\|\e\\\\" end t) (code (and
(let ((text (buffer-substring-no-properties (re-search-forward "\\=\\([0-9A-Za-z]*\\);" end-marker t)
pos1 (match-beginning 0)))) (match-string 1)))
(setq ansi-osc--marker nil) (pos1 (point)))
(delete-region pos0 (point)) (if (re-search-forward "\a\\|\e\\\\" end-marker t)
(when-let* ((fun (cdr (assoc-string code ansi-osc-handlers)))) (let ((text (buffer-substring-no-properties
(funcall fun code text))) pos1 (match-beginning 0))))
(put-text-property pos0 end 'invisible t) (setq ansi-osc--marker nil)
(setq ansi-osc--marker (copy-marker pos0))))))) (delete-region pos0 (point))
(when-let* ((fun (cdr (assoc-string code ansi-osc-handlers))))
(funcall fun code text)))
(put-text-property pos0 end-marker 'invisible t)
(setq ansi-osc--marker (copy-marker pos0))))))))
;; Window title handling (OSC 2) ;; Window title handling (OSC 2)

View file

@ -30,8 +30,7 @@
(require 'ert) (require 'ert)
(defvar ansi-osc-tests--strings (defvar ansi-osc-tests--strings
`( `(("Hello World" "Hello World")
("Hello World" "Hello World")
;; window title ;; window title
("Buffer \e]2;A window title\e\\content" "Buffer content") ("Buffer \e]2;A window title\e\\content" "Buffer content")
@ -44,6 +43,10 @@
;; hyperlink ;; hyperlink
("\e]8;;http://example.com\e\\This is a link\e]8;;\e\\" "This is a link") ("\e]8;;http://example.com\e\\This is a link\e]8;;\e\\" "This is a link")
;; multiple sequences
("Escape \e]2;A window title\e\\sequence followed by \e]2;unfinished sequence"
"Escape sequence followed by \e]2;unfinished sequence")
)) ))
;; Don't output those strings to stdout since they may have ;; Don't output those strings to stdout since they may have
;; side-effects on the environment ;; side-effects on the environment
@ -54,4 +57,44 @@
(with-temp-buffer (with-temp-buffer
(insert input) (insert input)
(ansi-osc-apply-on-region (point-min) (point-max)) (ansi-osc-apply-on-region (point-min) (point-max))
(should (equal (buffer-string) text)))))) (should (equal
(buffer-substring-no-properties
(point-min) (point-max))
text))))))
(ert-deftest ansi-osc-tests-apply-region-no-handlers-multiple-calls ()
(let ((ansi-osc-handlers nil))
(with-temp-buffer
(insert
(concat "First set the window title \e]2;A window title\e\\"
"then change it\e]2;Another "))
(ansi-osc-apply-on-region (point-min) (point-max))
(let ((pos (point)))
(insert "title\e\\, and stop.")
(ansi-osc-apply-on-region pos (point-max)))
(should
(equal
(buffer-substring-no-properties (point-min) (point-max))
"First set the window title then change it, and stop.")))))
(ert-deftest ansi-osc-tests-filter-region ()
(pcase-dolist (`(,input ,text) ansi-osc-tests--strings)
(with-temp-buffer
(insert input)
(ansi-osc-filter-region (point-min) (point-max))
(should (equal (buffer-string) text)))))
(ert-deftest ansi-osc-tests-filter-region-with-multiple-calls ()
(with-temp-buffer
(insert
(concat "First set the window title \e]2;A window title\e\\"
"then change it\e]2;Another "))
(ansi-osc-filter-region (point-min) (point-max))
(let ((pos (point)))
(insert "title\e\\, and stop.")
(ansi-osc-filter-region pos (point-max)))
(should
(equal
(buffer-string)
"First set the window title then change it, and stop."))))