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."
(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)))))
"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 (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."
(save-excursion
(goto-char (or ansi-osc--marker begin))
(when (eq (char-before) ?\e) (backward-char))
(while (re-search-forward "\e]" end t)
(let ((pos0 (match-beginning 0))
(code (and (re-search-forward "\\=\\([0-9A-Za-z]*\\);" end t)
(match-string 1)))
(pos1 (point)))
(if (re-search-forward "\a\\|\e\\\\" end 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)))))))
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-marker t)
(let ((pos0 (match-beginning 0))
(code (and
(re-search-forward "\\=\\([0-9A-Za-z]*\\);" end-marker t)
(match-string 1)))
(pos1 (point)))
(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-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."))))