💥 Replace core-popup with new feature/popup module

This is a breaking change! Update your :popup settings. Old ones will
throw errors!

Doom's new popup management system casts off its shackles (hur hur) and
replaces them with the monster that is `display-buffer-alist`, and
window parameters.

However, this is highly experimental! Expect edge cases.  Particularly
with org-mode and magit (or anything that does its own window
management).

Relevant to #261, #263, #325
This commit is contained in:
Henrik Lissner 2018-01-06 01:23:22 -05:00
parent 065091bdca
commit 91357a3e5d
No known key found for this signature in database
GPG key ID: 5F6C0EA160557395
33 changed files with 631 additions and 1038 deletions

View file

@ -0,0 +1,49 @@
#+TITLE: :feature popup
A short summary about what this module does.
If necessary, include a longer description below it that goes into more detail. This may be as long as you like.
+ If possible, include a list of features
+ Include links to major plugins that the module uses, if applicable
+ Use links whenever you can
+ Mention dependencies on other modules here
* Table of Contents :TOC:
- [[#install][Install]]
- [[#main-dependencies][Main dependencies]]
- [[#extra-dependencies][Extra Dependencies]]
- [[#configuration][Configuration]]
- [[#usage][Usage]]
- [[#appendix][Appendix]]
- [[#commands][Commands]]
- [[#hacks][Hacks]]
* Install
** Main dependencies
*** MacOS
#+BEGIN_SRC sh :tangle (if (doom-system-os 'macos) "yes")
brew install x
#+END_SRC
*** Arch Linux
#+BEGIN_SRC sh :dir /sudo:: :tangle (if (doom-system-os 'arch) "yes")
sudo pacman --needed --noconfirm -S X
#+END_SRC
** Extra Dependencies
+ A
+ B
+ C
#+BEGIN_SRC sh
Y install A B C
#+END_SRC
* Configuration
* Usage
* Appendix
** Commands
** Hacks

View file

@ -0,0 +1,328 @@
;;; feature/popup/autoload.el -*- lexical-binding: t; -*-
(defun +popup--cancel-buffer-timer ()
"Cancel the current buffer's transient timer."
(when (timerp +popup--timer)
(message "Cancelled timer")
(cancel-timer +popup--timer)
(setq +popup--timer nil))
t)
(defun +popup--remember (windows)
"Remember WINDOWS (a list of windows) for later restoration."
(cl-assert (cl-every #'windowp windows) t)
(setq +popup--last
(cl-loop for w in windows
collect (list (window-buffer w)
(window-parameter w 'alist)
(window-state-get w)))))
(defun +popup--kill-buffer (buffer)
"Tries to kill BUFFER, as was requested by a transient timer. If it fails, eg.
the buffer is visible, then set another timer and try again later."
(when (buffer-live-p buffer)
(if (get-buffer-window buffer)
(with-current-buffer buffer
(setq +popup--timer
(run-at-time (timer--time +popup--timer)
nil #'+popup--kill-buffer buffer)))
(with-demoted-errors "Error killing transient buffer: %s"
(let ((inhibit-message (not doom-debug-mode)))
(message "Cleaned up transient buffer: %s" buffer))
(kill-buffer buffer)))))
(defun +popup--init (window alist)
"Initializes a popup window. Run any time a popup is opened. It sets the
default window parameters for popup windows, clears leftover transient timers
and enables `+popup-buffer-mode'."
(with-selected-window window
(set-window-parameter window 'no-other-window t)
(set-window-parameter window 'delete-window #'+popup--destroy)
(set-window-parameter window 'alist alist)
(window-preserve-size
window (memq (window-parameter window 'window-side) '(left right)) t)
(+popup--cancel-buffer-timer)
(+popup-buffer-mode +1)))
(defun +popup--destroy (window)
"Do housekeeping before destroying a popup window.
+ Disables `+popup-buffer-mode' so that any hooks attached to it get a chance to
run and do cleanup of its own.
+ Either kills the buffer or sets a transient timer, if the window has a
`transient' window parameter (see `+popup-window-parameters').
+ And finally deletes the window!"
(let ((ttl (+popup-parameter 'transient window))
(buffer (window-buffer window)))
(let ((ignore-window-parameters t))
(delete-window window))
(unless (window-live-p window)
(with-current-buffer buffer
(+popup-buffer-mode -1)
;; t = default
;; integer = ttl
;; nil = no timer
(when ttl
(when (eq ttl t)
(setq ttl +popup-ttl))
(cl-assert (integerp ttl) t)
(if (= ttl 0)
(+popup--kill-buffer buffer)
(setq +popup--timer
(run-at-time ttl nil #'+popup--kill-buffer buffer))))))))
(defun +popup--normalize-alist (alist)
"Merge `+popup-default-alist' and `+popup-default-parameters' with ALIST."
(if (not alist)
(setq alist +popup-default-alist)
(let* ((alist (map-merge 'list +popup-default-alist alist))
(params (map-merge 'list
+popup-default-parameters
(cdr (assq 'window-parameters alist)))))
(setq alist (assq-delete-all 'window-parameters alist))
(push (cons 'window-parameters params) alist)
(nreverse alist))))
;;
;; Public library
;;
;;;###autoload
(defun +popup-p (&optional target)
"Return t if TARGET is a popup window or buffer. If TARGET is nil, use the
current buffer."
(unless target
(setq target (current-buffer)))
(cond ((windowp target)
(+popup-p (window-buffer target)))
((bufferp target)
(buffer-local-value '+popup-buffer-mode target))
(t
(error "Expected a window/buffer, got %s (%s)"
(type-of target) target))))
;;;###autoload
(defun +popup-buffer (buffer &optional alist)
"Open BUFFER in a popup window. ALIST describes its features."
(let* ((old-window (selected-window))
(alist (+popup--normalize-alist alist))
(new-window (or (display-buffer-reuse-window buffer alist)
(display-buffer-in-side-window buffer alist))))
(+popup--init new-window alist)
(select-window
(if (+popup-parameter 'select new-window)
new-window
old-window))
new-window))
;;;###autoload
(defun +popup-parameter (parameter &optional window)
"Fetch the window parameter of WINDOW"
(window-parameter (or window (selected-window)) parameter))
;;;###autoload
(defun +popup-windows ()
"Returns a list of all popup windows."
(cl-remove-if-not #'+popup-p (window-list)))
;;
;; Minor mode
;;
;;;###autoload
(define-minor-mode +popup-mode
"Global minor mode for popups."
:init-value nil
:global t
:keymap +popup-mode-map
(cond (+popup-mode
(add-hook 'doom-unreal-buffer-functions #'+popup-p)
(add-hook '+evil-esc-hook #'+popup|close-on-escape t)
(setq +popup--old-display-buffer-alist display-buffer-alist
display-buffer-alist +popup--display-buffer-alist)
(dolist (prop +popup-window-parameters)
(push (cons prop 'writeable) window-persistent-parameters)))
(t
(remove-hook 'doom-unreal-buffer-functions #'+popup-p)
(remove-hook '+evil-esc-hook #'+popup|close-on-escape)
(setq display-buffer-alist +popup--old-display-buffer-alist)
(dolist (prop +popup-window-parameters)
(assq-delete-all prop window-persistent-parameters)))))
;;;###autoload
(define-minor-mode +popup-buffer-mode
"Minor mode for popup windows."
:init-value nil
:keymap +popup-buffer-mode-map)
;;
;; Hooks
;;
;;;###autoload
(defun +popup|adjust-fringes ()
"Hides the fringe in popup windows, restoring them if `+popup-buffer-mode' is
disabled."
(let ((f (if +popup-buffer-mode 0 doom-fringe-size)))
(set-window-fringes nil f f fringes-outside-margins)))
;;;###autoload
(defun +popup|set-modeline ()
"Don't show modeline in popup windows without a `modeline' window-parameter.
+ If one exists and it's a symbol, use `doom-modeline' to grab the format.
+ If non-nil, show the mode-line as normal.
+ If nil (or omitted), then hide the modeline entirely (the default)."
(if +popup-buffer-mode
(let ((modeline (+popup-parameter 'modeline)))
(cond ((or (eq modeline 'nil)
(not modeline))
(doom-hide-modeline-mode +1))
((and (symbolp modeline)
(not (eq modeline 't)))
(setq-local doom--modeline-format (doom-modeline modeline))
(when doom--modeline-format
(doom-hide-modeline-mode +1)))))
(when doom-hide-modeline-mode
(doom-hide-modeline-mode -1))))
;;;###autoload
(defun +popup|close-on-escape ()
"If called inside a popup, try to close that popup window (see
`+popup/close'). If called outside, try to close all popup windows (see
`+popup/close-all')."
(call-interactively
(if (+popup-p)
#'+popup/close
#'+popup/close-all)))
;;
;; Commands
;;
;;;###autoload
(defalias 'other-popup #'+popup/other)
;;;###autoload
(defun +popup/other ()
"Cycle through popup windows, like `other-window'. Ignores regular windows."
(interactive)
(let ((popups (+popup-windows))
(window (selected-window)))
(unless popups
(user-error "No popups are open"))
(select-window (if (+popup-p)
(or (car-safe (cdr (memq window popups)))
(car (delq window popups))
(car popups))
(car popups)))))
;;;###autoload
(defun +popup/close (&optional window force-p)
"Close WINDOW, if it's a popup window.
This will do nothing if the popup's `escape-quit' window parameter is either nil
or 'other. This window parameter is ignored if FORCE-P is non-nil."
(interactive
(list (selected-window)
current-prefix-arg))
(unless window
(setq window (selected-window)))
(when (and (+popup-p window)
(or force-p
(memq (+popup-parameter 'escape-quit window)
'(t current))))
(when +popup--remember-last
(+popup--remember (list window)))
(delete-window window)
t))
;;;###autoload
(defun +popup/close-all (&optional force-p)
"Close all open popup windows.
This will ignore popups with an `escape-quit' parameter that is either nil or
'current. This window parameter is ignored if FORCE-P is non-nil."
(interactive "P")
(let (targets +popup--remember-last)
(dolist (window (+popup-windows))
(when (or force-p
(memq (+popup-parameter 'escape-quit window)
'(t other)))
(push window targets)))
(when targets
(+popup--remember targets)
(mapc #'delete-window targets)
t)))
;;;###autoload
(defun +popup/toggle ()
"If popups are open, close them. If they aren't, restore the last one or open
the message buffer in a popup window."
(interactive)
(cond ((+popup-windows)
(+popup/close-all))
((ignore-errors (+popup/restore)))
((display-buffer (get-buffer "*Messages*")))))
;;;###autoload
(defun +popup/restore ()
"Restore the last popups that were closed, if any."
(interactive)
(unless +popup--last
(error "No popups to restore"))
(cl-loop for (buffer alist state) in +popup--last
if (and (buffer-live-p buffer)
(+popup-buffer buffer alist))
do (window-state-put state it))
(setq +popup--last nil))
;;;###autoload
(defun +popup/raise ()
"Raise the current popup window into a regular window."
(interactive)
(unless (+popup-p)
(user-error "Cannot raise a non-popup window"))
(let ((window (selected-window))
(buffer (current-buffer))
+popup--remember-last)
(set-window-parameter window 'transient nil)
(+popup/close window 'force)
(display-buffer-pop-up-window buffer nil)))
;;
;; Macros
;;
;;;###autoload
(defmacro without-popups! (&rest body)
"Run BODY with a default `display-buffer-alist', ignoring the popup rules set
with the :popup setting."
`(let ((display-buffer-alist +popup--old-display-buffer-alist))
,@body))
;;;###autoload
(defmacro save-popups! (&rest body)
"Sets aside all popups before executing the original function, usually to
prevent the popup(s) from messing up the UI (or vice versa)."
`(let* ((in-popup-p (+popup-p))
(popups (+popup-windows))
(popup-states
(cl-loop for p in popups
collect (cons (window-buffer p) (window-state-get p))))
+popup--last)
(dolist (p popups)
(+popup/close p 'force))
(unwind-protect
(progn ,@body)
(when popups
(let ((origin (selected-window)))
(+popup/restore)
(unless in-popup-p
(select-window origin)))))))

View file

@ -0,0 +1,196 @@
;;; config.el -*- lexical-binding: t; -*-
(defconst +popup-window-parameters
'(transient escape-quit select modeline alist)
"A list of custom parameters to be added to `window-persistent-parameters'.
Modifying this has no effect, unless done before feature/popup loads.
(transient . CDR)
CDR can be t, an integer or nil. It represents the number of seconds before
the buffer belonging to a closed popup window is killed.
If t, CDR will default to `+popup-ttl'.
If 0, the buffer is immediately killed.
If nil, the buffer won't be killed.
(escape-quit . CDR)
CDR can be t, 'other, 'current or nil. This determines the behavior of the
escape key in or outside of popup windows.
If t, close the popup if escape is pressed inside or outside of popups.
If 'other, close this popup if escape is pressed outside of any popup. This is
great for popups you just want to peek at and discard.
If 'current, close the current popup if escape is pressed from inside of
the popup.
If nil, pressing escape will never close this buffer.
(select . BOOl)
CDR is a boolean that determines whether to focus the popup window after it
opens.
(modeline . CDR)
CDR can be t (show the default modeline), a symbol representing the name of a
modeline defined with `def-modeline!', or nil (show no modeline).
(alist . CDR)
This is an internal parameter and should not be set or modified.
Since I can't find this information anywhere but the Emacs manual, I'll include
a brief description of some native window parameters that Emacs uses:
(delete-window . CDR)
(delete-other-window . CDR)
(split-window . CDR)
(other-window . CDR)
This applies to all four of the above: CDR can be t or a function. If t, using
those functions on this window will ignore all window parameters.
If CDR is a function, it will replace the native function when used on this
window. e.g. if CDR is #'ignore (delete-window popup) will run (ignore popup)
instead of deleting the window!
(no-other-window . BOOL)
If CDR is non-nil, this window becomes invisible to `other-window' and
`pop-to-buffer'. Doom popups sets this. The default is nil.")
(defvar +popup-default-alist
'((slot . 1)
(window-height . 0.14)
(window-width . 26)
(reusable-frames . visible))
"The default alist for `display-buffer-alist' rules.")
(defvar +popup-default-parameters
'((transient . t)
(escape-quit . t))
"The default window parameters to add alists fed to `display-buffer-alist'.")
(defvar +popup-ttl 10
"The default time-to-live for transient buffers whose popup buffers have been
deleted.")
(defvar +popup-mode-map (make-sparse-keymap)
"Active keymap in a session with the popup system enabled. See
`+popup-mode'.")
(defvar +popup-buffer-mode-map (make-sparse-keymap)
"Active keymap in popup windows. See `+popup-buffer-mode'.")
(defvar +popup--display-buffer-alist nil)
(defvar +popup--old-display-buffer-alist nil)
(defvar +popup--remember-last t)
(defvar +popup--last nil)
(defvar-local +popup--timer nil)
;;
(def-setting! :popup (condition &optional alist parameters)
"Register a popup rule.
CONDITION can be a regexp string or a function. See `display-buffer' for a list
of possible entries for ALIST, which tells the display system how to initialize
the popup window. PARAMETERS is an alist of window parameters. See
`+popup-window-parameters' for a list of custom parameters provided by the popup
module."
`(let ((alist ,alist)
(parameters ,parameters))
,(when alist
'(when-let* ((size (cdr (assq 'size alist))))
(setq alist (assq-delete-all 'size alist))
(push (cons (pcase (cdr (or (assq 'side alist)
(assq 'side +popup-default-alist)))
((or `left `right) 'window-width)
(_ 'window-height))
size)
alist)))
(prog1 (push (append (list ,condition '(+popup-buffer))
alist
(list (cons 'window-parameters parameters)))
+popup--display-buffer-alist)
(when (bound-and-true-p +popup-mode)
(setq display-buffer-alist +popup--display-buffer-alist)))))
;;
;; Default popup rules & bootstrap
;;
(eval-when-compile
(set! :popup "^ \\*")
(set! :popup "^\\*" nil '((select . t)))
(set! :popup "^\\*\\(?:scratch\\|Messages\\)" nil '((transient)))
(set! :popup "^\\*Help"
'((window-height . 0.2))
'((select . t)))
(set! :+popup "^\\*doom:"
'((window-height . 0.35))
'((select . t) (escape-quit) (transient))))
(setq +popup--display-buffer-alist (eval-when-compile +popup--display-buffer-alist))
(add-hook 'doom-init-ui-hook #'+popup-mode)
(add-hook '+popup-buffer-mode-hook #'+popup|adjust-fringes)
(add-hook '+popup-buffer-mode-hook #'+popup|set-modeline)
;;
;; Hacks
;;
(defun doom*ignore-window-parameters (orig-fn &rest args)
"Allow *interactive* window moving commands to traverse popups."
(cl-letf (((symbol-function #'windmove-find-other-window)
(lambda (direction &optional window ignore sign wrap mini)
(window-in-direction
(pcase dir (`up 'above) (`down 'below) (_ dir))
window (bound-and-true-p +popup-mode) arg windmove-wrap-around t))))
(apply orig-fn args)))
(advice-add #'windmove-up :around #'doom*ignore-window-parameters)
(advice-add #'windmove-down :around #'doom*ignore-window-parameters)
(advice-add #'windmove-left :around #'doom*ignore-window-parameters)
(advice-add #'windmove-right :around #'doom*ignore-window-parameters)
(after! help-mode
(defun doom--switch-from-popup (location)
(let (origin)
(save-popups!
(switch-to-buffer (car location) nil t)
(if (not (cdr location))
(message "Unable to find location in file")
(goto-char (cdr location))
(recenter)
(setq origin (selected-window))))
(+popup/close)
(select-window origin)))
;; Help buffers use `pop-to-window' to decide where to open followed links,
;; which can be unpredictable. It should *only* replace the original buffer we
;; opened the popup from. To fix this these three button types need to be
;; redefined to set aside the popup before following a link.
(define-button-type 'help-function-def
:supertype 'help-xref
'help-function
(lambda (fun file)
(require 'find-func)
(when (eq file 'C-source)
(setq file (help-C-file-name (indirect-function fun) 'fun)))
(doom--switch-from-popup (find-function-search-for-symbol fun nil file))))
(define-button-type 'help-variable-def
:supertype 'help-xref
'help-function
(lambda (var &optional file)
(when (eq file 'C-source)
(setq file (help-C-file-name var 'var)))
(doom--switch-from-popup (find-variable-noselect var file))))
(define-button-type 'help-face-def
:supertype 'help-xref
'help-function
(lambda (fun file)
(require 'find-func)
(doom--switch-from-popup (find-function-search-for-symbol fun 'defface file)))))
(provide 'config)
;;; config.el ends here