summaryrefslogtreecommitdiff
path: root/emacs/site-lisp/elastic-indent.el
diff options
context:
space:
mode:
authornoa2024-11-11 12:56:26 +0800
committernoa2024-11-11 12:56:26 +0800
commit4c779e0190f438acd3426caa15fd357c6936ec2a (patch)
tree8a1afc9041cd7e95d0bc8096e64213604e771048 /emacs/site-lisp/elastic-indent.el
parent67f335e213b4bfa082c410c40f4e00b897c4c2f9 (diff)
Add some files from site-lisp
Diffstat (limited to 'emacs/site-lisp/elastic-indent.el')
-rw-r--r--emacs/site-lisp/elastic-indent.el291
1 files changed, 291 insertions, 0 deletions
diff --git a/emacs/site-lisp/elastic-indent.el b/emacs/site-lisp/elastic-indent.el
new file mode 100644
index 0000000..a37bacb
--- /dev/null
+++ b/emacs/site-lisp/elastic-indent.el
@@ -0,0 +1,291 @@
+;; elastic-indent-mode --- fix indentation with variable-pitch fonts. -*- lexical-binding: t; -*-
+;; Copyright (C) 2023 Jean-Philippe Bernardy
+;; Copyright (C) 2021 Scott Messick (tenbillionwords)
+
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; General terminological note: elastic-indent-mode is concerned with
+;;; adjusting the width of only spaces and tabs which occur before a
+;;; printing character (not space or tab) on a line. We use the word
+;;; “indentation” to refer to these tabs or spaces. It is ambiguous
+;;; whether Unicode space characters other than space and (horizontal)
+;;; tab should be considered part of the leading space or not, but in
+;;; the code we assume it is only spaces and tabs. Thus
+;;; elastic-indent-mode treats other space characters as printing
+;;; characters.
+;;; The support for tabs is currently limited. Tabs can only be first
+;;; in the indentation (they cannot follows spaces). Editting code
+;;; with tabs isn't fully supported either.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'dash)
+(require 'elastic-tools)
+
+(defgroup elastic-indent nil "Customization of elastic indentation."
+ :group 'elastic)
+
+(defcustom elastic-indent-lvl-cycle-size 1 "Size of the cycle used for faces.
+If N is the cycle size, then faces 0 to N-1 will be used. See
+also `elastic-indent-fst-col-faces' and `elastic-indent-rest-faces'."
+ :type 'int :group 'elastic-indent)
+
+(defcustom elastic-indent-fontify t
+ "If t, fontify indent levels.
+Fontification can only happen on a per-character basis.
+Therefore, if indentation is implemented by a mix of space and
+tabulation characters, as typical in Emacs source code, the
+results will not be pretty."
+ :type 'bool :group 'elastic-indent)
+
+(defface elastic-indent '((t (:inherit lazy-highlight))) "Face for indentation highlighting.")
+(defface elastic-indent-2 '((t (:inherit highlight))) "Second face for indentation highlighting.")
+
+(defcustom elastic-indent-fst-col-faces '(elastic-indent)
+ "Faces for various levels (First column)." :type '(list face) :group 'elastic-indent)
+
+(defcustom elastic-indent-rest-faces '(nil)
+ "Faces for various levels (First column)."
+ :type '(list face)
+ :group 'elastic-indent)
+
+(defface elastic-indent-vertical-lines '((t ())) "Face for indentation lines.")
+
+(defun elastic-indent-fontify-alternate ()
+ "Highlight indentation by columns of alternating background color."
+ (interactive)
+ (setq elastic-indent-lvl-cycle-size 2)
+ (setq elastic-indent-rest-faces '(elastic-indent elastic-indent-2))
+ (setq elastic-indent-fst-col-faces '(elastic-indent elastic-indent-2)))
+
+(defun elastic-indent-fontify-with-lines ()
+ "Experimental way to fontify indentation."
+ (interactive)
+ (setq elastic-indent-lvl-cycle-size 2)
+ (setq elastic-indent-rest-faces '(elastic-indent-vertical-lines default))
+ (setq elastic-indent-fst-col-faces '(elastic-indent-vertical-lines default)))
+
+(define-minor-mode elastic-indent-mode
+ "Improves indentation with in variable-pitch face.
+Adjust the width of indentation characters to align the indented
+code to the correct position. The correct position is defined as
+the same relative position to the previous line as it were if a
+fixed-pitch face was used.
+
+More precisely, any space character on a line with no printing
+characters before it will be matched up with a corresponding
+character on the previous line, if there is one. That character
+may itself be a width-adjusted space, meaning the width
+ultimately comes from some other kind of character higher up.
+
+Due to technical limitations, this mode does not try to detect
+situations where the font has changed but the text hasn't, which
+will mess up the alignment. You can put
+‘elastic-indent-do-buffer-if-enabled’ in appropriate hooks to
+mitigate the problem."
+ :init-value nil :lighter nil :global nil
+ (if elastic-indent-mode
+ (elastic-tools-add-handler 'elastic-indent-do-region 50)
+ (elastic-tools-remove-handler 'elastic-indent-do-region)
+ (elastic-indent-clear-buffer)))
+
+(defun elastic-indent-char-lvl (pos l-pos)
+ "Return the indentation level at POS.
+An indentation level is not the colum, but rather defined as the
+number of times an indentation occured. Level is negative for the
+spaces which aren't in the first column of any given level. If
+this cannot be determined locally by what happens at POS, then
+look at L-POS, which is a position just to the left of the
+position for which we want the level."
+ (or (get-text-property pos 'elastic-indent-lvl)
+ (if (or (eq pos (point-min)) (eq (char-after (1- pos)) ?\n))
+ 1 ;; first char in the line. creates an indentation level by default.
+ (let ((lvl-before (get-text-property (1- pos) 'elastic-indent-lvl)))
+ (if (and lvl-before (<= lvl-before 0))
+ ;; it's a space before this position. Thus this character creates a new indentation level.
+ (1+ (abs lvl-before))
+ (- (abs (or (get-text-property l-pos 'elastic-indent-lvl) 0)))))))) ; in case of tabs we have to come up with some number. Use 0.
+
+(defun elastic-indent-set-char-pixel-width (pos w)
+ "Set the width of character at POS to be W.
+This only works if the character in question is a space or a tab.
+Also add text properties to remember that we did this change and
+by what."
+ (if w (add-text-properties pos (1+ pos)
+ (list 'display (list 'space :width (list w))
+ 'elastic-indent-adjusted t
+ 'elastic-indent-width w))
+ (remove-text-properties pos (1+ pos) '(display elastic-indent-width elastic-indent-adjusted))))
+
+(defun elastic-indent-combine-info (std-width lst)
+ "Return the combination of infos in LST.
+STD-WIDTH is the width of a `tab-width' fraction of a tab."
+ (cons (-sum (--map (or (car it) std-width) lst))
+ (-some 'cdr lst)))
+
+(defun elastic-indent-set-char-info (pos i)
+ "Set width and face I for space at POS.
+The car of I is the width, and the cdr of I is the level."
+ (when i
+ (elastic-indent-set-char-pixel-width pos (car i))
+ (let* ((lvl (or (cdr i)))
+ (face-set (if (> lvl 0) elastic-indent-fst-col-faces elastic-indent-rest-faces))
+ (face (nth (mod lvl elastic-indent-lvl-cycle-size) face-set)))
+ (put-text-property pos (1+ pos) 'elastic-indent-lvl lvl)
+ (when (and elastic-indent-fontify lvl)
+ (if face (put-text-property pos (1+ pos) 'font-lock-face face)
+ (remove-text-properties pos (1+ pos) '(font-lock-face)))))))
+
+(defun elastic-indent-column-leaves-indent (target)
+ "Return t if by advancing TARGET columns one reaches the end of the indentation."
+ (let ((col 0))
+ (while (and (not (eobp)) (< col target))
+ (pcase (char-after)
+ (?\s (setq col (1+ col)))
+ (?\t (setq col (+ 8 col)))
+ (_ (setq target -1)))
+ (when (<= 0 target) (forward-char)))
+ (not (looking-at (rx (any "\s\t"))))))
+
+(defun elastic-indent-in-indent ()
+ "Return t iff all characters to the left are indentation chars."
+ (save-excursion
+ (while (and (not (bolp))
+ (pcase (char-before)
+ ((or ?\s ?\t) (or (backward-char 1) t)))))
+ (bolp)))
+
+(defun elastic-indent-do-1 (force-propagate start-col change-end)
+ "Adjust width of indentations.
+This is in response to a change starting at point and ending at
+CHANGE-END. START-COL is the minimum column where a change
+occured. Start at point and continue until line which cannot be
+impacted by the change is found. Such a line occurs if its
+indentation level is less than START-COL and starts after
+CHANGE-END.
+
+Thus a large value of START-COL means that little work needs to
+be done by this function. This optimisation is important,
+because otherwise one needs to find a line with zero indentation,
+which can be much further down the buffer.
+
+If a change spans several lines, then START-COL is ignored, and
+changes are propagated until indentation level reaches 0.
+FORCE-PROPAGATE forces even single-line changes to be treated
+this way."
+ (let (prev-widths ; the list of widths of each *column* of indentation of the previous line
+ (reference-pos 0) ; the buffer position in the previous line of 1st printable char
+ (std-width (window-font-width))
+ space-widths) ; accumulated widths of columns for current line
+ ;; (message "elastic-indent-do: %s [%s, %s]" start-col (point) change-end)
+ (cl-flet*
+ ((get-next-column-width () ; find reference width in the previous line. (effectful)
+ (let* ((l-pos (1- (point)))
+ (w (if prev-widths (pop prev-widths) ; we have a cached width: use it.
+ (if (eql (char-after reference-pos) ?\n) ; we're at the end of the reference line.
+ (cons nil (if (bolp)
+ (- 1) ; we're at a space on the 1st column with an empty line above. No indentation here.
+ (- (abs (elastic-indent-char-lvl l-pos nil))))) ; leave width as is. Level is the same as char on the left; but not 1st column.
+ (prog1
+ (cons (elastic-tools-char-pixel-width reference-pos) (elastic-indent-char-lvl reference-pos l-pos))
+ (setq reference-pos (1+ reference-pos)))))))
+ (push w space-widths) ; cache width for next line
+ ;; (message "char copy: %s->%s (w=%s) %s" (1- reference-pos) (point) w prev-widths)
+ w))
+ (char-loop () ; take care of one line.
+ ;; copy width from prev-widths, then reference-pos to (point). Loop to end of indentation.
+ ;; Preconditions: cur col = start-col. prev-widths=nil or contains cached widths
+ ;; (message "@%s %s=>%s %s" (line-number-at-pos) (- reference-pos (length prev-widths)) (point) prev-widths)
+ (while-let ((cur-line-not-ended-c (not (eolp)))
+ (char (char-after))
+ (char-is-indent-c (or (eql char ?\s) (eql char ?\t))))
+ (pcase char
+ (?\s (elastic-indent-set-char-info (point) (get-next-column-width)))
+ (?\t (elastic-indent-set-char-info (point)
+ (elastic-indent-combine-info std-width
+ (--map (get-next-column-width) (-repeat tab-width ()))))))
+ (forward-char)))
+ (next-line () ; advance to next line, maintaining state.
+ (setq prev-widths (nreverse space-widths))
+ (setq space-widths nil)
+ (setq reference-pos (point)) ; we go to next line exactly after we reached the last space
+ (forward-line)))
+ (save-excursion
+ (when (eq (forward-line -1) 0)
+ (setq reference-pos (progn (move-to-column start-col) (point)))))
+ ;; (message "%s: first line. update from startcol=%s curcol=%s" (line-number-at-pos) start-col (current-column))
+ (when (elastic-indent-in-indent) ; if not characters are not to be changed.
+ (char-loop))
+ (next-line)
+ (when (or force-propagate (<= (point) change-end))
+ ;; this is a multiline change. It influences column 0.
+ ;; (message "%s: main phase; update lines from col 0" (line-number-at-pos))
+ (when (> start-col 0) ; reference is wrong now
+ (setq start-col 0)
+ (setq prev-widths nil)
+ (setq reference-pos (save-excursion (forward-line -1) (point))))
+ (while (<= (point) change-end)
+ (char-loop)
+ (next-line)))
+ ;; (message "%s: propagate changes and stop if indentation is too small" (line-number-at-pos))
+ (while (not (elastic-indent-column-leaves-indent start-col))
+ (char-loop)
+ (next-line))
+ ;; (message "%s: propagation complete" (line-number-at-pos))
+ (beginning-of-line)))) ; we did not in fact propagate on this line yet.
+
+(defun elastic-indent-change-extend (end)
+ "Return the first position after END which does not contain a space."
+ (max (point)
+ (save-excursion
+ (goto-char end)
+ ;; if we are changing something then the spaces just after the changes are
+ ;; invalid and must be updated.
+ (search-forward-regexp "[^\t\s]" nil t)
+ (1- (point)))))
+
+(defun elastic-indent-do-region (force-propagate start end)
+ "Adjust width of indentation characters in given region and propagate.
+The region is between START and END in current
+buffer. Propagation means to also fix the indentation of the
+lines which follow, if their indentation widths might be impacted
+by changes in given region. See `elastic-indent-do' for the
+explanation of FORCE-PROPAGATE."
+ ;; (message "edr: (%s) %s-%s" force-propagate start end)
+ (let ((e (elastic-indent-change-extend end)))
+ (elastic-indent-clear-region start e)
+ (goto-char start)
+ (elastic-indent-do-1 force-propagate (current-column) e)))
+
+(defun elastic-indent-clear-region (start end)
+ "Remove all `elastic-indent-mode' properties between START and END."
+ (interactive "r")
+ (elastic-tools-clear-region-properties
+ start end 'elastic-indent-adjusted '(elastic-indent-adjusted
+ elastic-indent-width
+ elastic-indent-lvl
+ display
+ font-lock-face
+ mouse-face)))
+
+(defun elastic-indent-clear-buffer ()
+ "Remove all `elastic-indent-mode' properties in buffer."
+ (interactive)
+ (elastic-indent-clear-region (point-min) (point-max)))
+
+(provide 'elastic-indent)
+;;; elastic-indent.el ends here