From 45de01d6c2c234e73582b19e3a0519eac418a547 Mon Sep 17 00:00:00 2001 From: Matthias Meulien Date: Thu, 8 May 2025 16:51:46 +0200 Subject: [PATCH] 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. (cherry picked from commit 6f8cee03316e166e4204ba49fbb9964a075968ca) --- lisp/ansi-osc.el | 88 +++++++++++++++++++++---------------- 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 ef08ccd44ee..e5026ff5490 100644 --- a/lisp/ansi-osc.el +++ b/lisp/ansi-osc.el @@ -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) 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 @@ (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.")))) -- 2.39.5