Skip to content

Commit e41d32a

Browse files
authored
fix: backslash in \n, \t, \r etc. no longer hidden in chat (#147)
markdown-mode hides the backslash in escape sequences like \n, \t, \r when markdown-hide-markup is enabled, because its regex matches backslash + any character. CommonMark only defines escapes for ASCII punctuation, so \n should display as \n, not just n. Override markdown-regex-escape buffer-locally in chat mode to match only valid CommonMark escape targets (§2.4).
1 parent 8608994 commit e41d32a

File tree

2 files changed

+82
-0
lines changed

2 files changed

+82
-0
lines changed

pi-coding-agent-ui.el

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,18 @@ Returns \"text\" for unrecognized extensions to ensure consistent fencing."
341341
(or (cdr (assoc ext pi-coding-agent--extension-language-alist))
342342
"text"))))
343343

344+
;;;; Markdown Escape Fix
345+
346+
(defconst pi-coding-agent--markdown-regex-escape
347+
"\\(\\\\\\)[]!\"#$%&'()*+,./:;<=>?@[\\\\^_`{|}~-]"
348+
"Restricted version of `markdown-regex-escape' for CommonMark §2.4.
349+
Markdown-mode's regex matches backslash + ANY character and hides
350+
the backslash when `markdown-hide-markup' is enabled. This turns
351+
\"\\n\" into just \"n\", \"\\t\" into just the letter, etc. CommonMark
352+
only defines escapes for ASCII punctuation, so we override the regex
353+
buffer-locally in `pi-coding-agent-chat-mode' to match only valid
354+
escape targets.")
355+
344356
;;;; Major Modes
345357

346358
(defvar pi-coding-agent-chat-mode-map
@@ -505,6 +517,9 @@ This is a read-only buffer showing the conversation history."
505517
;; Hide markdown markup (**, `, ```) for cleaner display
506518
(setq-local markdown-hide-markup t)
507519
(add-to-invisibility-spec 'markdown-markup)
520+
;; Restrict backslash escapes to CommonMark punctuation only.
521+
;; Without this, \n \t \r etc. lose their backslash in the display.
522+
(setq-local markdown-regex-escape pi-coding-agent--markdown-regex-escape)
508523
;; Strip hidden markup from copy operations (M-w, C-w)
509524
(setq-local filter-buffer-substring-function
510525
#'pi-coding-agent--filter-buffer-substring)

test/pi-coding-agent-render-test.el

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,73 @@ then proper highlighting once block is closed."
524524
(should (or (eq face 'font-lock-keyword-face)
525525
(and (listp face) (memq 'font-lock-keyword-face face))))))))
526526

527+
;;; Markdown Escape Restriction
528+
529+
(ert-deftest pi-coding-agent-test-backslash-n-visible-in-chat ()
530+
"Backslash before non-punctuation chars stays visible in chat.
531+
Regression test: markdown-mode hides backslash in \\n, \\t, etc.
532+
because `markdown-match-escape' matches backslash + any char.
533+
We override `markdown-regex-escape' buffer-locally to restrict
534+
matching to CommonMark-valid escapes only."
535+
(with-temp-buffer
536+
(pi-coding-agent-chat-mode)
537+
(let ((inhibit-read-only t))
538+
(insert "Use \\n for a newline\n")
539+
(font-lock-ensure)
540+
(goto-char (point-min))
541+
(search-forward "\\" nil t)
542+
(let ((inv (get-text-property (1- (point)) 'invisible)))
543+
(should-not inv)))))
544+
545+
(ert-deftest pi-coding-agent-test-backslash-t-visible-in-chat ()
546+
"Backslash before t stays visible in chat."
547+
(with-temp-buffer
548+
(pi-coding-agent-chat-mode)
549+
(let ((inhibit-read-only t))
550+
(insert "Use \\t for a tab\n")
551+
(font-lock-ensure)
552+
(goto-char (point-min))
553+
(search-forward "\\" nil t)
554+
(let ((inv (get-text-property (1- (point)) 'invisible)))
555+
(should-not inv)))))
556+
557+
(ert-deftest pi-coding-agent-test-backslash-star-hidden-in-chat ()
558+
"Backslash before * (valid markdown escape) IS hidden.
559+
Ensures the restricted regex preserves intended escape behavior.
560+
Requires preceding text so gfm-mode doesn't classify content as
561+
YAML metadata (which would skip escape matching entirely)."
562+
(with-temp-buffer
563+
(pi-coding-agent-chat-mode)
564+
(let ((inhibit-read-only t))
565+
(insert "Some preceding text\n\nEscaped: \\* not bold\n")
566+
(font-lock-ensure)
567+
(goto-char (point-min))
568+
(search-forward "\\" nil t)
569+
(let ((inv (get-text-property (1- (point)) 'invisible)))
570+
(should (eq inv 'markdown-markup))))))
571+
572+
(ert-deftest pi-coding-agent-test-backslash-in-code-block-unaffected ()
573+
"Backslash in fenced code block is never hidden (existing behavior)."
574+
(with-temp-buffer
575+
(pi-coding-agent-chat-mode)
576+
(let ((inhibit-read-only t))
577+
(insert "```\nprint(\"hello\\nworld\")\n```\n")
578+
(font-lock-ensure)
579+
(goto-char (point-min))
580+
(search-forward "\\" nil t)
581+
(let ((inv (get-text-property (1- (point)) 'invisible)))
582+
(should-not inv)))))
583+
584+
(ert-deftest pi-coding-agent-test-escape-regex-is-buffer-local ()
585+
"Chat mode sets `markdown-regex-escape' buffer-locally.
586+
Verifies the fix is scoped to our buffer and does not affect
587+
other markdown-mode buffers."
588+
(with-temp-buffer
589+
(pi-coding-agent-chat-mode)
590+
(should (local-variable-p 'markdown-regex-escape))
591+
(should (equal markdown-regex-escape
592+
pi-coding-agent--markdown-regex-escape))))
593+
527594
;;; User Message Display
528595

529596
(ert-deftest pi-coding-agent-test-display-user-message-inserts-text ()

0 commit comments

Comments
 (0)