;;; 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
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)
(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."))))