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
88 changes: 53 additions & 35 deletions pi-coding-agent-table.el
Original file line number Diff line number Diff line change
Expand Up @@ -246,41 +246,52 @@ Returns (PREFIX . BARE), where PREFIX is everything before the first `|'."
(md-ts--set-hide-markup t)))
pi-coding-agent--visible-string-buffer)

(defconst pi-coding-agent--markdown-inline-chars-re "[*_`~\\[]"
"Regexp matching characters that can start markdown inline syntax.
Cells without any of these characters have identical visible rendering
and can skip the fontification buffer entirely.")

(defun pi-coding-agent--markdown-visible-string (markdown)
"Return the visible chat-buffer rendering of MARKDOWN.
This hides markdown delimiters the same way `pi-coding-agent-chat-mode'
does, so wrapped table overlays match ordinary visible-copy semantics.
Uses a persistent fontification buffer to avoid per-call mode setup."
(with-current-buffer (pi-coding-agent--visible-string-buffer)
(let ((inhibit-read-only t))
(erase-buffer)
(insert markdown))
(font-lock-ensure)
(pi-coding-agent--visible-text (point-min) (point-max))))
Uses a persistent fontification buffer to avoid per-call mode setup.
Cells with no inline syntax characters are returned as-is."
(if (not (string-match-p pi-coding-agent--markdown-inline-chars-re markdown))
markdown
(with-current-buffer (pi-coding-agent--visible-string-buffer)
(let ((inhibit-read-only t))
(erase-buffer)
(insert markdown))
(font-lock-ensure)
(pi-coding-agent--visible-text (point-min) (point-max)))))

;;;; Cell Rendering

(defun pi-coding-agent--render-table-row-lines (cells col-widths aligns)
"Render table CELLS into display lines using COL-WIDTHS and ALIGNS.
Builds each line with a flat string accumulator to reduce intermediate
consing: padding and cell fragments are pushed individually, then
joined with a single `apply concat'."
When `pi-coding-agent-prettify-tables' is non-nil, emits Unicode
box-drawing verticals instead of markdown pipes."
(let* ((num-cols (length col-widths))
(padded (append cells (make-list (max 0 (- num-cols (length cells))) "")))
(wrapped-cells
(cl-mapcar (lambda (cell column-width)
(markdown-table-wrap-cell (or cell "") column-width))
padded col-widths))
(max-height (apply #'max (mapcar #'length wrapped-cells))))
(max-height (apply #'max (mapcar #'length wrapped-cells)))
(pretty pi-coding-agent-prettify-tables)
(delim-open (if pretty "│ " "| "))
(delim-mid (if pretty " │ " " | "))
(delim-close (if pretty " │" " |")))
(cl-loop for line-index below max-height
collect
(let ((acc (list "| ")))
(let ((acc (list delim-open)))
(cl-loop for cell-lines in wrapped-cells
for column-width in col-widths
for align in aligns
for first = t then nil
do
(unless first (push " | " acc))
(unless first (push delim-mid acc))
(let* ((cell (or (nth line-index cell-lines) ""))
(empty (string-empty-p cell))
(pad (if empty
Expand All @@ -301,31 +312,37 @@ joined with a single `apply concat'."
(t
(push cell acc)
(push (make-string pad ?\s) acc)))))
(push " |" acc)
(push delim-close acc)
(apply #'concat (nreverse acc))))))

(defun pi-coding-agent--render-table-separator-line (col-widths aligns)
"Render the markdown separator line for COL-WIDTHS and ALIGNS."
(let ((parts
(cl-mapcar
(lambda (column-width align)
(let ((dashes (make-string (max 1 column-width) ?-)))
(pcase align
('left
(if (>= column-width 2)
(concat ":" (substring dashes 1))
":"))
('right
(if (>= column-width 2)
(concat (substring dashes 1) ":")
":"))
('center
(if (>= column-width 3)
(concat ":" (substring dashes 2) ":")
(if (>= column-width 2) "::" ":")))
(_ dashes))))
col-widths aligns)))
(concat "| " (mapconcat #'identity parts " | ") " |")))
"Render the separator line for COL-WIDTHS and ALIGNS.
When `pi-coding-agent-prettify-tables' is non-nil, emits a box-drawing
rule (├─┼─┤) directly; otherwise emits standard markdown syntax."
(if pi-coding-agent-prettify-tables
(concat "├─" (mapconcat (lambda (w) (make-string (max 1 w) ?─))
col-widths "─┼─")
"─┤")
(let ((parts
(cl-mapcar
(lambda (column-width align)
(let ((dashes (make-string (max 1 column-width) ?-)))
(pcase align
('left
(if (>= column-width 2)
(concat ":" (substring dashes 1))
":"))
('right
(if (>= column-width 2)
(concat (substring dashes 1) ":")
":"))
('center
(if (>= column-width 3)
(concat ":" (substring dashes 2) ":")
(if (>= column-width 2) "::" ":")))
(_ dashes))))
col-widths aligns)))
(concat "| " (mapconcat #'identity parts " | ") " |"))))

(defun pi-coding-agent--table-alignments (separator-line)
"Return column alignment symbols parsed from SEPARATOR-LINE."
Expand Down Expand Up @@ -560,6 +577,7 @@ blank-line spacing between a table and following text is not collapsed."
(ov (make-overlay line-beg line-end nil nil nil)))
(overlay-put ov 'display
(pi-coding-agent--neutralize-fonts display-str))
(overlay-put ov 'face 'default)
(overlay-put ov 'pi-coding-agent-table-display t)
(overlay-put ov 'evaporate t))
(forward-line 1))))
Expand Down
9 changes: 9 additions & 0 deletions pi-coding-agent-ui.el
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,15 @@ inside that suffix; older history stays frozen until explicitly rebuilt."
:type 'natnum
:group 'pi-coding-agent)

(defcustom pi-coding-agent-prettify-tables t
"Whether display-only markdown tables use prettier visible separators.
When non-nil, table overlays replace raw markdown pipes and separator rows
with Unicode box-drawing characters in the visible display. The underlying
buffer text stays canonical markdown, so copy, search, and session history
still operate on the raw table source."
:type 'boolean
:group 'pi-coding-agent)

;;;; Faces

(defface pi-coding-agent-timestamp
Expand Down
79 changes: 79 additions & 0 deletions test/pi-coding-agent-table-test.el
Original file line number Diff line number Diff line change
Expand Up @@ -379,6 +379,76 @@ so visible text still needs consistent alignment across all display lines."
(let ((widths (mapcar #'string-width (nreverse all-lines))))
(should (= (length (delete-dups (copy-sequence widths))) 1))))))

(ert-deftest pi-coding-agent-test-decorate-table-prettifies-visible-separators ()
"Rendered table display uses box-drawing separators instead of raw pipes."
(with-temp-buffer
(pi-coding-agent-chat-mode)
(let ((inhibit-read-only t))
(insert "| Name | Value |\n|------|-------|\n| Alpha | Beta |\n"))
(font-lock-ensure)
(pi-coding-agent--decorate-tables-in-region (point-min) (point-max) 40)
(let ((all-display (mapconcat #'identity
(pi-coding-agent-test--table-overlay-displays-in-region
(point-min) (point-max))
"\n")))
;; Rows use box-drawing verticals, not raw markdown pipes
(should (string-match-p "^│ " all-display))
(should-not (string-match-p "^| " all-display))
;; Separator uses box-drawing horizontals
(should (string-match-p "├.*┼.*┤" all-display))
;; Table content preserved
(should (string-match-p "Name" all-display))
(should (string-match-p "Alpha" all-display)))))

(ert-deftest pi-coding-agent-test-decorate-table-preserves-pipes-when-prettify-off ()
"With prettify disabled, rendered tables use standard markdown pipes."
(with-temp-buffer
(pi-coding-agent-chat-mode)
(let ((pi-coding-agent-prettify-tables nil)
(inhibit-read-only t))
(insert "| Name | Value |\n|------|-------|\n| Alpha | Beta |\n")
(font-lock-ensure)
(pi-coding-agent--decorate-tables-in-region (point-min) (point-max) 40)
(let ((all-display (mapconcat #'identity
(pi-coding-agent-test--table-overlay-displays-in-region
(point-min) (point-max))
"\n")))
;; Standard markdown pipe delimiters
(should (string-match-p "^| " all-display))
;; No box-drawing characters
(should-not (string-match-p "│" all-display))
(should-not (string-match-p "├" all-display))
;; Separator uses dashes
(should (string-match-p "---" all-display))
;; Table content preserved
(should (string-match-p "Name" all-display))
(should (string-match-p "Alpha" all-display))))))

(ert-deftest pi-coding-agent-test-table-overlay-suppresses-buffer-face ()
"Table overlays suppress tree-sitter buffer faces without losing inline formatting.
Tree-sitter applies `md-ts-delimiter' (shadow) to separator rows and `bold'
to headers. The overlay must suppress these while preserving inline markdown
formatting (bold, italic) in the display string's text properties."
(with-temp-buffer
(pi-coding-agent-chat-mode)
(let ((inhibit-read-only t))
(insert "| Name | Note |\n|------|------|\n| **bold** | text |\n"))
(font-lock-ensure)
(pi-coding-agent--decorate-tables-in-region (point-min) (point-max) 40)
(let ((ovs (sort (seq-filter
(lambda (ov) (overlay-get ov 'pi-coding-agent-table-display))
(overlays-in (point-min) (point-max)))
(lambda (a b) (< (overlay-start a) (overlay-start b))))))
;; Every overlay has an explicit face to block buffer face bleed-through
(should (cl-every (lambda (ov) (overlay-get ov 'face)) ovs))
;; Separator overlay does not inherit shadow
(let ((sep-face (overlay-get (nth 1 ovs) 'face)))
(should-not (eq sep-face 'md-ts-delimiter))
(should-not (eq sep-face 'shadow)))
;; Inline bold from **bold** survives in the data row display string
(should (pi-coding-agent-test--string-has-face-attr-p
(overlay-get (nth 2 ovs) 'display) :weight 'bold)))))

(ert-deftest pi-coding-agent-test-decorate-table-hides-inline-markup-in-display ()
"Wrapped table display hides markdown delimiters like the chat buffer does."
(with-temp-buffer
Expand Down Expand Up @@ -515,6 +585,15 @@ what the parser recognizes as a `pipe_table'."
1))
(should (= (pi-coding-agent-test--table-overlay-count) 0))))

(defun pi-coding-agent-test--string-has-face-attr-p (str attr value)
"Return non-nil if STR has face ATTR equal to VALUE at any position."
(let ((pos 0)
(len (length str)))
(cl-loop while (< pos len)
for face = (get-text-property pos 'face str)
thereis (and (consp face) (eq (plist-get face attr) value))
do (setq pos (next-single-property-change pos 'face str len)))))

(defun pi-coding-agent-test--table-overlay-count ()
"Count table display overlays in the current buffer."
(length (seq-filter
Expand Down
Loading