diff options
| author | Jack Jamison <jackqjamison@gmail.com> | 2026-07-05 01:19:30 -0400 |
|---|---|---|
| committer | Jack Jamison <jackqjamison@gmail.com> | 2026-07-05 01:19:30 -0400 |
| commit | bdf9a71ab7baa2b1de9abcfd5df1a9107a55d141 (patch) | |
| tree | c4524e6c41aa5107211103401dabfaefc81aa882 /.config/emacs/lisp/sideline.el | |
| parent | fe3984f541bd32bdfa418afb305b614176b55ca0 (diff) | |
add a bunch of emacs packages HELP
Diffstat (limited to '.config/emacs/lisp/sideline.el')
| -rw-r--r-- | .config/emacs/lisp/sideline.el | 908 |
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 |
