]> git.eshelyaron.com Git - emacs.git/commitdiff
ansi-osc.el: Use marker (bug#78184)
authorMatthias Meulien <orontee@gmail.com>
Thu, 8 May 2025 14:51:46 +0000 (16:51 +0200)
committerEshel Yaron <me@eshelyaron.com>
Sat, 7 Jun 2025 19:55:18 +0000 (21:55 +0200)
* 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
test/lisp/ansi-osc-tests.el

index ef08ccd44eeaa745b68dc5564394d4ee29dc5b69..e5026ff5490379ce59384fa5371bd33296c91e49 100644 (file)
 
 ;;; 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)
   "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)
 
index d2fb130e51888b727a2520c471d592338182059f..d083626f3f97b7190df05ceab87ebcef77afc6eb 100644 (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")
 
     ;; 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
       (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."))))