diff --git a/modules/init-look-and-feel.el b/modules/init-look-and-feel.el index 717e4073..0a05127f 100644 --- a/modules/init-look-and-feel.el +++ b/modules/init-look-and-feel.el @@ -281,13 +281,139 @@ Set FONT and SIZE if they are passed as arguments." :ensure vlf :defer t) +(require 'rx) + +(defun exordium--alist-get-derived-mode (alist) + "Return value from ALIST where key matches `derived-mode-p' of current buffer." + (alist-get nil alist nil nil + (lambda (elt _) + (apply #'derived-mode-p + (if (listp elt) elt (list elt)))))) + +(defun exordium--delete-trailing-whitespace-data-start-and-cleanup () + "Find beginning of data section start. +Also cleanup trailing lines when `delete-trailing-lines' is +non-nil and insert new line when `require-final-newline' is +non-nil." + (save-excursion + (when-let* + ((exordium-delete-trailing-whitespace-skip-data) + (data-keywords (exordium--alist-get-derived-mode + exordium-delete-trailing-whitespace-data-keywords))) + (when (re-search-forward + (rx-to-string + `(seq line-start + (or ,@data-keywords) + (zero-or-more whitespace) + line-end) + t) + nil t) + (let ((point (point))) + ;; When this is not a perlpod, clean it too - return nil + (unless (when-let* + ((perlpod-keywords + (exordium--alist-get-derived-mode + exordium-delete-trailing-whitespace-data-perlpod-keywords))) + (re-search-forward + (rx-to-string + `(seq "\n" (zero-or-more whitespace) + "\n=" (or ,@perlpod-keywords)) + t) + nil t)) + ;; Otherwise do a minimal cleanup: + ;; Delete trailing lines, as `delete-trailing-whitespace' won't + ;; handle it + (when-let* + ((delete-trailing-lines) + ((re-search-forward + (rx line-start + (group (one-or-more (or whitespace "\n"))) + string-end) + nil t)) + (b (match-beginning 1)) + (e (match-end 1)) + ((region-modifiable-p b e))) + (delete-region b e)) + ;; Ensure trailing new line is present, as + ;; `delete-trailing-whitespace' won't handle it + (when (and require-final-newline + (not (re-search-forward (rx "\n" string-end) nil t))) + (goto-char (point-max)) + (insert "\n")) + ;; trim whitespace only up to data section + point)))))) + +(defun exordium-delete-trailing-whitespace-in-buffer (&optional start end) + "Delete trailing whitespace in current buffer. +Like `delete-trailing-whitespace', but any restrictions are +ignored. + +Additionally, when called with a nil END and +`exordium-delete-trailing-whitespace-skip-data' is non-nil it +will not affect data sections in Ruby and Perl modes (with an +exception of a perlpod). When both END and +`exordium-delete-trailing-whitespace-skip-data' are nil it will +delete whitespace in the whole buffer. When called with a +non-nil END it will delete trailing whitespace up to END. + +If called interactively, START and END are the start/end of the +region if the mark is active, or the whole buffer, up to data +secion (when `exordium-delete-trailing-whitespace-skip-data' is +non-nil) if the mark is inactive. + +The function doesn't delete trailing whitespaces when buffer is +in any of `exordium-delete-trailing-whitespace-inhibit-modes', +and the function has been called without a prefix argument. + +Depending on mode a data section starts after a line with a one +of keywords __END__ or __DATA__ or a one of control +characters (^D, ^Z) and ends at the end of file. Such a +preservation of whitespaces may be important for some types of +data (i.e., patches) but should be to safe to clear it from +others other (i.e., perlpod). See +https://perldoc.perl.org/perldata, +https://perldoc.perl.org/perlpodspec, and +https://docs.ruby-lang.org/en/3.3/Object.html for more details." + (interactive (progn + (barf-if-buffer-read-only) + (if (use-region-p) + (list (region-beginning) (region-end)) + (list nil nil)))) + (if (and (not current-prefix-arg) + (apply #'derived-mode-p + exordium-delete-trailing-whitespace-inhibit-modes)) + (when (called-interactively-p 'any) + (user-error "Not deleting trailing whitespaces in '%s', call with prefix to override" + major-mode)) + (without-restriction + (save-mark-and-excursion + (save-match-data + (goto-char (point-min)) + (when-let* + (((looking-at + (rx string-start + (group (zero-or-more (or whitespace "\n")) + (or "\n" line-end))))) + (b (match-beginning 1)) + (e (match-end 1)) + ((region-modifiable-p b e))) + (delete-region b e)) + + (let ((end + (or end + (exordium--delete-trailing-whitespace-data-start-and-cleanup)))) + (delete-trailing-whitespace start end))))))) + ;; Remove trailing blanks on save (define-minor-mode delete-trailing-whitespace-mode - "Remove trailing whitespace upon saving a buffer." + "Remove trailing whitespace upon saving a buffer. +See `exordium-delete-trailing-whitespace-in-buffer' for more details." :lighter nil (if delete-trailing-whitespace-mode - (add-hook 'before-save-hook #'delete-trailing-whitespace nil t) - (remove-hook 'before-save-hook #'delete-trailing-whitespace t))) + (add-hook 'before-save-hook + #'exordium-delete-trailing-whitespace-in-buffer nil t) + (remove-hook 'before-save-hook + #'exordium-delete-trailing-whitespace-in-buffer t))) (define-globalized-minor-mode global-delete-trailing-whitespace-mode delete-trailing-whitespace-mode diff --git a/modules/init-prefs.el b/modules/init-prefs.el index ec0aac40..92bd422a 100644 --- a/modules/init-prefs.el +++ b/modules/init-prefs.el @@ -91,6 +91,59 @@ a lot of trailing whitespaces." :group 'exordium :type 'boolean) +(defcustom exordium-delete-trailing-whitespace-skip-data t + "When t preserve whitespaces in Data section in Perl and Ruby modes. +This variable is used by +`exordium-delete-trailing-whitespace-in-buffer' (which see) and +in consequence in `delete-trailing-whitespace-mode' and +`global-delete-trailing-whitespace-mode'. + +See also `exordium-delete-trailing-whitespace-data-keywords'." + :group 'exordium + :type 'boolean) + +(defcustom exordium-delete-trailing-whitespace-inhibit-modes + '(diff-mode special-mode view-mode comint-mode cider-repl-mode + haskell-interactive-mode) + "List of modes where deletion of trailing whitespaces is inhibited. +See also docstring for +`exordium-delete-trailing-whitespace-in-buffer' and +`delete-trailing-whitespace-mode'." + :type '(repeat (symbol :tag "Mode")) + :group 'exordium) + +(defcustom exordium-delete-trailing-whitespace-data-keywords + '(((ruby-mode ruby-ts-mode enh-ruby-mode) . ("__END__")) + ((perl-mode perl-ts-mode cperl-mode) . ("__END__" "__DATA__" "" ""))) + "Mapping between modes and keywords that indicate data section for that mode. +Data section will not have its trailing whitespace +deleted, barred for trailing new lines when +`delete-trailing-lines' is non nil and for ensuring ending +newline when `require-final-newline' is non nil. + +See also `exordium-delete-trailing-whitespace-data-perlpod-keywords'." + :type '(alist :key-type (choice + (symbol :tag "Mode") + (repeat (symbol :tag "Mode"))) + :value-type (repeat (string :tag "Keyword"))) + :group 'exordium) + +(defcustom exordium-delete-trailing-whitespace-data-perlpod-keywords + '((perl-mode perl-ts-mode cperl-mode) . ("pod" "head1" "head2" "head3" + "head4" "head5" "head6" "over" + "item" "back" "begin" "end" + "for" "encoding" "cut")) + "Mapping between Perl mode and perlpod keywords. +When a data section happens to contain a perlpod as sensed by +existence of a perlpod keyword it will have its trailing +whitespaces deleted, even when +`exordium-delete-trailing-whitespace-skip-data' is non-nil." + :type '(alist :key-type (choice + (symbol :tag "Mode") + (repeat (symbol :tag "Mode"))) + :value-type (repeat (string :tag "Keyword"))) + :group 'exordium) + (defcustom exordium-enable-electric-pair-mode t "Whether to enable `electric-pair-mode'." :group 'exordium