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

View file

@ -30,8 +30,7 @@
(require 'ert)
(defvar ansi-osc-tests--strings
`(
("Hello World" "Hello World")
`(("Hello World" "Hello World")
;; window title
("Buffer \e]2;A window title\e\\content" "Buffer content")
@ -44,6 +43,10 @@
;; hyperlink
("\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
;; side-effects on the environment
@ -54,4 +57,44 @@
(with-temp-buffer
(insert input)
(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."))))