aboutsummaryrefslogtreecommitdiff
path: root/.config/emacs/lisp/sideline.el
diff options
context:
space:
mode:
Diffstat (limited to '.config/emacs/lisp/sideline.el')
-rw-r--r--.config/emacs/lisp/sideline.el908
1 files changed, 908 insertions, 0 deletions
diff --git a/.config/emacs/lisp/sideline.el b/.config/emacs/lisp/sideline.el
new file mode 100644
index 0000000..e989456
--- /dev/null
+++ b/.config/emacs/lisp/sideline.el
@@ -0,0 +1,908 @@
+;;; sideline.el --- Show information on the side -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022-2026 Shen, Jen-Chieh
+;; Created date 2022-06-13 22:08:26
+
+;; Author: Shen, Jen-Chieh <jcs090218@gmail.com>
+;; URL: https://github.com/emacs-sideline/sideline
+;; Version: 0.2.0
+;; Package-Requires: ((emacs "28.1") (ht "2.4"))
+;; Keywords: convenience
+
+;; This file is NOT part of GNU Emacs.
+
+;; 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:
+;;
+;; This library provides the frontend UI to display information either on the
+;; left/right side of the buffer window.
+;;
+;; 1) You would need to first set up the backends,
+;;
+;; (setq sideline-backends-left '(sideline-flycheck))
+;;
+;; 2) Then enable the sideline in the target buffer,
+;;
+;; M-x sideline-mode
+;;
+;; For backends choice, see https://github.com/emacs-sideline/sideline#-example-projects
+;;
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'face-remap)
+(require 'mule-util)
+(require 'rect)
+(require 'subr-x)
+
+(require 'ht)
+
+(defgroup sideline nil
+ "Show information on the side."
+ :prefix "sideline-"
+ :group 'tool
+ :link '(url-link :tag "Repository" "https://github.com/emacs-sideline/sideline"))
+
+(defcustom sideline-backends-left nil
+ "The list of active backends to display sideline on the left."
+ :type '(list symbol)
+ :group 'sideline)
+
+(defcustom sideline-backends-right nil
+ "The list of active backends to display sideline on the right."
+ :type '(list symbol)
+ :group 'sideline)
+
+(defcustom sideline-order-left 'down
+ "Display order on the left sidelines."
+ :type '(choice (const :tag "Search up" up)
+ (const :tag "Search down" down))
+ :group 'sideline)
+
+(defcustom sideline-order-right 'up
+ "Display order on the right sidelines."
+ :type '(choice (const :tag "Search up" up)
+ (const :tag "Search down" down))
+ :group 'sideline)
+
+(defcustom sideline-force-display-if-exceeds nil
+ "Display sideline even if the line width are wider than the window width."
+ :type 'boolean
+ :group 'sideline)
+
+(defcustom sideline-truncate t
+ "Truncate sideline if the line width are wider than the window width."
+ :type 'boolean
+ :group 'sideline)
+
+(defface sideline-default
+ '((((background light)) :foreground "DarkOrange")
+ (t :foreground "yellow"))
+ "Face used to highlight action text."
+ :group 'sideline)
+
+(defface sideline-backend
+ '((((background light)) :foreground "#7F7F7F")
+ (t :foreground "#9B9B9B"))
+ "Face used to highlight action text."
+ :group 'sideline)
+
+(defcustom sideline-display-backend-name nil
+ "Weather to display backend name in the candidate."
+ :type 'boolean
+ :group 'sideline)
+
+(defcustom sideline-display-backend-type 'outer
+ "Method type to display backend name."
+ :type '(choice (const :tag "Display on left" left)
+ (const :tag "Display on right" right)
+ (const :tag "Display on inner" inner)
+ (const :tag "Display on outer" outer))
+ :group 'sideline)
+
+(defcustom sideline-display-backend-format "[%s]"
+ "Format string for candidate and backend name."
+ :type 'string
+ :group 'sideline)
+
+(defcustom sideline-backends-left-skip-current-line t
+ "Don't display left sideline in current line."
+ :type 'boolean
+ :group 'sideline)
+
+(defcustom sideline-backends-right-skip-current-line t
+ "Don't display right sideline in current line."
+ :type 'boolean
+ :group 'sideline)
+
+(defcustom sideline-format-left "%s "
+ "Format candidate string for left alignment."
+ :type 'string
+ :group 'sideline)
+
+(defcustom sideline-format-right " %s"
+ "Format candidate string for right alignment."
+ :type 'string
+ :group 'sideline)
+
+(defcustom sideline-priority 100
+ "Overlays' priority."
+ :type 'integer
+ :group 'sideline)
+
+(defcustom sideline-delay 0.2
+ "Number of seconds to wait before showing sideline."
+ :type 'number
+ :group 'sideline)
+
+(defcustom sideline-backend-delays nil
+ "Number of seconds to wait before showing sideline per backend."
+ :type '(list symbol)
+ :group 'sideline)
+
+(defcustom sideline-pre-render-hook nil
+ "Hooks runs before rendering sidelines."
+ :type 'hook
+ :group 'sideline)
+
+(defcustom sideline-post-render-hook nil
+ "Hooks runs after rendering sidelines."
+ :type 'hook
+ :group 'sideline)
+
+(defcustom sideline-reset-hook nil
+ "Hooks runs once the sideline is reset in `post-command-hook'."
+ :type 'hook
+ :group 'sideline)
+
+(defcustom sideline-inhibit-display-function #'sideline-stop-p
+ "Function call to determine weather to display sideline or not."
+ :type 'function
+ :group 'sideline)
+
+(defvar-local sideline--render-data nil
+ "Data used to render; only used everytime before rendering.")
+
+(defvar-local sideline--render-data-wapp (make-hash-table)
+ "Record pixel position; only used everytime before rendering.")
+
+(defvar-local sideline--overlays (make-hash-table)
+ "Displayed overlays.")
+
+(defvar-local sideline--ex-bound-or-point nil
+ "Record of last bound; if this isn't the same, clean up overlays.")
+
+(defvar-local sideline--occupied-lines-left nil
+ "Occupied lines on the left.")
+
+(defvar-local sideline--occupied-lines-right nil
+ "Occupied lines on the right.")
+
+(defvar-local sideline--text-scale-mode-amount nil
+ "Record of last variable `text-scale-mode-amount'.")
+
+(defvar-local sideline-render-this-command nil
+ "If this is non-nil, re-render this command.")
+
+(defvar-local sideline--max-remain-spaces nil
+ "Hold the maximum remaining spaces when finding lines..")
+
+(defvar-local sideline--max-remain-spaces-line nil
+ "Hold the line information that has the maximum remaining spaces.")
+
+;;
+;; (@* "Render Flags" )
+;;
+
+(defvar-local sideline--ex-window nil
+ "Holds previous window.")
+
+(defvar-local sideline--ex-window-start nil
+ "Holds previous window start point; detect vertical scrolling.")
+
+(defvar-local sideline--ex-window-hscroll nil
+ "Holds previous window hscroll; detect horizontal scrolling.")
+
+(defvar-local sideline--ex-window-width nil
+ "Holds previous window width; detect size change.")
+
+(defvar-local sideline--ex-window-height nil
+ "Holds previous window height; detect size change.")
+
+(defvar-local sideline--ex-face-remapping-alist nil
+ "Holds previous face remapping alist.")
+
+;;
+;; (@* "Externals" )
+;;
+
+(defvar overflow-newline-into-fringe)
+
+(declare-function buffer-text-pixel-size "src/xdisp.c")
+
+(declare-function shr-string-pixel-width "shr.el") ; TODO: remove this after 29.1
+
+(defun sideline--handle-externals ()
+ "Alter external packages to work with `sideline'."
+ (add-hook 'whitespace-mode-hook #'sideline-render-this)
+ ;; Tree-sitter
+ (add-hook 'ts-fold-on-fold-hook #'sideline-render-this)
+ (add-hook 'treesit-fold-on-fold-hook #'sideline-render-this))
+
+;;
+;; (@* "Entry" )
+;;
+
+(defun sideline--enable ()
+ "Enable `sideline' in current buffer."
+ (setq sideline-backends-left (cl-remove-if #'null sideline-backends-left)
+ sideline-backends-right (cl-remove-if #'null sideline-backends-right))
+ ;; XXX: Still don't know why local variable doesn't work!
+ (progn
+ (sideline--delete-ovs)
+ (setq-local sideline--overlays (make-hash-table)
+ sideline--backend-delay-timers (make-hash-table)))
+ (setq sideline--render-data-wapp (make-hash-table)
+ sideline--ex-bound-or-point t ; reset, render immediately
+ sideline--text-scale-mode-amount text-scale-mode-amount)
+ (add-hook 'post-command-hook #'sideline--post-command -90 t)
+ (add-hook 'before-revert-hook #'sideline--before-revert nil t)
+ (sideline--handle-externals)
+ ;; Render immediately after reopened file!
+ (sideline--post-command))
+
+(defun sideline--disable ()
+ "Disable `sideline' in current buffer."
+ (remove-hook 'post-command-hook #'sideline--post-command t)
+ (remove-hook 'before-revert-hook #'sideline--before-revert t)
+ (sideline--reset))
+
+;;;###autoload
+(define-minor-mode sideline-mode
+ "Minor mode `sideline-mode'."
+ :lighter " Sideline"
+ :group sideline
+ (if sideline-mode (sideline--enable) (sideline--disable)))
+
+(defun sideline--turn-on-sideline-mode ()
+ "Turn on the `sideline-mode'."
+ (sideline-mode 1))
+
+;;;###autoload
+(define-globalized-minor-mode global-sideline-mode
+ sideline-mode sideline--turn-on-sideline-mode
+ :require 'sideline)
+
+;;
+;; (@* "Util" )
+;;
+
+(defun sideline-2str (obj)
+ "Convert OBJ to string."
+ (format "%s" obj))
+
+;; Copied from s.el
+(defun sideline--s-replace (old new s)
+ "Replace OLD with NEW in S."
+ (declare (pure t) (side-effect-free t))
+ (replace-regexp-in-string (regexp-quote old) new s t t))
+
+(defmacro sideline--with-buffer-window (buffer-or-name &rest body)
+ "Execute the forms in BODY with BUFFER-OR-NAME temporarily current."
+ (declare (indent 1) (debug t))
+ `(when-let* (((buffer-live-p ,buffer-or-name))
+ (window (get-buffer-window ,buffer-or-name)))
+ (with-selected-window window
+ (with-current-buffer ,buffer-or-name ,@body))))
+
+(defun sideline--window-hscroll ()
+ "Like function `window-hscroll' but take more stuff into account."
+ (ceiling (* (window-hscroll)
+ (/ 1.0 (expt text-scale-mode-step
+ text-scale-mode-amount)))))
+
+;; TODO: Use function `string-pixel-width' after 29.1
+(defun sideline--string-pixel-width (str)
+ "Return the width of STR in pixels."
+ (cond ((fboundp #'buffer-text-pixel-size)
+ (let ((remapping-alist face-remapping-alist))
+ ;; Prevent use original buffer name for minimal side-effects
+ (with-current-buffer (get-buffer-create " *sideline-string-pixel-width*")
+ (setq-local display-line-numbers nil)
+ (delete-region (point-min) (point-max))
+ (setq-local face-remapping-alist remapping-alist)
+ (insert str)
+ (car (buffer-text-pixel-size nil nil t)))))
+ (t
+ (require 'shr)
+ (shr-string-pixel-width str))))
+
+(defun sideline--to-text-width (w)
+ "Convert pixel W to row/column width."
+ (let ((width (window-font-width)))
+ (+ (/ w width)
+ (if (zerop (% w width)) 0 1)))) ; add one if exceeed
+
+(defun sideline--str-len (str)
+ "Calculate STR in pixel width."
+ (sideline--to-text-width (sideline--string-pixel-width str)))
+
+(defun sideline--kill-timer (timer)
+ "Kill TIMER."
+ (when (timerp timer) (cancel-timer timer)))
+
+(defun sideline--column-to-point (column)
+ "Convert COLUMN to point."
+ (save-excursion (move-to-column (max column 0)) (point)))
+
+(defun sideline--modeline-height ()
+ "Return lines modeline cost."
+ (ceiling (/ (float (window-mode-line-height)) (frame-char-height))))
+
+(defun sideline--window-end ()
+ "Return the accurate window end position."
+ (save-excursion
+ (goto-char (window-end))
+ (forward-visible-line (- 0 (sideline--modeline-height)))
+ (line-beginning-position)))
+
+(defun sideline--window-width ()
+ "Correct window width for sideline."
+ (window-max-chars-per-line))
+
+(defun sideline--overlays-in (prop name &optional beg end)
+ "Return overlays with PROP of NAME, from region BEG to END."
+ (let* ((beg (or beg (point-min)))
+ (end (or end (point-max)))
+ (lst)
+ (ovs (overlays-in beg end)))
+ (dolist (ov ovs)
+ (when (eq name (overlay-get ov prop))
+ (push ov lst)))
+ lst))
+
+(defun sideline--overlays-in-line ()
+ "Return sideline overlays in line."
+ (sideline--overlays-in 'creator 'sideline (line-beginning-position) (line-end-position)))
+
+(defun sideline--opposing-str-len ()
+ "Return opposing overlay's content length."
+ (if-let* ((ov (car (sideline--overlays-in-line))) ; always return length of 1
+ (str (overlay-get ov 'before-string)))
+ (sideline--str-len str)
+ 0)) ; not found, then return 0
+
+(defun sideline--align-right (str offset)
+ "Align sideline STR from the right of the window.
+
+Argument OFFSET is additional calculation from the right alignment."
+ (let ((graphic-p (display-graphic-p))
+ (fringes (window-fringes)))
+ (list ; use pixel instead of character unit
+ (+ (sideline--string-pixel-width str)
+ (* (window-font-width)
+ (+ offset
+ ;; If the sideline text is displayed without at least 1 pixel gap from the right fringe and
+ ;; overflow-newline-into-fringe is not true, emacs will line wrap it.
+ (if (and graphic-p
+ (> (nth 1 fringes) 0)
+ (not overflow-newline-into-fringe))
+ 1
+ 0)
+ (if graphic-p
+ ;; If right fringe deactivated add 1 offset
+ (if (= 0 (nth 1 fringes)) 1 0)
+ 1)
+ (if (or
+ (bound-and-true-p whitespace-mode)
+ (bound-and-true-p global-whitespace-mode))
+ 1
+ 0)))))))
+
+(defun sideline--line-pixel-start ()
+ "Return the pixel start of the line."
+ (let* ((bol (line-beginning-position))
+ (left (car (save-excursion
+ (goto-char bol)
+ (or (ht-get sideline--render-data-wapp bol)
+ (let ((wapp (window-absolute-pixel-position)))
+ (ht-set sideline--render-data-wapp bol wapp)
+ wapp)))))
+ (left-edge (window-pixel-left)))
+ (- left left-edge)))
+
+(defun sideline--line-pixel-end ()
+ "Return the pixel end of the line."
+ (let* ((eol (line-end-position))
+ (left (car (save-excursion
+ (goto-char eol)
+ (or (ht-get sideline--render-data-wapp eol)
+ (let ((wapp (window-absolute-pixel-position)))
+ (ht-set sideline--render-data-wapp eol wapp)
+ wapp)))))
+ (left-edge (window-pixel-left)))
+ (- left left-edge)))
+
+(defun sideline--line-width ()
+ "Return the width of the line."
+ (let* ((start (sideline--line-pixel-start)) ; in pixel
+ (end (sideline--line-pixel-end)) ; in pixel
+ (len (- end start)))
+ (sideline--to-text-width len))) ; to text space
+
+(defun sideline--exceed-display-p ()
+ "Return non-nil if exceed display."
+ (or sideline-force-display-if-exceeds
+ sideline-truncate))
+
+(defun sideline--calc-space (str-len on-left opposing-str-len bol)
+ "Calculate space in current line.
+
+Argument STR-LEN is the string size. Another argument OPPOSING-STR-LEN is the
+string size already occupied.
+
+If argument ON-LEFT is non-nil, we calculate to the left side. Otherwise,
+calculate to the right side."
+ ;; XXX: We add up the `str-len' with `opposing-str-len' because they are using
+ ;; the same line!
+ ;;
+ ;; This is smart since we add up the string size before the calculation!
+ (setq str-len (+ str-len opposing-str-len))
+ ;; Start the calculation!
+ (when-let* ((win-width (sideline--render-data :win-width))
+ ((or (<= str-len win-width)
+ (sideline--exceed-display-p)))
+ (column-start (sideline--render-data :hscroll))
+ (pos-end (ignore-errors (sideline--line-width)))
+ (pos-end (max pos-end column-start)))
+ (cond
+ (on-left
+ (let* ((pos-first (save-excursion (back-to-indentation) (current-column)))
+ ;; The variable `pos-first' cannot be lower than the `hscroll'.
+ (pos-first (max pos-first column-start))
+ (remain-spaces (- pos-first column-start)))
+ (cond ((<= str-len remain-spaces)
+ (cons column-start pos-first))
+ ((= pos-first pos-end)
+ (cons column-start win-width))
+ ((and (sideline--exceed-display-p)
+ (< sideline--max-remain-spaces remain-spaces))
+ (setq sideline--max-remain-spaces remain-spaces
+ sideline--max-remain-spaces-line (cons bol
+ (cons column-start pos-first)))
+ nil)))) ; Ensure return `nil'.
+ (t
+ (let* ((column-end (+ column-start win-width))
+ (remain-spaces (- column-end pos-end)))
+ (cond ((<= str-len remain-spaces)
+ (cons column-end pos-end))
+ ((and (sideline--exceed-display-p)
+ (< sideline--max-remain-spaces remain-spaces))
+ (setq sideline--max-remain-spaces remain-spaces
+ sideline--max-remain-spaces-line (cons bol
+ (cons column-end pos-end)))
+ nil)))))))
+
+(defun sideline--find-line (str-len on-left &optional direction exceeded)
+ "Find a line where the string can be inserted.
+
+Argument STR-LEN is the length of the message, use to calculate the alignment.
+
+If argument ON-LEFT is non-nil, it will align to the left instead of right.
+
+See variable `sideline-order' document string for optional argument DIRECTION
+for details.
+
+Optional argument EXCEEDED is set to non-nil when we have already searched
+available lines in both directions (up & down)."
+ (let ((occupied-lines (if on-left sideline--occupied-lines-left
+ sideline--occupied-lines-right))
+ (going-up (eq direction 'up))
+ (skip-first t)
+ (break-it)
+ (data))
+ (save-excursion
+ (while (not break-it)
+ (if skip-first (setq skip-first nil)
+ (forward-visible-line (if going-up -1 1)))
+ ;; Reach window edge, means find nothing.
+ (unless break-it
+ (unless (if going-up
+ (<= (sideline--render-data :win-start) (point))
+ (<= (point) (sideline--render-data :win-end)))
+ (setq break-it t)))
+ ;; Reach buffer edge, means find nothing.
+ (unless break-it
+ (when (if going-up (bobp) (eobp))
+ (setq break-it t)))
+ (when-let*
+ ((occ-bol (line-beginning-position))
+ ((not (memq occ-bol occupied-lines)))
+ (col (or (sideline--calc-space str-len on-left
+ (sideline--opposing-str-len)
+ occ-bol)
+ ;; Handle exceed display.
+ (when (and exceeded ; after search on both sides.
+ break-it ; after reach edges.
+ sideline--max-remain-spaces-line)
+ ;; Apply to BOL
+ (setq occ-bol (car sideline--max-remain-spaces-line))
+ (goto-char occ-bol)
+ ;; Return column data.
+ (cdr sideline--max-remain-spaces-line))))
+ (pos-start (sideline--column-to-point (car col)))
+ (pos-end (sideline--column-to-point (cdr col)))
+ ((or on-left
+ ;; Skip virtual line from `truncate-lines'.
+ (= pos-start pos-end))))
+ (setq data (list pos-start pos-end occ-bol))
+ (setq break-it t)
+ (push occ-bol occupied-lines))))
+ (if on-left
+ (setq sideline--occupied-lines-left occupied-lines)
+ (setq sideline--occupied-lines-right occupied-lines))
+ (or data
+ (and (not exceeded)
+ (sideline--find-line str-len on-left (if going-up 'down 'up) t)))))
+
+(defun sideline--create-keymap (action candidate)
+ "Create keymap for sideline ACTION.
+
+Argument CANDIDATE is the data for users."
+ (let ((map (make-sparse-keymap)))
+ (define-key map [down-mouse-1]
+ (lambda ()
+ (interactive)
+ (funcall action candidate)))
+ map))
+
+;;
+;; (@* "Overlays" )
+;;
+
+(defun sideline-backend-ovs (backend)
+ "Return overlays for BACKEND."
+ (sideline--overlays-in 'backend backend))
+
+(defun sideline-delete-backend-ovs (backend)
+ "Delete overlays from BACKEND."
+ (dolist (ov (ht-get sideline--overlays backend))
+ (let ((on-left (overlay-get ov 'left))
+ (occ-pt (overlay-get ov 'occ-pt)))
+ (if on-left
+ (setq sideline--occupied-lines-left
+ (delete occ-pt sideline--occupied-lines-left))
+ (setq sideline--occupied-lines-right
+ (delete occ-pt sideline--occupied-lines-right))))
+ (delete-overlay ov))
+ (ht-set sideline--overlays backend nil))
+
+(defun sideline--reset-occupied-lines ()
+ "Reset occupied lines."
+ (let ((mark (list (line-beginning-position))))
+ (setq sideline--occupied-lines-left
+ (if sideline-backends-left-skip-current-line mark nil))
+ (setq sideline--occupied-lines-right
+ (if sideline-backends-right-skip-current-line mark nil))))
+
+(defun sideline--delete-ovs ()
+ "Clean up all overlays."
+ (sideline--reset-occupied-lines)
+ (ht-map (lambda (_key value)
+ (mapc #'delete-overlay value))
+ sideline--overlays)
+ (ht-clear sideline--overlays)
+ (ht-clear sideline--render-data-wapp))
+
+(defun sideline--display-string (on-left backend-str candidate &optional type)
+ "Return the display string to render the text correctly.
+
+Argument ON-LEFT is used to calculate the output string.
+
+Arguments BACKEND-STR and CANDIDATE are used to string concatenation, it
+produces the result string.
+
+Optional argument TYPE is used for recursive `outer' and `inner'."
+ (cl-case (or type sideline-display-backend-type)
+ (`left (concat backend-str " " candidate))
+ (`right (concat candidate " " backend-str))
+ (`inner (sideline--display-string on-left backend-str candidate (if on-left 'right 'left)))
+ (`outer (sideline--display-string on-left backend-str candidate (if on-left 'left 'right)))))
+
+(defun sideline--display-starting (on-left backend-str &optional type)
+ "Return the starting text position to render the text correctly.
+
+Argument ON-LEFT is used to calculate the starting text position..
+
+Argument BACKEND-STR is used to calculate the starting text position.
+
+Optional argument TYPE is used for recursive `outer' and `inner'."
+ (cl-case (or type sideline-display-backend-type)
+ (`left (1+ (length backend-str)))
+ (`right 0)
+ (`inner (sideline--display-starting on-left backend-str (if on-left 'right 'left)))
+ (`outer (sideline--display-starting on-left backend-str (if on-left 'left 'right)))))
+
+(defun sideline--create-ov (backend candidate action face name on-left order)
+ "Create information (CANDIDATE) overlay.
+
+Argument BACKEND is used to categorize overlays.
+
+See function `sideline--render-candidates' document string for arguments ACTION,
+FACE, NAME, ON-LEFT, and ORDER for details."
+ (when-let*
+ ((backend-str (format sideline-display-backend-format name))
+ (text (if sideline-display-backend-name ; this is the displayed text
+ (progn
+ (add-face-text-property 0 (length backend-str) 'sideline-backend nil backend-str)
+ (sideline--display-string on-left backend-str candidate))
+ candidate))
+ (len-text (length text))
+ (len-cand (length candidate))
+ (title
+ (progn
+ (unless (get-text-property 0 'face candidate) ; If no face, we apply one
+ (let ((start (if sideline-display-backend-name
+ (sideline--display-starting on-left backend-str)
+ 0)))
+ (add-face-text-property start (+ start len-cand) face nil text)))
+ (when action ; apply action listener
+ (let ((keymap (sideline--create-keymap action candidate)))
+ (add-text-properties 0 len-text `(keymap ,keymap mouse-face highlight) text)))
+ (if on-left (format sideline-format-left text)
+ (format sideline-format-right text))))
+ (len-title (sideline--str-len title))
+ (data (let ((sideline--max-remain-spaces 0) ; Reset before use.
+ (sideline--max-remain-spaces-line nil)) ; Reset before use.
+ (sideline--find-line len-title on-left order)))
+ (pos-start (nth 0 data)) (pos-end (nth 1 data)) (occ-pt (nth 2 data))
+ (hscroll (sideline--render-data :hscroll))
+ (offset (- 0 hscroll))
+ ;; Truncate
+ (title
+ (or (and sideline-truncate
+ (let* ((win-width (sideline--render-data :win-width))
+ (end-col (save-excursion
+ (goto-char pos-end)
+ (current-column)))
+ (available-space (if on-left
+ (if (zerop end-col)
+ win-width
+ (- end-col hscroll 1))
+ (- win-width (- end-col hscroll))))
+ (suffix (copy-sequence (truncate-string-ellipsis))))
+ (when (< available-space len-title)
+ (set-text-properties 0 (length suffix)
+ (text-properties-at (1- (length title)) title)
+ suffix)
+ (truncate-string-to-width title available-space 0 nil
+ suffix))))
+ title))
+ ;; Align left/right
+ (str (concat
+ (unless on-left
+ (propertize " "
+ 'display
+ `((space :align-to (- right ,(sideline--align-right title offset)))
+ (space :width 0))
+ `cursor t))
+ title)))
+
+ ;; Create overlay
+ (let* ((len-str (length str))
+ (left-empty-ln (= pos-start pos-end))
+ (ov (make-overlay pos-start
+ (if left-empty-ln pos-start
+ ;; Cannot exceed `pos-end'.
+ (min pos-end (+ pos-start len-str)))
+ nil t t)))
+ (cond (on-left
+ (if left-empty-ln
+ (overlay-put ov 'before-string (concat (spaces-string hscroll) str))
+ (overlay-put ov 'display str)
+ (overlay-put ov 'invisible t)))
+ (t (overlay-put ov 'before-string str)))
+ (overlay-put ov 'window (selected-window))
+ (overlay-put ov 'priority (if on-left sideline-priority
+ ;; Add 1 to render on the same line!
+ (1+ sideline-priority)))
+ (overlay-put ov 'creator 'sideline)
+ (overlay-put ov 'backend backend)
+ (overlay-put ov 'on-left on-left)
+ (overlay-put ov 'occ-pt occ-pt)
+ (unless (gethash backend sideline--overlays)
+ (setf (gethash backend sideline--overlays) nil))
+ (push ov (gethash backend sideline--overlays)))))
+
+;;
+;; (@* "Async" )
+;;
+
+(defun sideline--guess-backend-name (backend)
+ "Guess BACKEND's name."
+ (let ((name (sideline-2str backend)))
+ (setq name (sideline--s-replace "sideline-" "" name)
+ name (sideline--s-replace "-sideline" "" name))
+ name))
+
+(defun sideline--render-candidates (candidates backend on-left order)
+ "Render a list of backends (CANDIDATES).
+
+Argument BACKEND is the backend symbol.
+
+Argument ON-LEFT is a flag indicates rendering alignment; if it's non-nil then
+we align to the left, otherwise to the right.
+
+Argument ORDER determined the search order for going up or down."
+ (let ((inhibit-field-text-motion t)
+ (action (sideline--call-backend backend 'action))
+ (face (or (sideline--call-backend backend 'face) 'sideline-default))
+ (name (or (sideline--call-backend backend 'name)
+ (sideline--guess-backend-name backend))))
+ (dolist (candidate candidates)
+ (sideline--create-ov backend candidate action face name on-left order))))
+
+;;
+;; (@* "Core" )
+;;
+
+(defvar-local sideline--backend-delay-timers nil
+ "Delay timer per backend.")
+
+(defun sideline--call-backend (backend command)
+ "Return BACKEND's result with COMMAND."
+ (funcall backend command))
+
+(defun sideline--render-backend (backend on-left order)
+ "Render for single BACKEND.
+
+All arguments BACKEND, ON-LEFT and ORDER are extracted from the function
+`sideline--render-backends'."
+ (when-let* ((candidates (sideline--call-backend backend 'candidates))
+ (buffer (current-buffer))) ; for async check
+ (if (eq (car candidates) :async)
+ (funcall (cdr candidates)
+ (lambda (cands &rest _)
+ (sideline--with-buffer-window buffer
+ (when sideline-mode
+ ;; XXX: The bug occurs when you have two or more indentical
+ ;; windows (same buffer and same cursor position) and
+ ;; frequently change between different buffers.
+ ;;
+ ;; To solve this, just record the "real" rendering
+ ;; buffer in async. So when you switch back to the same
+ ;; buffer, the sideline can adjust to find the correct
+ ;; buffer in the function `sideline--do-render-p' since
+ ;; it has been flag here.
+ (setq sideline--ex-window (selected-window))
+ (sideline--render-candidates cands backend on-left order)))))
+ (sideline--render-candidates candidates backend on-left order))))
+
+(defun sideline--render-backends (backends on-left)
+ "Render a list of BACKENDS.
+
+If argument ON-LEFT is non-nil, it will align to the left
+instead of right."
+ (dolist (data backends)
+ (let ((is-cons (consp data)))
+ (when-let* ((backend (if is-cons (car data) data))
+ (backend (if (symbolp backend) backend
+ (eval backend)))
+ (order (if is-cons (cdr data) ; configured
+ ;; fallback to default
+ (if on-left sideline-order-left sideline-order-right))))
+ ;; Handle backend delay here.
+ (if-let* ((delay (alist-get backend sideline-backend-delays)))
+ (progn
+ (sideline--kill-timer (ht-get sideline--backend-delay-timers backend))
+ (ht-set sideline--backend-delay-timers
+ backend
+ (run-with-idle-timer delay nil #'sideline--render-backend
+ backend on-left order)))
+ (sideline--render-backend backend on-left order))))))
+
+(defun sideline-stop-p ()
+ "Return non-nil if the sideline should not be display."
+ (or (region-active-p)
+ (bound-and-true-p company-pseudo-tooltip-overlay)
+ (bound-and-true-p lsp-ui-peek--overlay)))
+
+(defun sideline--render-data (prop)
+ "Get render data by PROP."
+ (plist-get sideline--render-data prop))
+
+(defun sideline--render-data-update (&rest _)
+ "Update the render data once."
+ (setq sideline--render-data
+ `( :win-end ,(sideline--window-end)
+ :win-start ,(window-start)
+ :hscroll ,(sideline--window-hscroll)
+ :win-width ,(sideline--window-width))))
+
+(defun sideline-render (&optional buffer)
+ "Render sideline once in the BUFFER of WINDOW."
+ (sideline--with-buffer-window (or buffer (current-buffer))
+ (unless (funcall sideline-inhibit-display-function)
+ (sideline--render-data-update)
+ (run-hooks 'sideline-pre-render-hook)
+ (sideline--render-backends sideline-backends-left t)
+ (sideline--render-backends sideline-backends-right nil)
+ (run-hooks 'sideline-post-render-hook))))
+
+(defun sideline--do-render-p ()
+ "Return non-nil if we should re-render sidelines in the post-command."
+ (let ((bound-or-point (or (bounds-of-thing-at-point 'symbol)
+ (point)))
+ (window (selected-window))
+ (win-start (window-start))
+ (win-hscroll (window-hscroll))
+ (win-width (window-width))
+ (win-height (window-height))
+ (remapping-alist face-remapping-alist))
+ (when ; conditions allow to re-render sidelines
+ (or (not (equal sideline--ex-bound-or-point bound-or-point))
+ (not (equal sideline--text-scale-mode-amount text-scale-mode-amount))
+ (not (equal sideline--ex-window window))
+ (not (equal sideline--ex-window-start win-start))
+ (not (equal sideline--ex-window-hscroll win-hscroll))
+ (not (equal sideline--ex-window-width win-width))
+ (not (equal sideline--ex-window-height win-height))
+ (not (equal sideline--ex-face-remapping-alist remapping-alist))
+ sideline-render-this-command)
+ ;; update
+ (setq sideline--ex-bound-or-point bound-or-point
+ sideline--text-scale-mode-amount text-scale-mode-amount
+ sideline--ex-window window
+ sideline--ex-window-start win-start
+ sideline--ex-window-hscroll win-hscroll
+ sideline--ex-window-width win-width
+ sideline--ex-window-height win-height
+ sideline--ex-face-remapping-alist remapping-alist
+ sideline-render-this-command nil)
+ t)))
+
+(defun sideline--before-revert (&rest _)
+ "Before revert."
+ (sideline--reset))
+
+(defvar-local sideline--delay-timer nil
+ "Delay timer to render sideline.")
+
+(defun sideline--post-command ()
+ "Post command."
+ (when (sideline--do-render-p)
+ (sideline--delete-ovs)
+ (sideline--kill-timer sideline--delay-timer)
+ (setq sideline--delay-timer
+ (run-with-idle-timer sideline-delay nil #'sideline-render
+ (current-buffer)))
+ ;; XXX: Reset hook can potentially re-render sideline; update the render
+ ;; data once to ensure display's correctioness.
+ (sideline--render-data-update)
+ (run-hooks 'sideline-reset-hook)))
+
+;;;###autoload
+(defun sideline-render-this (&rest _)
+ "Use to force render the next post command."
+ (setq sideline-render-this-command t))
+
+(defun sideline--reset ()
+ "Clean up for next use."
+ (setq sideline--ex-bound-or-point nil)
+ (sideline--delete-ovs))
+
+(provide 'sideline)
+;;; sideline.el ends here