aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMatthias Meulien2025-05-08 16:51:46 +0200
committerEli Zaretskii2025-05-31 15:21:21 +0300
commit6f8cee03316e166e4204ba49fbb9964a075968ca (patch)
tree189b3e11f5281d9763376f30c0bd2453bdca1dd8
parent32d911cddfd6ccdf288247a5d7eaf3d9a977b87c (diff)
downloademacs-6f8cee03316e166e4204ba49fbb9964a075968ca.tar.gz
emacs-6f8cee03316e166e4204ba49fbb9964a075968ca.zip
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.
-rw-r--r--lisp/ansi-osc.el88
-rw-r--r--test/lisp/ansi-osc-tests.el49
2 files changed, 97 insertions, 40 deletions
diff --git a/lisp/ansi-osc.el b/lisp/ansi-osc.el
index 97d6f6c8754..06359779823 100644
--- a/lisp/ansi-osc.el
+++ b/lisp/ansi-osc.el
@@ -35,18 +35,32 @@
35 35
36;;; Code: 36;;; Code:
37 37
38(defconst ansi-osc-control-seq-regexp 38;; According to ECMA 48, section 8.3.89 "OSC - OPERATING SYSTEM COMMAND"
39 ;; See ECMA 48, section 8.3.89 "OSC - OPERATING SYSTEM COMMAND". 39;; OSC control sequences match:
40 "\e\\][\x08-\x0D]*[\x20-\x7E]*\\(\a\\|\e\\\\\\)" 40;; "\e\\][\x08-\x0D]*[\x20-\x7E]*\\(\a\\|\e\\\\\\)"
41 "Regexp matching an OSC control sequence.") 41
42(defvar-local ansi-osc--marker nil
43 "Marker pointing to the start of an escape sequence.
44Used by `ansi-osc-filter-region' and `ansi-osc-apply-on-region' to store
45position of an unfinished escape sequence, for the complete sequence to
46be handled in next call.")
42 47
43(defun ansi-osc-filter-region (begin end) 48(defun ansi-osc-filter-region (begin end)
44 "Filter out all OSC control sequences from region between BEGIN and END." 49 "Filter out all OSC control sequences from region between BEGIN and END.
45 (save-excursion 50When an unfinished escape sequence is found, the start position is saved
46 (goto-char begin) 51to `ansi-osc--marker'. Later call will override BEGIN with the position
47 ;; Delete escape sequences. 52pointed by `ansi-osc--marker'."
48 (while (re-search-forward ansi-osc-control-seq-regexp end t) 53 (let ((end-marker (copy-marker end)))
49 (delete-region (match-beginning 0) (match-end 0))))) 54 (save-excursion
55 (goto-char (or ansi-osc--marker begin))
56 (when (eq (char-before) ?\e) (backward-char))
57 (while (re-search-forward "\e]" end-marker t)
58 (let ((pos0 (match-beginning 0)))
59 (if (re-search-forward
60 "\\=[\x08-\x0D]*[\x20-\x7E]*\\(\a\\|\e\\\\\\)"
61 end-marker t)
62 (delete-region pos0 (point))
63 (setq ansi-osc--marker (copy-marker pos0))))))))
50 64
51(defvar-local ansi-osc-handlers '(("2" . ansi-osc-window-title-handler) 65(defvar-local ansi-osc-handlers '(("2" . ansi-osc-window-title-handler)
52 ("7" . ansi-osc-directory-tracker) 66 ("7" . ansi-osc-directory-tracker)
@@ -54,10 +68,6 @@
54 "Alist of handlers for OSC escape sequences. 68 "Alist of handlers for OSC escape sequences.
55See `ansi-osc-apply-on-region' for details.") 69See `ansi-osc-apply-on-region' for details.")
56 70
57(defvar-local ansi-osc--marker nil)
58;; The function `ansi-osc-apply-on-region' can set `ansi-osc--marker'
59;; to the start position of an escape sequence without termination.
60
61(defun ansi-osc-apply-on-region (begin end) 71(defun ansi-osc-apply-on-region (begin end)
62 "Interpret OSC escape sequences in region between BEGIN and END. 72 "Interpret OSC escape sequences in region between BEGIN and END.
63This function searches for escape sequences of the forms 73This function searches for escape sequences of the forms
@@ -65,29 +75,33 @@ This function searches for escape sequences of the forms
65 ESC ] command ; text BEL 75 ESC ] command ; text BEL
66 ESC ] command ; text ESC \\ 76 ESC ] command ; text ESC \\
67 77
68Every occurrence of such escape sequences is removed from the 78Every occurrence of such escape sequences is removed from the buffer.
69buffer. Then, if `command' is a key in the alist that is the 79Then, if `command' is a key in the alist that is the value of the local
70value of the local variable `ansi-osc-handlers', that key's 80variable `ansi-osc-handlers', that key's value, which should be a
71value, which should be a function, is called with `command' and 81function, is called with `command' and `text' as arguments, with point
72`text' as arguments, with point where the escape sequence was 82where the escape sequence was located. When an unfinished escape
73located." 83sequence is identified, it's hidden and the start position is saved to
74 (save-excursion 84`ansi-osc--marker'. Later call will override BEGIN with the position
75 (goto-char (or ansi-osc--marker begin)) 85pointed by `ansi-osc--marker'."
76 (when (eq (char-before) ?\e) (backward-char)) 86 (let ((end-marker (copy-marker end)))
77 (while (re-search-forward "\e]" end t) 87 (save-excursion
78 (let ((pos0 (match-beginning 0)) 88 (goto-char (or ansi-osc--marker begin))
79 (code (and (re-search-forward "\\=\\([0-9A-Za-z]*\\);" end t) 89 (when (eq (char-before) ?\e) (backward-char))
80 (match-string 1))) 90 (while (re-search-forward "\e]" end-marker t)
81 (pos1 (point))) 91 (let ((pos0 (match-beginning 0))
82 (if (re-search-forward "\a\\|\e\\\\" end t) 92 (code (and
83 (let ((text (buffer-substring-no-properties 93 (re-search-forward "\\=\\([0-9A-Za-z]*\\);" end-marker t)
84 pos1 (match-beginning 0)))) 94 (match-string 1)))
85 (setq ansi-osc--marker nil) 95 (pos1 (point)))
86 (delete-region pos0 (point)) 96 (if (re-search-forward "\a\\|\e\\\\" end-marker t)
87 (when-let* ((fun (cdr (assoc-string code ansi-osc-handlers)))) 97 (let ((text (buffer-substring-no-properties
88 (funcall fun code text))) 98 pos1 (match-beginning 0))))
89 (put-text-property pos0 end 'invisible t) 99 (setq ansi-osc--marker nil)
90 (setq ansi-osc--marker (copy-marker pos0))))))) 100 (delete-region pos0 (point))
101 (when-let* ((fun (cdr (assoc-string code ansi-osc-handlers))))
102 (funcall fun code text)))
103 (put-text-property pos0 end-marker 'invisible t)
104 (setq ansi-osc--marker (copy-marker pos0))))))))
91 105
92;; Window title handling (OSC 2) 106;; Window title handling (OSC 2)
93 107
diff --git a/test/lisp/ansi-osc-tests.el b/test/lisp/ansi-osc-tests.el
index d2fb130e518..d083626f3f9 100644
--- a/test/lisp/ansi-osc-tests.el
+++ b/test/lisp/ansi-osc-tests.el
@@ -30,8 +30,7 @@
30(require 'ert) 30(require 'ert)
31 31
32(defvar ansi-osc-tests--strings 32(defvar ansi-osc-tests--strings
33 `( 33 `(("Hello World" "Hello World")
34 ("Hello World" "Hello World")
35 34
36 ;; window title 35 ;; window title
37 ("Buffer \e]2;A window title\e\\content" "Buffer content") 36 ("Buffer \e]2;A window title\e\\content" "Buffer content")
@@ -44,6 +43,10 @@
44 43
45 ;; hyperlink 44 ;; hyperlink
46 ("\e]8;;http://example.com\e\\This is a link\e]8;;\e\\" "This is a link") 45 ("\e]8;;http://example.com\e\\This is a link\e]8;;\e\\" "This is a link")
46
47 ;; multiple sequences
48 ("Escape \e]2;A window title\e\\sequence followed by \e]2;unfinished sequence"
49 "Escape sequence followed by \e]2;unfinished sequence")
47 )) 50 ))
48;; Don't output those strings to stdout since they may have 51;; Don't output those strings to stdout since they may have
49;; side-effects on the environment 52;; side-effects on the environment
@@ -54,4 +57,44 @@
54 (with-temp-buffer 57 (with-temp-buffer
55 (insert input) 58 (insert input)
56 (ansi-osc-apply-on-region (point-min) (point-max)) 59 (ansi-osc-apply-on-region (point-min) (point-max))
57 (should (equal (buffer-string) text)))))) 60 (should (equal
61 (buffer-substring-no-properties
62 (point-min) (point-max))
63 text))))))
64
65(ert-deftest ansi-osc-tests-apply-region-no-handlers-multiple-calls ()
66 (let ((ansi-osc-handlers nil))
67 (with-temp-buffer
68 (insert
69 (concat "First set the window title \e]2;A window title\e\\"
70 "then change it\e]2;Another "))
71 (ansi-osc-apply-on-region (point-min) (point-max))
72 (let ((pos (point)))
73 (insert "title\e\\, and stop.")
74 (ansi-osc-apply-on-region pos (point-max)))
75 (should
76 (equal
77 (buffer-substring-no-properties (point-min) (point-max))
78 "First set the window title then change it, and stop.")))))
79
80(ert-deftest ansi-osc-tests-filter-region ()
81 (pcase-dolist (`(,input ,text) ansi-osc-tests--strings)
82 (with-temp-buffer
83 (insert input)
84 (ansi-osc-filter-region (point-min) (point-max))
85 (should (equal (buffer-string) text)))))
86
87
88(ert-deftest ansi-osc-tests-filter-region-with-multiple-calls ()
89 (with-temp-buffer
90 (insert
91 (concat "First set the window title \e]2;A window title\e\\"
92 "then change it\e]2;Another "))
93 (ansi-osc-filter-region (point-min) (point-max))
94 (let ((pos (point)))
95 (insert "title\e\\, and stop.")
96 (ansi-osc-filter-region pos (point-max)))
97 (should
98 (equal
99 (buffer-string)
100 "First set the window title then change it, and stop."))))