9da3ffee41
This is a massive diff that I had to do in a hurry - when leaving Urbint. I'm pretty sure that most of these are updating Emacs packages, but I'm not positive.
461 lines
18 KiB
EmacsLisp
461 lines
18 KiB
EmacsLisp
;;; sesman-browser.el --- Interactive Browser for Sesman -*- lexical-binding: t -*-
|
||
;;
|
||
;; Copyright (C) 2018, Vitalie Spinu
|
||
;; Author: Vitalie Spinu
|
||
;; URL: https://github.com/vspinu/sesman
|
||
;;
|
||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||
;;
|
||
;; 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, 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; see the file COPYING. If not, write to
|
||
;; the Free Software Foundation, Inc., 51 Franklin Street, Fifth
|
||
;; Floor, Boston, MA 02110-1301, USA.
|
||
;;
|
||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||
;;
|
||
;;; Commentary:
|
||
;;
|
||
;; Interactive session browser.
|
||
;;
|
||
;;; Code:
|
||
|
||
(require 'seq)
|
||
(require 'sesman)
|
||
|
||
(defgroup sesman-browser nil
|
||
"Browser for Sesman."
|
||
:prefix "sesman-browser-"
|
||
:group 'sesman
|
||
:link '(url-link :tag "GitHub" "https://github.com/vspinu/sesman"))
|
||
|
||
(defface sesman-browser-highligh-face
|
||
'((default (:inherit highlight :weight bold)))
|
||
"Face used to highlight currently selected button."
|
||
:group 'sesman-browser)
|
||
|
||
(defface sesman-browser-button-face
|
||
'((default (:inherit button :slant italic)))
|
||
"Face used to highlight currently selected object."
|
||
:group 'sesman-browser)
|
||
|
||
(defvar-local sesman-browser--sort-types '(name relevance))
|
||
(defcustom sesman-browser-sort-type 'name
|
||
"Default sorting type in sesman browser buffers.
|
||
Currently can be either 'name or 'relevance."
|
||
:type '(choice (const name) (const relevance))
|
||
:group 'sesman-browser)
|
||
|
||
(defvar sesman-browser-map
|
||
(let (sesman-browser-map)
|
||
(define-prefix-command 'sesman-browser-map)
|
||
(define-key sesman-browser-map (kbd "r") #'sesman-browser-restart-session)
|
||
(define-key sesman-browser-map (kbd "q") #'sesman-browser-quit-session)
|
||
(define-key sesman-browser-map (kbd "b") #'sesman-browser-link-with-buffer)
|
||
(define-key sesman-browser-map (kbd "d") #'sesman-browser-link-with-directory)
|
||
(define-key sesman-browser-map (kbd "p") #'sesman-browser-link-with-project)
|
||
(define-key sesman-browser-map (kbd "u") #'sesman-browser-unlink)
|
||
sesman-browser-map)
|
||
"Prefix keymap for sesman commands from sesman browser.")
|
||
|
||
(defvar sesman-browser-mode-map
|
||
(let ((sesman-browser-mode-map (make-sparse-keymap)))
|
||
(define-key sesman-browser-mode-map (kbd "n") #'sesman-browser-vertical-next)
|
||
(define-key sesman-browser-mode-map (kbd "p") #'sesman-browser-vertical-prev)
|
||
(define-key sesman-browser-mode-map (kbd "f") #'sesman-browser-forward)
|
||
(define-key sesman-browser-mode-map (kbd "b") #'sesman-browser-backward)
|
||
(define-key sesman-browser-mode-map [remap forward-paragraph] #'sesman-browser-session-next)
|
||
(define-key sesman-browser-mode-map [remap backward-paragraph] #'sesman-browser-session-prev)
|
||
(define-key sesman-browser-mode-map (kbd "C-M-n") #'sesman-browser-session-next)
|
||
(define-key sesman-browser-mode-map (kbd "C-M-p") #'sesman-browser-session-prev)
|
||
(define-key sesman-browser-mode-map (kbd "<tab>") #'sesman-browser-forward)
|
||
(define-key sesman-browser-mode-map (kbd "<backtab>") #'sesman-browser-backward)
|
||
(define-key sesman-browser-mode-map (kbd "<RET>") #'sesman-goto)
|
||
(define-key sesman-browser-mode-map (kbd "o") #'sesman-show)
|
||
(define-key sesman-browser-mode-map (kbd "t") #'sesman-browser-toggle-sort)
|
||
(define-key sesman-browser-mode-map (kbd "S") #'sesman-browser-toggle-sort)
|
||
(define-key sesman-browser-mode-map (kbd "l b") #'sesman-browser-link-with-buffer)
|
||
(define-key sesman-browser-mode-map (kbd "l d") #'sesman-browser-link-with-directory)
|
||
(define-key sesman-browser-mode-map (kbd "l p") #'sesman-browser-link-with-project)
|
||
(define-key sesman-browser-mode-map (kbd "u") #'sesman-browser-unlink)
|
||
(define-key sesman-browser-mode-map (kbd "s") 'sesman-browser-map)
|
||
(define-key sesman-browser-mode-map (kbd "C-c C-s") 'sesman-browser-map)
|
||
sesman-browser-mode-map)
|
||
"Local keymap in `sesman-browser-mode'.")
|
||
|
||
|
||
;;; Utilities
|
||
|
||
(defun sesman-browser--closeby-pos (prop lax)
|
||
(or (when (get-text-property (point) prop)
|
||
(point))
|
||
(when (and (not (bobp))
|
||
(get-text-property (1- (point)) prop))
|
||
(1- (point)))
|
||
(when lax
|
||
(let ((next (save-excursion
|
||
(and
|
||
(goto-char (next-single-char-property-change (point) prop))
|
||
(get-text-property (point) prop)
|
||
(point))))
|
||
(prev (save-excursion
|
||
(and
|
||
(goto-char (previous-single-char-property-change (point) prop))
|
||
(not (bobp))
|
||
(get-text-property (1- (point)) prop)
|
||
(1- (point))))))
|
||
(if next
|
||
(if prev
|
||
(if (< (- (point) prev) (- next (point)))
|
||
prev
|
||
next)
|
||
next)
|
||
prev)))))
|
||
|
||
(defun sesman-browser--closeby-value (prop lax)
|
||
(when-let ((pos (sesman-browser--closeby-pos prop lax)))
|
||
(get-text-property pos prop)))
|
||
|
||
(defun sesman-browser-get (what &optional no-error lax)
|
||
"Get value of the property WHAT at point.
|
||
If NO-ERROR is non-nil, don't throw an error if no value has been found and
|
||
return nil. If LAX is non-nil, search nearby and return the closest value."
|
||
(when (derived-mode-p 'sesman-browser-mode)
|
||
(or (let ((prop (pcase what
|
||
('session :sesman-session)
|
||
('link :sesman-link)
|
||
('object :sesman-object)
|
||
(_ what))))
|
||
(sesman-browser--closeby-value prop 'lax))
|
||
(unless no-error
|
||
(user-error "No %s %s" what (if lax "nearby" "at point"))))))
|
||
|
||
|
||
;;; Navigation
|
||
|
||
(defvar-local sesman-browser--section-overlay nil)
|
||
(defvar-local sesman-browser--stop-overlay nil)
|
||
|
||
(when (fboundp 'define-fringe-bitmap)
|
||
(define-fringe-bitmap 'sesman-left-bar
|
||
[#b00001100] nil nil '(top t)))
|
||
|
||
(defun sesman-browser--next (prop)
|
||
(let ((pos (point)))
|
||
(goto-char (previous-single-char-property-change (point) prop))
|
||
(unless (get-text-property (point) prop)
|
||
(goto-char (previous-single-char-property-change (point) prop)))
|
||
(when (bobp)
|
||
(goto-char pos))))
|
||
|
||
(defun sesman-browser--prev (prop)
|
||
(let ((pos (point)))
|
||
(goto-char (next-single-char-property-change (point) prop))
|
||
(unless (get-text-property (point) prop)
|
||
(goto-char (next-single-char-property-change (point) prop)))
|
||
(when (eobp)
|
||
(goto-char pos))))
|
||
|
||
(defun sesman-browser-forward ()
|
||
"Go to next button."
|
||
(interactive)
|
||
(sesman-browser--prev :sesman-stop))
|
||
|
||
(defun sesman-browser-backward ()
|
||
"Go to previous button."
|
||
(interactive)
|
||
(sesman-browser--next :sesman-stop))
|
||
|
||
(defun sesman-browser-vertical-next ()
|
||
"Go to next button section or row."
|
||
(interactive)
|
||
(sesman-browser--prev :sesman-vertical-stop))
|
||
|
||
(defun sesman-browser-vertical-prev ()
|
||
"Go to previous button section or row."
|
||
(interactive)
|
||
(sesman-browser--next :sesman-vertical-stop))
|
||
|
||
(defun sesman-browser-session-next ()
|
||
"Go to next session."
|
||
(interactive)
|
||
(sesman-browser--prev :sesman-session-stop))
|
||
|
||
(defun sesman-browser-session-prev ()
|
||
"Go to previous session."
|
||
(interactive)
|
||
(sesman-browser--next :sesman-session-stop))
|
||
|
||
|
||
;;; Display
|
||
|
||
(defun sesman-goto (&optional no-switch)
|
||
"Go to most relevant buffer for session at point.
|
||
If NO-SWITCH is non-nil, only display the buffer."
|
||
(interactive "P")
|
||
(let ((object (get-text-property (point) :sesman-object)))
|
||
(if (and object (bufferp object))
|
||
(if no-switch
|
||
(display-buffer object)
|
||
(pop-to-buffer object))
|
||
(let* ((session (sesman-browser-get 'session))
|
||
(info (sesman-session-info (sesman--system) session))
|
||
(buffers (or (plist-get info :buffers)
|
||
(let ((objects (plist-get info :objects)))
|
||
(seq-filter #'bufferp objects)))))
|
||
(if buffers
|
||
(let ((most-recent-buf (seq-find (lambda (b)
|
||
(member b buffers))
|
||
(buffer-list))))
|
||
(if no-switch
|
||
(display-buffer most-recent-buf)
|
||
(pop-to-buffer most-recent-buf)))
|
||
(user-error "Cannot jump to session %s; it doesn't contain any buffers" (car session)))))))
|
||
|
||
(defun sesman-show ()
|
||
"Show the most relevant buffer for the session at point."
|
||
(interactive)
|
||
(sesman-goto 'no-switch))
|
||
|
||
(defun sesman-browser--sensor-function (&rest ignore)
|
||
(let ((beg (or (when (get-text-property (point) :sesman-stop)
|
||
(if (get-text-property (1- (point)) :sesman-stop)
|
||
(previous-single-char-property-change (point) :sesman-stop)
|
||
(point)))
|
||
(next-single-char-property-change (point) :sesman-stop)))
|
||
(end (next-single-char-property-change (point) :sesman-stop)))
|
||
(move-overlay sesman-browser--stop-overlay beg end)
|
||
(when window-system
|
||
(when-let* ((beg (get-text-property (point) :sesman-fragment-beg))
|
||
(end (get-text-property (point) :sesman-fragment-end)))
|
||
(move-overlay sesman-browser--section-overlay beg end)))))
|
||
|
||
|
||
;;; Sesman UI
|
||
|
||
(defun sesman-browser-quit-session ()
|
||
"Quite session at point."
|
||
(interactive)
|
||
(sesman-quit (sesman-browser-get 'session)))
|
||
|
||
(defun sesman-browser-restart-session ()
|
||
"Restart session at point."
|
||
(interactive)
|
||
(sesman-restart (sesman-browser-get 'session)))
|
||
|
||
(defun sesman-browser-link-with-buffer ()
|
||
"Ask for buffer to link session at point to."
|
||
(interactive)
|
||
(let ((session (sesman-browser-get 'session)))
|
||
(sesman-link-with-buffer 'ask session)))
|
||
|
||
(defun sesman-browser-link-with-directory ()
|
||
"Ask for directory to link session at point to."
|
||
(interactive)
|
||
(let ((session (sesman-browser-get 'session)))
|
||
(sesman-link-with-directory 'ask session)))
|
||
|
||
(defun sesman-browser-link-with-project ()
|
||
"Ask for project to link session at point to."
|
||
(interactive)
|
||
(let ((session (sesman-browser-get 'session)))
|
||
(sesman-link-with-project 'ask session)))
|
||
|
||
(defun sesman-browser-unlink ()
|
||
"Unlink the link at point or ask for link to unlink."
|
||
(interactive)
|
||
(if-let ((link (sesman-browser-get 'link 'no-error)))
|
||
(sesman--unlink link)
|
||
(if-let ((links (sesman-links (sesman--system)
|
||
(sesman-browser-get 'session))))
|
||
(mapc #'sesman--unlink
|
||
(sesman--ask-for-link "Unlink: " links 'ask-all))
|
||
(user-error "No links for session %s" (car (sesman-browser-get 'session)))))
|
||
(run-hooks 'sesman-post-command-hook))
|
||
|
||
|
||
;;; Major Mode
|
||
|
||
(defun sesman-browser-revert (&rest _ignore)
|
||
"Refresh current browser buffer."
|
||
(let ((pos (point)))
|
||
(sesman-browser)
|
||
;; simple but not particularly reliable or useful
|
||
(goto-char (min pos (point-max)))))
|
||
|
||
(defun sesman-browser-revert-all (system)
|
||
"Refresh all Sesman SYSTEM browsers."
|
||
(mapc (lambda (b)
|
||
(with-current-buffer b
|
||
(when (and (derived-mode-p 'sesman-browser-mode)
|
||
(eq system (sesman--system)))
|
||
(sesman-browser-revert))))
|
||
(buffer-list)))
|
||
|
||
(defun sesman-browser--goto-stop (stop-value)
|
||
(let ((search t))
|
||
(goto-char (point-min))
|
||
(while search
|
||
(goto-char (next-single-char-property-change (point) :sesman-stop))
|
||
(if (eobp)
|
||
(progn (setq search nil)
|
||
(goto-char (next-single-char-property-change (point-min) :sesman-stop)))
|
||
(when (equal (get-text-property (point) :sesman-stop) stop-value)
|
||
(setq search nil))))))
|
||
|
||
(defun sesman-browser-toggle-sort ()
|
||
"Toggle sorting of sessions.
|
||
See `sesman-browser-sort-type' for the default sorting type."
|
||
(interactive)
|
||
(when (eq sesman-browser-sort-type
|
||
(car sesman-browser--sort-types))
|
||
(pop sesman-browser--sort-types))
|
||
(unless sesman-browser--sort-types
|
||
(setq-local sesman-browser--sort-types (default-value 'sesman-browser--sort-types)))
|
||
(setq sesman-browser-sort-type (pop sesman-browser--sort-types))
|
||
(let ((stop (sesman-browser-get :sesman-stop nil 'lax)))
|
||
(sesman-browser)
|
||
(sesman-browser--goto-stop stop)
|
||
(sesman-browser--sensor-function))
|
||
(message "Sorted by %s"
|
||
(propertize (symbol-name sesman-browser-sort-type) 'face 'bold)))
|
||
|
||
(define-derived-mode sesman-browser-mode special-mode "SesmanBrowser"
|
||
"Interactive view of Sesman sessions.
|
||
When applicable, system specific commands are locally bound to j when point is
|
||
on a session object."
|
||
;; ensure there is a sesman-system here
|
||
(sesman--system)
|
||
(delete-all-overlays)
|
||
(setq-local sesman-browser--stop-overlay (make-overlay (point) (point)))
|
||
(overlay-put sesman-browser--stop-overlay 'face 'sesman-browser-highligh-face)
|
||
(setq-local sesman-browser--section-overlay (make-overlay (point) (point)))
|
||
(when window-system
|
||
(let* ((fringe-spec '(left-fringe sesman-left-bar sesman-browser-highligh-face))
|
||
(dummy-string (propertize "|" 'display fringe-spec)))
|
||
(overlay-put sesman-browser--section-overlay 'line-prefix dummy-string)))
|
||
(add-hook 'sesman-post-command-hook 'sesman-browser-revert nil t)
|
||
(setq-local display-buffer-base-action '(nil . ((inhibit-same-window . t))))
|
||
(setq-local sesman-browser--sort-types (default-value 'sesman-browser--sort-types))
|
||
(setq-local revert-buffer-function #'sesman-browser-revert))
|
||
|
||
(defun sesman-browser--insert-session (system ses i)
|
||
(let ((ses-name (car ses))
|
||
(head-template "%17s")
|
||
beg end)
|
||
(setq beg (point))
|
||
|
||
;; session header
|
||
(insert (format "%3d: " i))
|
||
(insert (propertize (car ses)
|
||
:sesman-stop ses-name
|
||
:sesman-vertical-stop t
|
||
:sesman-session-stop t
|
||
'face 'bold
|
||
'cursor-sensor-functions (list #'sesman-browser--sensor-function)
|
||
'mouse-face 'highlight)
|
||
"\n")
|
||
|
||
;; links
|
||
(insert (format head-template "linked-to: "))
|
||
(let ((link-groups (sesman-grouped-links system ses))
|
||
(vert-stop))
|
||
(dolist (grp link-groups)
|
||
(let* ((type (car grp)))
|
||
(dolist (link (cdr grp))
|
||
(when (> (current-column) fill-column)
|
||
(insert "\n" (format head-template " "))
|
||
(setq vert-stop nil))
|
||
(let ((val (sesman--abbrev-path-maybe (sesman--lnk-value link))))
|
||
(insert (propertize (sesman--format-context type val 'sesman-browser-button-face)
|
||
:sesman-stop (car link)
|
||
:sesman-vertical-stop (unless vert-stop (setq vert-stop t))
|
||
:sesman-link link
|
||
'cursor-sensor-functions (list #'sesman-browser--sensor-function)
|
||
'mouse-face 'highlight)))
|
||
(insert " ")))))
|
||
(insert "\n")
|
||
|
||
;; objects
|
||
(insert (format head-template "objects: "))
|
||
(let* ((info (sesman-session-info system ses))
|
||
(map (plist-get info :map))
|
||
(objects (plist-get info :objects))
|
||
(strings (or (plist-get info :strings)
|
||
(mapcar (lambda (x) (format "%s" x)) objects)))
|
||
(kvals (seq-mapn #'cons objects strings))
|
||
(kvals (seq-sort (lambda (a b) (string-lessp (cdr a) (cdr b)))
|
||
kvals))
|
||
(vert-stop))
|
||
(dolist (kv kvals)
|
||
(when (> (current-column) fill-column)
|
||
(insert "\n" (format head-template " "))
|
||
(setq vert-stop nil))
|
||
(let ((str (replace-regexp-in-string ses-name "%s" (cdr kv) nil t)))
|
||
(insert (propertize str
|
||
:sesman-stop str
|
||
:sesman-vertical-stop (unless vert-stop (setq vert-stop t))
|
||
:sesman-object (car kv)
|
||
'cursor-sensor-functions (list #'sesman-browser--sensor-function)
|
||
'face 'sesman-browser-button-face
|
||
'mouse-face 'highlight
|
||
'help-echo "mouse-2: visit in other window"
|
||
'keymap map)
|
||
" "))))
|
||
|
||
;; session properties
|
||
(setq end (point))
|
||
(put-text-property beg end :sesman-session ses)
|
||
(put-text-property beg end :sesman-session-name ses-name)
|
||
(put-text-property beg end :sesman-fragment-beg beg)
|
||
(put-text-property beg end :sesman-fragment-end end)
|
||
(insert "\n\n")))
|
||
|
||
;;;###autoload
|
||
(defun sesman-browser ()
|
||
"Display an interactive session browser.
|
||
See `sesman-browser-mode' for more details."
|
||
(interactive)
|
||
(let* ((system (sesman--system))
|
||
(pop-to (called-interactively-p 'any))
|
||
(sessions (sesman-sessions system))
|
||
(cur-session (when pop-to
|
||
(sesman-current-session 'CIDER)))
|
||
(buff (get-buffer-create (format "*sesman %s browser*" system))))
|
||
(with-current-buffer buff
|
||
(setq-local sesman-system system)
|
||
(sesman-browser-mode)
|
||
(cursor-sensor-mode 1)
|
||
(let ((inhibit-read-only t)
|
||
(sessions (pcase sesman-browser-sort-type
|
||
('name (seq-sort (lambda (a b) (string-greaterp (car b) (car a)))
|
||
sessions))
|
||
('relevance (sesman--sort-sessions system sessions))
|
||
(_ (error "Invalid `sesman-browser-sort-type'"))))
|
||
(i 0))
|
||
(erase-buffer)
|
||
(insert "\n ")
|
||
(insert (propertize (format "%s Sessions:" system)
|
||
'face '(bold font-lock-keyword-face)))
|
||
(insert "\n\n")
|
||
(dolist (ses sessions)
|
||
(setq i (1+ i))
|
||
(sesman-browser--insert-session system ses i))
|
||
(when pop-to
|
||
(pop-to-buffer buff)
|
||
(sesman-browser--goto-stop (car cur-session)))
|
||
(sesman-browser--sensor-function)))))
|
||
|
||
(provide 'sesman-browser)
|
||
;;; sesman-browser.el ends here
|