Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions pi-coding-agent-ui.el
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,18 @@ Returns \"text\" for unrecognized extensions to ensure consistent fencing."
(or (cdr (assoc ext pi-coding-agent--extension-language-alist))
"text"))))

;;;; Markdown Escape Fix

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

;;;; Major Modes

(defvar pi-coding-agent-chat-mode-map
Expand Down Expand Up @@ -505,6 +517,9 @@ This is a read-only buffer showing the conversation history."
;; Hide markdown markup (**, `, ```) for cleaner display
(setq-local markdown-hide-markup t)
(add-to-invisibility-spec 'markdown-markup)
;; Restrict backslash escapes to CommonMark punctuation only.
;; Without this, \n \t \r etc. lose their backslash in the display.
(setq-local markdown-regex-escape pi-coding-agent--markdown-regex-escape)
;; Strip hidden markup from copy operations (M-w, C-w)
(setq-local filter-buffer-substring-function
#'pi-coding-agent--filter-buffer-substring)
Expand Down
67 changes: 67 additions & 0 deletions test/pi-coding-agent-render-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -524,6 +524,73 @@ then proper highlighting once block is closed."
(should (or (eq face 'font-lock-keyword-face)
(and (listp face) (memq 'font-lock-keyword-face face))))))))

;;; Markdown Escape Restriction

(ert-deftest pi-coding-agent-test-backslash-n-visible-in-chat ()
"Backslash before non-punctuation chars stays visible in chat.
Regression test: markdown-mode hides backslash in \\n, \\t, etc.
because `markdown-match-escape' matches backslash + any char.
We override `markdown-regex-escape' buffer-locally to restrict
matching to CommonMark-valid escapes only."
(with-temp-buffer
(pi-coding-agent-chat-mode)
(let ((inhibit-read-only t))
(insert "Use \\n for a newline\n")
(font-lock-ensure)
(goto-char (point-min))
(search-forward "\\" nil t)
(let ((inv (get-text-property (1- (point)) 'invisible)))
(should-not inv)))))

(ert-deftest pi-coding-agent-test-backslash-t-visible-in-chat ()
"Backslash before t stays visible in chat."
(with-temp-buffer
(pi-coding-agent-chat-mode)
(let ((inhibit-read-only t))
(insert "Use \\t for a tab\n")
(font-lock-ensure)
(goto-char (point-min))
(search-forward "\\" nil t)
(let ((inv (get-text-property (1- (point)) 'invisible)))
(should-not inv)))))

(ert-deftest pi-coding-agent-test-backslash-star-hidden-in-chat ()
"Backslash before * (valid markdown escape) IS hidden.
Ensures the restricted regex preserves intended escape behavior.
Requires preceding text so gfm-mode doesn't classify content as
YAML metadata (which would skip escape matching entirely)."
(with-temp-buffer
(pi-coding-agent-chat-mode)
(let ((inhibit-read-only t))
(insert "Some preceding text\n\nEscaped: \\* not bold\n")
(font-lock-ensure)
(goto-char (point-min))
(search-forward "\\" nil t)
(let ((inv (get-text-property (1- (point)) 'invisible)))
(should (eq inv 'markdown-markup))))))

(ert-deftest pi-coding-agent-test-backslash-in-code-block-unaffected ()
"Backslash in fenced code block is never hidden (existing behavior)."
(with-temp-buffer
(pi-coding-agent-chat-mode)
(let ((inhibit-read-only t))
(insert "```\nprint(\"hello\\nworld\")\n```\n")
(font-lock-ensure)
(goto-char (point-min))
(search-forward "\\" nil t)
(let ((inv (get-text-property (1- (point)) 'invisible)))
(should-not inv)))))

(ert-deftest pi-coding-agent-test-escape-regex-is-buffer-local ()
"Chat mode sets `markdown-regex-escape' buffer-locally.
Verifies the fix is scoped to our buffer and does not affect
other markdown-mode buffers."
(with-temp-buffer
(pi-coding-agent-chat-mode)
(should (local-variable-p 'markdown-regex-escape))
(should (equal markdown-regex-escape
pi-coding-agent--markdown-regex-escape))))

;;; User Message Display

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