Subject: bug#71325: 30.0.50; multiline entries in tabulated-lists are left-aligned

From: Andrey Listopadov <[email-masked]>
Date: Sun, 02 Jun 2024 16:12:31 +0300

Hello! I was working with tabulated lists, and noticed, that multiline
entries are not properly inserted, when the column is not the first
one. E.g. this multiline entry:

tabulated-list-format:
[("1" 3 t) ("2" 10 t) ("3" 4 t)]

tabulated-list-entries:
((nil ["foo" "bar
baz
qux" "quux"]))

Is shown like this:

1   2          3
foo bar
baz
qux            quux

moreover, I can't set the second column's width to 3, as with the rest
ones, because it will truncate the string not accounting to multiline
widths:

1   2    3
foo bar… quux

What I'd like to see instead is:

1   2   3
foo bar
    baz
    qux quux

I've made a quick prototype, changing the tabulated-list-print-col
function to handle multiline strings:

>From 583367791aa6f2f87984331f370be01dcba31971 Mon Sep 17 00:00:00 2001
From: Andrey Listopadov <[email-masked]>
Date: Sun, 2 Jun 2024 15:54:52 +0300
Subject: [PATCH] preserve column offset when printing multiline strings

---
 lisp/emacs-lisp/tabulated-list.el | 30 +++++++++++++++++++++++++-----
 1 file changed, 25 insertions(+), 5 deletions(-)

diff --git a/lisp/emacs-lisp/tabulated-list.el b/lisp/emacs-lisp/tabulated-list.el
index c86e3f9c5df..3e24c823f9e 100644
--- a/lisp/emacs-lisp/tabulated-list.el
+++ b/lisp/emacs-lisp/tabulated-list.el
@@ -608,8 +608,16 @@ tabulated-list-print-col
     ;; and has some space left, truncate to `available-space' instead.
     (when (and not-last-col
 	       (> label-width available-space))
-      (setq label (truncate-string-to-width
-		   label available-space nil nil t t)
+      (setq label
+            (if (string-match-p "\n" label)
+                (string-join
+                 (mapcar (lambda (row)
+                           (truncate-string-to-width
+	                    row available-space nil nil t t))
+                         (string-split label "\n"))
+                 "\n")
+              (truncate-string-to-width
+	       label available-space nil nil t t))
 	    label-width available-space))
     (setq label (bidi-string-mark-left-to-right label))
     (when (and right-align (> width label-width))
@@ -619,9 +627,21 @@ tabulated-list-print-col
         (setq width (- width shift))
         (setq x (+ x shift))))
     (cond ((stringp col-desc)
-           (insert (if (get-text-property 0 'help-echo label)
-                       label
-                     (propertize label 'help-echo help-echo))))
+           (let ((label (if (string-match-p "\n" label)
+                            (string-replace
+                             "\n"
+                             (concat
+                              "\n"
+                              (make-string (save-mark-and-excursion
+                                             (let ((pos (point)))
+                                               (beginning-of-line)
+                                               (- pos (point))))
+                                           (string-to-char " ")))
+                             label)
+                          label)))
+             (insert (if (get-text-property 0 'help-echo label)
+                         label
+                       (propertize label 'help-echo help-echo)))))
           ((eq (car col-desc) 'image)
            (insert (propertize " "
                                'display col-desc
--
2.44.0

It handles truncation on per-line basis, and pads the string in a
probably wrong way (I don't know how it should work with right padding
and all)..

There's probably a better way of doing it though, and if possible I'd
like to preserver columns following that on the same line as other
columns. I.e. instead of

1   2   3
foo bar
    baz
    qux quux

I'd like to see

1   2   3
foo bar quux
    baz
    qux

The patch above is merely a POC, mayve someone with better elisp skills
and knowledge of tabulated-list-mode would be able to tackle this problem?

In GNU Emacs 30.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version
 3.24.41, cairo version 1.17.8) of 2024-05-12 built on toolbox
Repository revision: dcb6a9ebcbf92a4ca7c0e2c393d7a8dd9b1d351e
Repository branch: master
System Description: Fedora Linux 38 (Container Image)

Configured using:
 'configure --without-compress-install --with-native-compilation=yes
 --with-pgtk --with-mailutils --with-xwidgets
 --prefix=/var/home/alist/.local'

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GPM GSETTINGS HARFBUZZ JPEG
LCMS2 LIBOTF LIBSELINUX LIBXML2 MODULES NATIVE_COMP NOTIFY INOTIFY
PDUMPER PGTK PNG RSVG SECCOMP SOUND SQLITE3 THREADS TIFF
TOOLKIT_SCROLL_BARS TREE_SITTER XIM XWIDGETS GTK3 ZLIB

Important settings:
  value of $LANG: en_US.UTF-8
  locale-coding-system: utf-8-unix

Major mode: ELisp/d

Minor modes in effect:
  bug-reference-prog-mode: t
  csv-field-index-mode: t
  global-git-commit-mode: t
  magit-auto-revert-mode: t
  outline-minor-mode: t
  electric-pair-mode: t
  isayt-mode: t
  savehist-mode: t
  delete-selection-mode: t
  pixel-scroll-precision-mode: t
  global-auto-revert-mode: t
  repeat-mode: t
  vertico-mode: t
  marginalia-mode: t
  corfu-popupinfo-mode: t
  global-corfu-mode: t
  corfu-mode: t
  global-region-bindings-mode: t
  recentf-mode: t
  server-mode: t
  common-lisp-modes-mode: t
  override-global-mode: t
  puni-mode: t
  tooltip-mode: t
  global-eldoc-mode: t
  eldoc-mode: t
  show-paren-mode: t
  electric-indent-mode: t
  mouse-wheel-mode: t
  menu-bar-mode: t
  file-name-shadow-mode: t
  context-menu-mode: t
  global-font-lock-mode: t
  font-lock-mode: t
  blink-cursor-mode: t
  minibuffer-regexp-mode: t
  column-number-mode: t
  line-number-mode: t
  transient-mark-mode: t
  auto-composition-mode: t
  auto-encryption-mode: t
  auto-compression-mode: t
  hs-minor-mode: t

Load-path shadows:
/var/home/alist/.config/emacs/elpa/transient-20240509.1849/transient hides /var/home/alist/.local/share/emacs/30.0.50/lisp/transient
/var/home/alist/.config/emacs/elpa/modus-themes-20240505.331/theme-loaddefs hides /var/home/alist/.local/share/emacs/30.0.50/lisp/theme-loaddefs

Features:
(shadow mail-extr mailalias rmail emacsbug whitespace dired-aux
magit-patch bug-reference etags fileloop flyspell ispell org-element
org-persist org-id org-refile avl-tree generator oc-basic ol-eww eww
url-queue mm-url ol-rmail ol-mhe ol-irc ol-info ol-gnus nnselect
gnus-art mm-uu mml2015 mm-view mml-smime smime dig gnus-sum gnus-group
gnus-undo gnus-start gnus-dbus gnus-cloud nnimap nnmail mail-source utf7
nnoo gnus-spec gnus-int gnus-range gnus-win gnus nnheader range
ol-docview doc-view jka-compr image-mode exif ol-bibtex bibtex ol-bbdb
ol-w3m ol-doi org-link-doi org-tempo tempo ob-clojure ob-fennel
fennel-proto-repl fennel-mode inf-lisp ob-lua ob-shell org ob ob-tangle
ob-ref ob-lob ob-table ob-exp org-macro org-src ob-comint org-pcomplete
org-list org-footnote org-faces org-entities org-version ob-emacs-lisp
ob-core ob-eval org-cycle org-table ol org-fold org-fold-core org-keys
oc org-loaddefs cal-menu calendar cal-loaddefs org-compat org-macs
vtable edebug tabify csv csv-autoloads loaddefs-gen tar-mode mm-archive
gnutls network-stream url-cache url-http url-auth url-gw nsm cl-print
debug backtrace vc-git find-func shortdoc puni consult magit-bookmark
bookmark display-line-numbers help-fns radix-tree mule-util csv-mode
sort cus-start clj-refactor cap-words superword subword hydra lv
inflections mc-hide-unmatched-lines-mode mc-mark-more sgml-mode facemenu
mc-cycle-cursors multiple-cursors-core advice rect paredit yasnippet
cider tramp-sh cider-debug cider-browse-ns cider-mode cider-xref-backend
cider-find cider-inspector cider-completion cider-profile cider-eval
cider-jar arc-mode archive-mode cider-repl-history pulse cider-repl
cider-resolve cider-test cider-overlays cider-stacktrace cider-doc
cider-browse-spec cider-clojuredocs cider-eldoc cider-docstring shr
pixel-fill kinsoku url-file svg dom cider-client cider-common xref
cider-completion-context cider-connection cider-popup sesman-browser
nrepl-client cider-util color tramp trampver tramp-integration files-x
tramp-message tramp-compat xdg parse-time iso8601 tramp-loaddefs sesman
vc vc-dispatcher queue nrepl-dict spinner parseedn parseclj-parser
parseclj-lex parseclj-alist clojure-mode lisp-mnt align thingatpt
misearch multi-isearch clojure-compilation-mode compile face-remap
magit-extras vertico-directory orderless magit-submodule magit-blame
magit-stash magit-reflog magit-bisect magit-push magit-pull magit-fetch
magit-clone magit-remote magit-commit magit-sequence magit-notes
magit-worktree magit-tag magit-merge magit-branch magit-reset
magit-files magit-refs magit-status magit magit-repos magit-apply
magit-wip magit-log which-func imenu magit-diff smerge-mode diff
diff-mode track-changes git-commit log-edit message sendmail yank-media
puny rfc822 mml mml-sec epa derived epg rfc6068 epg-config gnus-util
text-property-search time-date mm-decode mm-bodies mm-encode mail-parse
rfc2231 rfc2047 rfc2045 mm-util ietf-drums mail-prsvr mailabbrev
mail-utils gmm-utils mailheader pcvs-util add-log magit-core
magit-autorevert magit-margin magit-transient magit-process with-editor
shell pcomplete comint ansi-osc ansi-color magit-mode transient
magit-git magit-base magit-section format-spec cursor-sensor crm project
noutline outline elec-pair isayt hideshow savehist delsel pixel-scroll
cua-base ring autorevert filenotify repeat vertico marginalia
corfu-popupinfo cape corfu compat region-bindings recentf tree-widget
init gnome-proxy gsettings s gvariant parsec dash server lua-prettify
json-hs-extra treesit indirect-narrow dired dired-loaddefs
use-package-delight modus-vivendi-tritanopia-theme
modus-operandi-tritanopia-theme modus-vivendi-deuteranopia-theme
modus-operandi-deuteranopia-theme modus-vivendi-tinted-theme
modus-operandi-tinted-theme modus-vivendi-theme modus-operandi-theme
modus-themes dbus xml common-lisp-modes novice cus-edit pp cus-load
wid-edit font mode-line defaults edmacro kmacro functions
use-package-bind-key bind-key local-config delight comp comp-cstr
warnings comp-run comp-common rx use-package-ensure cl-extra help-mode
use-package-core early-init finder-inf blog-autoloads cape-autoloads
clj-decompiler-autoloads clj-refactor-autoloads cider-autoloads
clojure-mode-autoloads common-lisp-modes-autoloads consult-autoloads
corfu-terminal-autoloads corfu-autoloads csv-mode-autoloads
delight-autoloads eat-autoloads expand-region-autoloads
fennel-mode-autoloads gnome-proxy-autoloads gsettings-autoloads
gvariant-autoloads infinite-autoloads inflections-autoloads
isayt-autoloads jdecomp-autoloads lsp-java-autoloads
lsp-metals-autoloads dap-mode-autoloads lsp-docker-autoloads
bui-autoloads lsp-treemacs-autoloads lsp-mode-autoloads f-autoloads
marginalia-autoloads markdown-mode-autoloads
message-view-patch-autoloads magit-autoloads pcase
magit-section-autoloads git-commit-autoloads modus-themes-autoloads
mu4e-alert-autoloads alert-autoloads log4e-autoloads gntp-autoloads
multiple-cursors-autoloads orderless-autoloads ov-autoloads
ox-hugo-autoloads paredit-autoloads parsec-autoloads parseedn-autoloads
parseclj-autoloads phi-search-autoloads popon-autoloads puni-autoloads
easy-mmode queue-autoloads racket-mode-autoloads
region-bindings-autoloads request-autoloads scala-mode-autoloads
separedit-autoloads edit-indirect-autoloads sesman-autoloads
spinner-autoloads sql-indent-autoloads tomelr-autoloads
transient-autoloads treemacs-autoloads cfrs-autoloads posframe-autoloads
ht-autoloads hydra-autoloads lv-autoloads pfuture-autoloads
ace-window-autoloads avy-autoloads s-autoloads dash-autoloads
vertico-autoloads vundo-autoloads info with-editor-autoloads
yaml-autoloads yaml-mode-autoloads yasnippet-autoloads
zig-mode-autoloads reformatter-autoloads package browse-url url
url-proxy url-privacy url-expand url-methods url-history url-cookie
generate-lisp-file url-domsuf url-util mailcap url-handlers url-parse
auth-source cl-seq eieio eieio-core cl-macs icons password-cache json
subr-x map byte-opt gv bytecomp byte-compile url-vars cl-loaddefs cl-lib
rmc iso-transl tooltip cconv eldoc paren electric uniquify ediff-hook
vc-hooks lisp-float-type elisp-mode mwheel term/pgtk-win pgtk-win
term/common-win pgtk-dnd tool-bar dnd fontset image regexp-opt fringe
tabulated-list replace newcomment text-mode lisp-mode prog-mode register
page tab-bar menu-bar rfn-eshadow isearch easymenu timer select
scroll-bar mouse jit-lock font-lock syntax font-core term/tty-colors
frame minibuffer nadvice seq simple cl-generic indonesian philippine
cham georgian utf-8-lang misc-lang vietnamese tibetan thai tai-viet lao
korean japanese eucjp-ms cp51932 hebrew greek romanian slovak czech
european ethiopic indian cyrillic chinese composite emoji-zwj charscript
charprop case-table epa-hook jka-cmpr-hook help abbrev obarray oclosure
cl-preloaded button loaddefs theme-loaddefs faces cus-face macroexp
files window text-properties overlay sha1 md5 base64 format env
code-pages mule custom widget keymap hashtable-print-readable backquote
threads xwidget-internal dbusbind inotify dynamic-setting
system-font-setting font-render-setting cairo gtk pgtk lcms2 multi-tty
move-toolbar make-network-process native-compile emacs)

Memory information:
((conses 16 1161598 294352) (symbols 48 63695 6)
 (strings 32 336047 7960) (string-bytes 1 10612200)
 (vectors 16 113623) (vector-slots 8 2104398 91480)
 (floats 8 1081 3104) (intervals 56 25909 3309) (buffers 992 38))

--
Andrey Listopadov