diff options
| author | Matthias Meulien | 2025-05-08 16:51:46 +0200 |
|---|---|---|
| committer | Eli Zaretskii | 2025-05-31 15:21:21 +0300 |
| commit | 6f8cee03316e166e4204ba49fbb9964a075968ca (patch) | |
| tree | 189b3e11f5281d9763376f30c0bd2453bdca1dd8 | |
| parent | 32d911cddfd6ccdf288247a5d7eaf3d9a977b87c (diff) | |
| download | emacs-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.el | 88 | ||||
| -rw-r--r-- | test/lisp/ansi-osc-tests.el | 49 |
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. | ||
| 44 | Used by `ansi-osc-filter-region' and `ansi-osc-apply-on-region' to store | ||
| 45 | position of an unfinished escape sequence, for the complete sequence to | ||
| 46 | be 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 | 50 | When an unfinished escape sequence is found, the start position is saved |
| 46 | (goto-char begin) | 51 | to `ansi-osc--marker'. Later call will override BEGIN with the position |
| 47 | ;; Delete escape sequences. | 52 | pointed 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. |
| 55 | See `ansi-osc-apply-on-region' for details.") | 69 | See `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. |
| 63 | This function searches for escape sequences of the forms | 73 | This 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 | ||
| 68 | Every occurrence of such escape sequences is removed from the | 78 | Every occurrence of such escape sequences is removed from the buffer. |
| 69 | buffer. Then, if `command' is a key in the alist that is the | 79 | Then, if `command' is a key in the alist that is the value of the local |
| 70 | value of the local variable `ansi-osc-handlers', that key's | 80 | variable `ansi-osc-handlers', that key's value, which should be a |
| 71 | value, which should be a function, is called with `command' and | 81 | function, is called with `command' and `text' as arguments, with point |
| 72 | `text' as arguments, with point where the escape sequence was | 82 | where the escape sequence was located. When an unfinished escape |
| 73 | located." | 83 | sequence 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)) | 85 | pointed 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.")))) | ||