Skip to content

Commit 5591a8d

Browse files
authored
Display tables with box-drawing characters (#173)
Tables in the chat buffer now render with Unicode box-drawing characters instead of raw markdown pipes and dashes. This gives tables cleaner visual structure without altering the underlying buffer text, which stays canonical markdown so that copy, search, and session history work exactly as before. Table overlays also now carry an explicit face that prevents tree-sitter buffer faces from bleeding through into the display. To restore the old appearance, set pi-coding-agent-prettify-tables to nil.
1 parent b2709c5 commit 5591a8d

3 files changed

Lines changed: 141 additions & 35 deletions

File tree

pi-coding-agent-table.el

Lines changed: 53 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -246,41 +246,52 @@ Returns (PREFIX . BARE), where PREFIX is everything before the first `|'."
246246
(md-ts--set-hide-markup t)))
247247
pi-coding-agent--visible-string-buffer)
248248

249+
(defconst pi-coding-agent--markdown-inline-chars-re "[*_`~\\[]"
250+
"Regexp matching characters that can start markdown inline syntax.
251+
Cells without any of these characters have identical visible rendering
252+
and can skip the fontification buffer entirely.")
253+
249254
(defun pi-coding-agent--markdown-visible-string (markdown)
250255
"Return the visible chat-buffer rendering of MARKDOWN.
251256
This hides markdown delimiters the same way `pi-coding-agent-chat-mode'
252257
does, so wrapped table overlays match ordinary visible-copy semantics.
253-
Uses a persistent fontification buffer to avoid per-call mode setup."
254-
(with-current-buffer (pi-coding-agent--visible-string-buffer)
255-
(let ((inhibit-read-only t))
256-
(erase-buffer)
257-
(insert markdown))
258-
(font-lock-ensure)
259-
(pi-coding-agent--visible-text (point-min) (point-max))))
258+
Uses a persistent fontification buffer to avoid per-call mode setup.
259+
Cells with no inline syntax characters are returned as-is."
260+
(if (not (string-match-p pi-coding-agent--markdown-inline-chars-re markdown))
261+
markdown
262+
(with-current-buffer (pi-coding-agent--visible-string-buffer)
263+
(let ((inhibit-read-only t))
264+
(erase-buffer)
265+
(insert markdown))
266+
(font-lock-ensure)
267+
(pi-coding-agent--visible-text (point-min) (point-max)))))
260268

261269
;;;; Cell Rendering
262270

263271
(defun pi-coding-agent--render-table-row-lines (cells col-widths aligns)
264272
"Render table CELLS into display lines using COL-WIDTHS and ALIGNS.
265-
Builds each line with a flat string accumulator to reduce intermediate
266-
consing: padding and cell fragments are pushed individually, then
267-
joined with a single `apply concat'."
273+
When `pi-coding-agent-prettify-tables' is non-nil, emits Unicode
274+
box-drawing verticals instead of markdown pipes."
268275
(let* ((num-cols (length col-widths))
269276
(padded (append cells (make-list (max 0 (- num-cols (length cells))) "")))
270277
(wrapped-cells
271278
(cl-mapcar (lambda (cell column-width)
272279
(markdown-table-wrap-cell (or cell "") column-width))
273280
padded col-widths))
274-
(max-height (apply #'max (mapcar #'length wrapped-cells))))
281+
(max-height (apply #'max (mapcar #'length wrapped-cells)))
282+
(pretty pi-coding-agent-prettify-tables)
283+
(delim-open (if pretty "" "| "))
284+
(delim-mid (if pretty "" " | "))
285+
(delim-close (if pretty "" " |")))
275286
(cl-loop for line-index below max-height
276287
collect
277-
(let ((acc (list "| ")))
288+
(let ((acc (list delim-open)))
278289
(cl-loop for cell-lines in wrapped-cells
279290
for column-width in col-widths
280291
for align in aligns
281292
for first = t then nil
282293
do
283-
(unless first (push " | " acc))
294+
(unless first (push delim-mid acc))
284295
(let* ((cell (or (nth line-index cell-lines) ""))
285296
(empty (string-empty-p cell))
286297
(pad (if empty
@@ -301,31 +312,37 @@ joined with a single `apply concat'."
301312
(t
302313
(push cell acc)
303314
(push (make-string pad ?\s) acc)))))
304-
(push " |" acc)
315+
(push delim-close acc)
305316
(apply #'concat (nreverse acc))))))
306317

307318
(defun pi-coding-agent--render-table-separator-line (col-widths aligns)
308-
"Render the markdown separator line for COL-WIDTHS and ALIGNS."
309-
(let ((parts
310-
(cl-mapcar
311-
(lambda (column-width align)
312-
(let ((dashes (make-string (max 1 column-width) ?-)))
313-
(pcase align
314-
('left
315-
(if (>= column-width 2)
316-
(concat ":" (substring dashes 1))
317-
":"))
318-
('right
319-
(if (>= column-width 2)
320-
(concat (substring dashes 1) ":")
321-
":"))
322-
('center
323-
(if (>= column-width 3)
324-
(concat ":" (substring dashes 2) ":")
325-
(if (>= column-width 2) "::" ":")))
326-
(_ dashes))))
327-
col-widths aligns)))
328-
(concat "| " (mapconcat #'identity parts " | ") " |")))
319+
"Render the separator line for COL-WIDTHS and ALIGNS.
320+
When `pi-coding-agent-prettify-tables' is non-nil, emits a box-drawing
321+
rule (├─┼─┤) directly; otherwise emits standard markdown syntax."
322+
(if pi-coding-agent-prettify-tables
323+
(concat "├─" (mapconcat (lambda (w) (make-string (max 1 w) ?─))
324+
col-widths "─┼─")
325+
"─┤")
326+
(let ((parts
327+
(cl-mapcar
328+
(lambda (column-width align)
329+
(let ((dashes (make-string (max 1 column-width) ?-)))
330+
(pcase align
331+
('left
332+
(if (>= column-width 2)
333+
(concat ":" (substring dashes 1))
334+
":"))
335+
('right
336+
(if (>= column-width 2)
337+
(concat (substring dashes 1) ":")
338+
":"))
339+
('center
340+
(if (>= column-width 3)
341+
(concat ":" (substring dashes 2) ":")
342+
(if (>= column-width 2) "::" ":")))
343+
(_ dashes))))
344+
col-widths aligns)))
345+
(concat "| " (mapconcat #'identity parts " | ") " |"))))
329346

330347
(defun pi-coding-agent--table-alignments (separator-line)
331348
"Return column alignment symbols parsed from SEPARATOR-LINE."
@@ -560,6 +577,7 @@ blank-line spacing between a table and following text is not collapsed."
560577
(ov (make-overlay line-beg line-end nil nil nil)))
561578
(overlay-put ov 'display
562579
(pi-coding-agent--neutralize-fonts display-str))
580+
(overlay-put ov 'face 'default)
563581
(overlay-put ov 'pi-coding-agent-table-display t)
564582
(overlay-put ov 'evaporate t))
565583
(forward-line 1))))

pi-coding-agent-ui.el

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,15 @@ inside that suffix; older history stays frozen until explicitly rebuilt."
174174
:type 'natnum
175175
:group 'pi-coding-agent)
176176

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

179188
(defface pi-coding-agent-timestamp

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

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,76 @@ so visible text still needs consistent alignment across all display lines."
379379
(let ((widths (mapcar #'string-width (nreverse all-lines))))
380380
(should (= (length (delete-dups (copy-sequence widths))) 1))))))
381381

382+
(ert-deftest pi-coding-agent-test-decorate-table-prettifies-visible-separators ()
383+
"Rendered table display uses box-drawing separators instead of raw pipes."
384+
(with-temp-buffer
385+
(pi-coding-agent-chat-mode)
386+
(let ((inhibit-read-only t))
387+
(insert "| Name | Value |\n|------|-------|\n| Alpha | Beta |\n"))
388+
(font-lock-ensure)
389+
(pi-coding-agent--decorate-tables-in-region (point-min) (point-max) 40)
390+
(let ((all-display (mapconcat #'identity
391+
(pi-coding-agent-test--table-overlay-displays-in-region
392+
(point-min) (point-max))
393+
"\n")))
394+
;; Rows use box-drawing verticals, not raw markdown pipes
395+
(should (string-match-p "^│ " all-display))
396+
(should-not (string-match-p "^| " all-display))
397+
;; Separator uses box-drawing horizontals
398+
(should (string-match-p "├.*┼.*┤" all-display))
399+
;; Table content preserved
400+
(should (string-match-p "Name" all-display))
401+
(should (string-match-p "Alpha" all-display)))))
402+
403+
(ert-deftest pi-coding-agent-test-decorate-table-preserves-pipes-when-prettify-off ()
404+
"With prettify disabled, rendered tables use standard markdown pipes."
405+
(with-temp-buffer
406+
(pi-coding-agent-chat-mode)
407+
(let ((pi-coding-agent-prettify-tables nil)
408+
(inhibit-read-only t))
409+
(insert "| Name | Value |\n|------|-------|\n| Alpha | Beta |\n")
410+
(font-lock-ensure)
411+
(pi-coding-agent--decorate-tables-in-region (point-min) (point-max) 40)
412+
(let ((all-display (mapconcat #'identity
413+
(pi-coding-agent-test--table-overlay-displays-in-region
414+
(point-min) (point-max))
415+
"\n")))
416+
;; Standard markdown pipe delimiters
417+
(should (string-match-p "^| " all-display))
418+
;; No box-drawing characters
419+
(should-not (string-match-p "" all-display))
420+
(should-not (string-match-p "" all-display))
421+
;; Separator uses dashes
422+
(should (string-match-p "---" all-display))
423+
;; Table content preserved
424+
(should (string-match-p "Name" all-display))
425+
(should (string-match-p "Alpha" all-display))))))
426+
427+
(ert-deftest pi-coding-agent-test-table-overlay-suppresses-buffer-face ()
428+
"Table overlays suppress tree-sitter buffer faces without losing inline formatting.
429+
Tree-sitter applies `md-ts-delimiter' (shadow) to separator rows and `bold'
430+
to headers. The overlay must suppress these while preserving inline markdown
431+
formatting (bold, italic) in the display string's text properties."
432+
(with-temp-buffer
433+
(pi-coding-agent-chat-mode)
434+
(let ((inhibit-read-only t))
435+
(insert "| Name | Note |\n|------|------|\n| **bold** | text |\n"))
436+
(font-lock-ensure)
437+
(pi-coding-agent--decorate-tables-in-region (point-min) (point-max) 40)
438+
(let ((ovs (sort (seq-filter
439+
(lambda (ov) (overlay-get ov 'pi-coding-agent-table-display))
440+
(overlays-in (point-min) (point-max)))
441+
(lambda (a b) (< (overlay-start a) (overlay-start b))))))
442+
;; Every overlay has an explicit face to block buffer face bleed-through
443+
(should (cl-every (lambda (ov) (overlay-get ov 'face)) ovs))
444+
;; Separator overlay does not inherit shadow
445+
(let ((sep-face (overlay-get (nth 1 ovs) 'face)))
446+
(should-not (eq sep-face 'md-ts-delimiter))
447+
(should-not (eq sep-face 'shadow)))
448+
;; Inline bold from **bold** survives in the data row display string
449+
(should (pi-coding-agent-test--string-has-face-attr-p
450+
(overlay-get (nth 2 ovs) 'display) :weight 'bold)))))
451+
382452
(ert-deftest pi-coding-agent-test-decorate-table-hides-inline-markup-in-display ()
383453
"Wrapped table display hides markdown delimiters like the chat buffer does."
384454
(with-temp-buffer
@@ -515,6 +585,15 @@ what the parser recognizes as a `pipe_table'."
515585
1))
516586
(should (= (pi-coding-agent-test--table-overlay-count) 0))))
517587

588+
(defun pi-coding-agent-test--string-has-face-attr-p (str attr value)
589+
"Return non-nil if STR has face ATTR equal to VALUE at any position."
590+
(let ((pos 0)
591+
(len (length str)))
592+
(cl-loop while (< pos len)
593+
for face = (get-text-property pos 'face str)
594+
thereis (and (consp face) (eq (plist-get face attr) value))
595+
do (setq pos (next-single-property-change pos 'face str len)))))
596+
518597
(defun pi-coding-agent-test--table-overlay-count ()
519598
"Count table display overlays in the current buffer."
520599
(length (seq-filter

0 commit comments

Comments
 (0)