Subject: bug#67161: 30.0.50; [PATCH] Add option `dired-filename-display-length'

From: Liu Hui <[email-masked]>
Date: Sat, 18 Nov 2023 17:23:26 +0800

Eli Zaretskii <[email-masked]> 于2023年11月16日周四 20:11写道：
>
> > From: Liu Hui <[email-masked]>
> > Date: Thu, 16 Nov 2023 18:07:04 +0800
> > Cc: Stefan Kangas <[email-masked]>, Stefan Monnier <[email-masked]>,
> >       [email-masked]
> >
> > From 991ea588df6799331a7feea9e83493ef0d724096 Mon Sep 17 00:00:00 2001
> > From: Liu Hui <[email-masked]>
> > Date: Tue, 14 Nov 2023 16:14:12 +0800
> > Subject: [PATCH] Add option `dired-filename-display-length'
> >
> > * lisp/dired.el (dired-filename-display-length): New option.
> > (dired-insert-set-properties): Set invisibility spec for long
> > filenames.
> > (dired--get-ellipsis-length)
> > (dired--get-filename-display-length)
> > (dired-filename-update-invisibility-spec): New functions.
> > (dired-mode): Add filename invisibility spec.
> > (dired-make-directory-clickable):
> > (dired-kill-when-opening-new-dired-buffer):
> > (dired-hide-details-preserved-columns): Add missing group.
> > * lisp/wdired.el (wdired-change-to-wdired-mode)
> > (wdired-change-to-dired-mode): Update filename invisibility spec.
>
> These changes need a NEWS entry to describe the new option and its
> effect in short.

Added.

> > diff --git a/lisp/dired.el b/lisp/dired.el
> > index 8919d2c223f..aad77a3dfc0 100644
> > --- a/lisp/dired.el
> > +++ b/lisp/dired.el
> > @@ -350,6 +350,7 @@ dired-after-readin-hook
> >  (defcustom dired-make-directory-clickable t
> >    "When non-nil, make the directory at the start of the dired buffer clickable."
> >    :version "29.1"
> > +  :group 'dired
> >    :type 'boolean)
> >
> >  (defcustom dired-initial-position-hook nil
> > @@ -429,6 +430,7 @@ dired-mark-region
> >  (defcustom dired-kill-when-opening-new-dired-buffer nil
> >    "If non-nil, kill the current buffer when selecting a new directory."
> >    :type 'boolean
> > +  :group 'dired
> >    :version "28.1")
> >
> >  (defcustom dired-guess-shell-case-fold-search t
> > @@ -515,6 +517,18 @@ dired-movement-style
> >  (defcustom dired-hide-details-preserved-columns nil
> >    "List of columns which are not hidden in `dired-hide-details-mode'."
> >    :type '(repeat integer)
> > +  :group 'dired
> > +  :version "30.1")
>
> Why do we need an explicit :group tag here?  Options whose group is
> not 'dired' indeed need it, but those whose group is 'dired' get it
> implicitly, AFAIU.

I find these options are implicitly assigned to the dired-guess group
rather than dired group, since dired-guess is the last group.

> > +(defcustom dired-filename-display-length nil
> > +  "If non-nil, hide middle part of long filenames in Dired buffers.
> > +If the value is the symbol `window', then filenames are shortened
> > +to not exceed the right edge of current window.  Otherwise, it
> > +should be an integer representing the maximum filename length."
>
> This should tell more about the effect: that portions of longer file
> names are hidden by using the 'invisible' property and that the
> ellipsis is displayed in their stead.

Done.

> > +(defun dired--get-ellipsis-length ()
> > +  "Return length of ellipsis."
> > +  (let* ((dt (or (window-display-table)
> > +                 buffer-display-table
> > +                 standard-display-table))
> > +         (glyphs (and dt (display-table-slot dt 'selective-display))))
> > +    (if glyphs (length glyphs) (eval-when-compile (length "...")))))
>
> Why do you use 'length' here and not 'string-width' or similar?  You
> seem to assume that each character takes just one column on display?

Fixed.

> > +(defun dired--get-filename-display-length ()
> > +  "Return maximum display length of filename."
>
> This doc string is inaccurate.  The function actually returns the
> number of columns available for displaying the file names in a Dired
> buffer, and it should be called with point at the first character of
> the file name.

Why is it inaccurate? When `dired-filename-display-length' is window,
the function does return the number of columns available, and it is
used as the maximum display length. Moreover, it could be an integer,
which also means the maximum display length.

> > +(defun dired-filename-update-invisibility-spec ()
>
> This function should have a doc string.

Added.

> > diff --git a/lisp/wdired.el b/lisp/wdired.el
> > index 079d93d6011..5d50a574290 100644
> > --- a/lisp/wdired.el
> > +++ b/lisp/wdired.el
> > @@ -261,6 +261,7 @@ wdired-change-to-wdired-mode
> >    (add-function :override (local 'revert-buffer-function) #'wdired-revert)
> >    (set-buffer-modified-p nil)
> >    (setq buffer-undo-list nil)
> > +  (dired-filename-update-invisibility-spec)
> >    (run-mode-hooks 'wdired-mode-hook)
> >    (message "%s" (substitute-command-keys
> >                "Press \\[wdired-finish-edit] when finished \
> > @@ -456,6 +457,7 @@ wdired-change-to-dired-mode
> >    (dired-sort-set-mode-line)
> >    (dired-advertise)
> >    (dired-hide-details-update-invisibility-spec)
> > +  (dired-filename-update-invisibility-spec)
>
> Please add comments in these two places explaining why you call
> dired-filename-update-invisibility-spec here, so that the reader won't
> need to look at that function to understand the reason.

Added.

> Thanks.

BTW, I just find isearch doesn't handle hidden filenames based on
'invisible' text property, which needs to be fixed. So maybe an
overlay-based approach is better? though I don't know if there is any
possible problem with using overlays for this feature. WDYT?
From 9f42d75566e980c326783e4f55c161a67cdf5ae6 Mon Sep 17 00:00:00 2001
From: Liu Hui <[email-masked]>
Date: Tue, 14 Nov 2023 16:14:12 +0800
Subject: [PATCH] Add option `dired-filename-display-length'

* lisp/dired.el (dired-filename-display-length): New option.
(dired-insert-set-properties): Set invisibility spec for long
filenames.
(dired--get-ellipsis-length)
(dired--get-filename-display-length)
(dired-filename-update-invisibility-spec): New functions.
(dired-mode): Add filename invisibility spec.
(dired-make-directory-clickable):
(dired-kill-when-opening-new-dired-buffer):
(dired-hide-details-preserved-columns): Add missing group.
* lisp/wdired.el (wdired-change-to-wdired-mode)
(wdired-change-to-dired-mode): Update filename invisibility spec.
* etc/NEWS: Announce the change.
---
 etc/NEWS       |   7 +++
 lisp/dired.el  | 138 ++++++++++++++++++++++++++++++++++---------------
 lisp/wdired.el |   6 +++
 3 files changed, 110 insertions(+), 41 deletions(-)

diff --git a/etc/NEWS b/etc/NEWS
index 23f4a8b5311..e9a84cb9183 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -472,6 +472,13 @@ empty lines.  It also controls how to move point when encountering a
 boundary (e.g., if every line is visible, invoking 'dired-next-line'
 at the last line will move to the first line).  The default is nil.
 
+*** New user option 'dired-filename-display-length'.
+It is an integer representing the maximum display length of filenames.
+The middle part of filename whose length exceeds the restriction is
+hidden and a ellipsis is displayed instead.  A value of 'window' means
+using the right edge of window as the display restriction.  The
+default is nil.
+
 ** Ediff
 
 ---
diff --git a/lisp/dired.el b/lisp/dired.el
index 8919d2c223f..7eeab86d851 100644
--- a/lisp/dired.el
+++ b/lisp/dired.el
@@ -350,6 +350,7 @@ dired-after-readin-hook
 (defcustom dired-make-directory-clickable t
   "When non-nil, make the directory at the start of the dired buffer clickable."
   :version "29.1"
+  :group 'dired
   :type 'boolean)
 
 (defcustom dired-initial-position-hook nil
@@ -429,6 +430,7 @@ dired-mark-region
 (defcustom dired-kill-when-opening-new-dired-buffer nil
   "If non-nil, kill the current buffer when selecting a new directory."
   :type 'boolean
+  :group 'dired
   :version "28.1")
 
 (defcustom dired-guess-shell-case-fold-search t
@@ -515,6 +517,22 @@ dired-movement-style
 (defcustom dired-hide-details-preserved-columns nil
   "List of columns which are not hidden in `dired-hide-details-mode'."
   :type '(repeat integer)
+  :group 'dired
+  :version "30.1")
+
+(defcustom dired-filename-display-length nil
+  "If non-nil, restrict the display length of filenames.
+If the value is the symbol `window', the right edge of current
+window is used as the restriction.  Otherwise, it should be an
+integer representing the maximum filename length.
+
+The middle part of filename whose length exceeds the restriction
+is hidden by using the `invisible' property and a ellipsis is
+displayed instead."
+  :type '(choice (const   :tag "No restriction" nil)
+                 (const   :tag "Window" window)
+                 (integer :tag "Integer"))
+  :group 'dired
   :version "30.1")
 
 
@@ -1903,48 +1921,61 @@ dired-insert-set-properties
   "Add various text properties to the lines in the region, from BEG to END."
   (save-excursion
     (goto-char beg)
-    (while (< (point) end)
-      (ignore-errors
-	(if (not (dired-move-to-filename))
-	    (unless (or (looking-at-p "^$")
-			(looking-at-p dired-subdir-regexp))
-	      (put-text-property (line-beginning-position)
-				 (1+ (line-end-position))
-				 'invisible 'dired-hide-details-information))
-	  (save-excursion
-            (let ((end (1- (point)))
-                  (opoint (goto-char (1+ (pos-bol))))
-                  (i 0))
-              (put-text-property opoint end 'invisible 'dired-hide-details-detail)
-              (while (re-search-forward "[^ ]+" end t)
-                (when (member (cl-incf i) dired-hide-details-preserved-columns)
-                  (put-text-property opoint (point) 'invisible nil))
-                (setq opoint (point)))))
-          (let ((beg (point)) (end (save-excursion
-	                             (dired-move-to-end-of-filename)
-	                             (1- (point)))))
-            (if dired-click-to-select-mode
-                (put-text-property beg end 'keymap
-                                   dired-click-to-select-map)
-              (when (and dired-mouse-drag-files (fboundp 'x-begin-drag))
-                (put-text-property beg end 'keymap
-                                   dired-mouse-drag-files-map)))
-	    (add-text-properties
-	     beg (1+ end)
-	     `(mouse-face
-	       highlight
-	       dired-filename t
-	       help-echo ,(if dired-click-to-select-mode
-                              "mouse-2: mark or unmark this file"
-                            (if (and dired-mouse-drag-files
-                                     (fboundp 'x-begin-drag))
-                                "down-mouse-1: drag this file to another program
+    (let ((ell-len (dired--get-ellipsis-length)) maxlen filename-col)
+      (while (< (point) end)
+        (ignore-errors
+	  (if (not (dired-move-to-filename))
+	      (unless (or (looking-at-p "^$")
+			  (looking-at-p dired-subdir-regexp))
+	        (put-text-property (line-beginning-position)
+				   (1+ (line-end-position))
+				   'invisible 'dired-hide-details-information))
+	    (save-excursion
+              (let ((end (1- (point)))
+                    (opoint (goto-char (1+ (pos-bol))))
+                    (i 0))
+                (put-text-property opoint end 'invisible 'dired-hide-details-detail)
+                (while (re-search-forward "[^ ]+" end t)
+                  (when (member (cl-incf i) dired-hide-details-preserved-columns)
+                    (put-text-property opoint (point) 'invisible nil))
+                  (setq opoint (point)))))
+            (let ((beg (point)) (end (save-excursion
+	                               (dired-move-to-end-of-filename)
+	                               (1- (point)))))
+              (if dired-click-to-select-mode
+                  (put-text-property beg end 'keymap
+                                     dired-click-to-select-map)
+                (when (and dired-mouse-drag-files (fboundp 'x-begin-drag))
+                  (put-text-property beg end 'keymap
+                                     dired-mouse-drag-files-map)))
+              (when dired-filename-display-length
+                (let ((len (string-width (buffer-substring beg (1+ end))))
+                      ell-beg)
+                  (or maxlen (setq maxlen (dired--get-filename-display-length)))
+                  (when (and (integerp maxlen) (> len maxlen (+ ell-len 2)))
+                    (or filename-col (setq filename-col (current-column)))
+                    (move-to-column (+ filename-col (/ maxlen 2)))
+                    (setq ell-beg (point))
+                    (move-to-column (+ filename-col (/ maxlen 2)
+                                       (- len maxlen) ell-len))
+                    (put-text-property
+                     ell-beg (point) 'invisible 'dired-filename-hide))))
+	      (add-text-properties
+	       beg (1+ end)
+	       `(mouse-face
+	         highlight
+	         dired-filename t
+	         help-echo ,(if dired-click-to-select-mode
+                                "mouse-2: mark or unmark this file"
+                              (if (and dired-mouse-drag-files
+                                       (fboundp 'x-begin-drag))
+                                  "down-mouse-1: drag this file to another program
 mouse-2: visit this file in other window"
-                              "mouse-2: visit this file in other window"))))
-	    (when (< (+ end 5) (line-end-position))
-	      (put-text-property (+ end 5) (line-end-position)
-			         'invisible 'dired-hide-details-link)))))
-      (forward-line 1))))
+                                "mouse-2: visit this file in other window"))))
+	      (when (< (+ end 5) (line-end-position))
+	        (put-text-property (+ end 5) (line-end-position)
+			           'invisible 'dired-hide-details-link)))))
+        (forward-line 1)))))
 
 (defun dired--make-directory-clickable ()
   (save-excursion
@@ -1976,6 +2007,21 @@ dired--make-directory-clickable
                           "RET" click))))
           (setq segment-start (point)))))))
 
+(defun dired--get-ellipsis-length ()
+  "Return length of ellipsis."
+  (let* ((dt (or (window-display-table)
+                 buffer-display-table
+                 standard-display-table))
+         (glyphs (and dt (display-table-slot dt 'selective-display))))
+    (string-width (if glyphs (concat glyphs) "..."))))
+
+(defun dired--get-filename-display-length ()
+  "Return maximum display length of filename.
+It is called at the first character of the file name."
+  (if (integerp dired-filename-display-length)
+      dired-filename-display-length
+    (- (window-max-chars-per-line) 1 (current-column))))
+
 
 ;;; Reverting a dired buffer
 
@@ -2617,6 +2663,7 @@ dired-mode
 	mode-line-buffer-identification
 	(propertized-buffer-identification "%17b"))
   (add-to-invisibility-spec '(dired . t))
+  (dired-filename-update-invisibility-spec)
   ;; Ignore dired-hide-details-* value of invisible text property by default.
   (when (eq buffer-invisibility-spec t)
     (setq buffer-invisibility-spec (list t)))
@@ -3106,6 +3153,15 @@ dired-hide-details-update-invisibility-spec
 
 ;;; Functions to hide/unhide text
 
+(defun dired-filename-update-invisibility-spec ()
+  "Update `buffer-invisibility-spec' for filenames.
+Specifically, the filename invisibility spec is added in Dired
+buffers and removed in WDired buffers."
+  (funcall (if (derived-mode-p 'dired-mode)
+               'add-to-invisibility-spec
+             'remove-from-invisibility-spec)
+           '(dired-filename-hide . t)))
+
 (defun dired--find-hidden-pos (start end)
   (text-property-any start end 'invisible 'dired))
 
diff --git a/lisp/wdired.el b/lisp/wdired.el
index 079d93d6011..b996be9baa9 100644
--- a/lisp/wdired.el
+++ b/lisp/wdired.el
@@ -261,6 +261,9 @@ wdired-change-to-wdired-mode
   (add-function :override (local 'revert-buffer-function) #'wdired-revert)
   (set-buffer-modified-p nil)
   (setq buffer-undo-list nil)
+  ;; remove filename invisibility spec to ensure filenames are visible
+  ;; for editing
+  (dired-filename-update-invisibility-spec)
   (run-mode-hooks 'wdired-mode-hook)
   (message "%s" (substitute-command-keys
 		 "Press \\[wdired-finish-edit] when finished \
@@ -456,6 +459,9 @@ wdired-change-to-dired-mode
   (dired-sort-set-mode-line)
   (dired-advertise)
   (dired-hide-details-update-invisibility-spec)
+  ;; re-add filename invisibility spec that is removed in
+  ;; `wdired-change-to-wdired-mode'
+  (dired-filename-update-invisibility-spec)
   (remove-hook 'kill-buffer-hook #'wdired-check-kill-buffer t)
   (remove-hook 'before-change-functions #'wdired--before-change-fn t)
   (remove-hook 'after-change-functions #'wdired--restore-properties t)
-- 
2.25.1