|
|
- ;;; sesman.el --- Generic Session Manager -*- lexical-binding: t -*-
- ;;
- ;; Copyright (C) 2018, Vitalie Spinu
- ;; Author: Vitalie Spinu
- ;; URL: https://github.com/vspinu/sesman
- ;; Keywords: process
- ;; Version: 0.3.3-DEV
- ;; Package-Requires: ((emacs "25"))
- ;; Keywords: processes, tools, vc
- ;;
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;
- ;; 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:
- ;;
- ;; Sesman provides facilities for session management and interactive session
- ;; association with the current contexts (project, directory, buffers etc). See
- ;; project's readme for more details.
- ;;
- ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
- ;;
- ;;; Code:
-
- (require 'cl-generic)
- (require 'seq)
- (require 'subr-x)
- (require 'vc)
-
- (defgroup sesman nil
- "Generic Session Manager."
- :prefix "sesman-"
- :group 'tools
- :link '(url-link :tag "GitHub" "https://github.com/vspinu/sesman"))
-
- (defface sesman-project-face
- '((default (:inherit font-lock-doc-face)))
- "Face used to mark projects."
- :group 'sesman)
-
- (defface sesman-directory-face
- '((default (:inherit font-lock-type-face)))
- "Face used to mark directories."
- :group 'sesman)
-
- (defface sesman-buffer-face
- '((default (:inherit font-lock-preprocessor-face)))
- "Face used to mark buffers."
- :group 'sesman)
-
- (defcustom sesman-use-friendly-sessions t
- "If non-nil consider friendly sessions when looking for current sessions.
- The definition of friendly sessions is system dependent but usually means
- sessions running in dependent projects."
- :group 'sesman
- :type 'boolean
- :package-version '(sesman . "0.3.2"))
-
- (defcustom sesman-follow-symlinks 'vc
- "When non-nil, follow symlinks during the file expansion.
- When nil, don't follow symlinks. When 'vc, follow symlinks only when
- `vc-follow-symlinks' is non-nil. When t, always follow symlinks."
- :group 'sesman
- :type '(choice (const :tag "Comply with `vc-follow-symlinks'" vc)
- (const :tag "Don't follow symlinks" nil)
- (const :tag "Follow symlinks" t))
- :package-version '(sesman . "0.3.3"))
- (put 'sesman-follow-symlinks 'safe-local-variable (lambda (x) (memq x '(vc nil t))))
-
- ;; (defcustom sesman-disambiguate-by-relevance t
- ;; "If t choose most relevant session in ambiguous situations, otherwise ask.
- ;; Ambiguity arises when multiple sessions are associated with current context. By
- ;; default only projects could be associated with multiple sessions. See
- ;; `sesman-single-link-contexts' in order to change that. Relevance is decided by
- ;; system's implementation, see `sesman-more-relevant-p'."
- ;; :group 'sesman
- ;; :type 'boolean)
-
- (defcustom sesman-single-link-context-types '(buffer)
- "List of context types to which at most one session can be linked."
- :group 'sesman
- :type '(repeat symbol)
- :package-version '(sesman . "0.1.0"))
-
- ;; FIXME:
- ;; (defcustom sesman-abbreviate-paths 2
- ;; "Abbreviate paths to that many parents.
- ;; When set to nil, don't abbreviate directories."
- ;; :group 'sesman
- ;; :type '(choice number
- ;; (const :tag "Don't abbreviate" nil)))
-
- (defvar sesman-sessions-hashmap (make-hash-table :test #'equal)
- "Hash-table of all sesman sessions.
- Key is a cons (system-name . session-name).")
-
- (defvar sesman-links-alist nil
- "An alist of all sesman links.
- Each element is of the form (key cxt-type cxt-value) where
- \"key\" is of the form (system-name . session-name). system-name
- and cxt-type must be symbols.")
-
- (defvar-local sesman-system nil
- "Name of the system managed by `sesman'.
- Can be either a symbol, or a function returning a symbol.")
- (put 'sesman-system 'permanent-local 't)
-
-
- ;; Internal Utilities
-
- (defun sesman--on-C-u-u-sessions (system which)
- (cond
- ((null which)
- (let ((ses (sesman-current-session system)))
- (when ses
- (list ses))))
- ((or (equal which '(4)) (eq which 'linked))
- (sesman--linked-sessions system 'sort))
- ((or (equal which '(16)) (eq which 'all) (eq which t))
- (sesman--all-system-sessions system 'sort))
- ;; session itself
- ((and (listp which)
- (or (stringp (car which))
- (symbolp (car which))))
- (list which))
- ;; session name
- ((or (stringp which)
- (symbolp which)
- (gethash (cons system which) sesman-sessions-hashmap)))
- (t (error "Invalid which argument (%s)" which))))
-
- (defun sesman--cap-system-name (system)
- (let ((name (symbol-name system)))
- (if (string-match-p "^[[:upper:]]" name)
- name
- (capitalize name))))
-
- (defun sesman--least-specific-context (system)
- (seq-some (lambda (ctype)
- (when-let (val (sesman-context ctype system))
- (cons ctype val)))
- (reverse (sesman-context-types system))))
-
- (defun sesman--link-session-interactively (session cxt-type cxt-val)
- (let ((system (sesman--system)))
- (unless cxt-type
- (let ((cxt (sesman--least-specific-context system)))
- (setq cxt-type (car cxt)
- cxt-val (cdr cxt))))
- (let ((cxt-name (symbol-name cxt-type)))
- (if (member cxt-type (sesman-context-types system))
- (let ((session (or session
- (sesman-ask-for-session
- system
- (format "Link with %s %s: "
- cxt-name (sesman--abbrev-path-maybe
- (sesman-context cxt-type system)))
- (sesman--all-system-sessions system 'sort)
- 'ask-new))))
- (sesman-link-session system session cxt-type cxt-val))
- (error (format "%s association not allowed for this system (%s)"
- (capitalize cxt-name)
- system))))))
-
- ;; FIXME: incorporate `sesman-abbreviate-paths'
- (defun sesman--abbrev-path-maybe (obj)
- (if (stringp obj)
- (abbreviate-file-name obj)
- obj))
-
- (defun sesman--system-in-buffer (&optional buffer)
- (with-current-buffer (or buffer (current-buffer))
- (if (functionp sesman-system)
- (funcall sesman-system)
- sesman-system)))
-
- (defun sesman--system ()
- (if sesman-system
- (if (functionp sesman-system)
- (funcall sesman-system)
- sesman-system)
- (error "No `sesman-system' in buffer `%s'" (current-buffer))))
-
- (defun sesman--linked-sessions (system &optional sort cxt-types)
- (let* ((system (or system (sesman--system)))
- (cxt-types (or cxt-types (sesman-context-types system))))
- ;; just in case some links are lingering due to user errors
- (sesman--clear-links)
- (delete-dups
- (mapcar (lambda (assoc)
- (gethash (car assoc) sesman-sessions-hashmap))
- (sesman-current-links system nil sort cxt-types)))))
-
- (defun sesman--friendly-sessions (system &optional sort)
- (let ((sessions (seq-filter (lambda (ses) (sesman-friendly-session-p system ses))
- (sesman--all-system-sessions system))))
- (if sort
- (sesman--sort-sessions system sessions)
- sessions)))
-
- (defun sesman--all-system-sessions (&optional system sort)
- "Return a list of sessions registered with SYSTEM.
- If SORT is non-nil, sort in relevance order."
- (let ((system (or system (sesman--system)))
- sessions)
- (maphash
- (lambda (k s)
- (when (eql (car k) system)
- (push s sessions)))
- sesman-sessions-hashmap)
- (if sort
- (sesman--sort-sessions system sessions)
- sessions)))
-
- ;; FIXME: make this a macro
- (defun sesman--link-lookup-fn (&optional system ses-name cxt-type cxt-val x)
- (let ((system (or system (caar x)))
- (ses-name (or ses-name (cdar x)))
- (cxt-type (or cxt-type (nth 1 x)))
- (cxt-val (or cxt-val (nth 2 x))))
- (lambda (el)
- (and (or (null system) (eq (caar el) system))
- (or (null ses-name) (equal (cdar el) ses-name))
- (or (null cxt-type)
- (if (listp cxt-type)
- (member (nth 1 el) cxt-type)
- (eq (nth 1 el) cxt-type)))
- (or (null cxt-val) (equal (nth 2 el) cxt-val))))))
-
- (defun sesman--unlink (x)
- (setq sesman-links-alist
- (seq-remove (sesman--link-lookup-fn nil nil nil nil x)
- sesman-links-alist)))
-
- (defun sesman--clear-links ()
- (setq sesman-links-alist
- (seq-filter (lambda (x)
- (gethash (car x) sesman-sessions-hashmap))
- sesman-links-alist)))
-
- (defun sesman--format-session-objects (system session &optional sep)
- (let ((info (sesman-session-info system session)))
- (if (and (listp info)
- (keywordp (car info)))
- (let ((ses-name (car session))
- (sep (or sep " "))
- (strings (or (plist-get info :strings)
- (mapcar (lambda (x) (format "%s" x))
- (plist-get info :objects)))))
- (mapconcat (lambda (str)
- (replace-regexp-in-string ses-name "..." str nil t))
- strings sep))
- (format "%s" info))))
-
- (defun sesman--format-session (system ses &optional prefix)
- (format (propertize "%s%s [%s] linked-to %s" 'face 'bold)
- (or prefix "")
- (propertize (car ses) 'face 'bold)
- (propertize (sesman--format-session-objects system ses ", ") 'face 'italic)
- (sesman-grouped-links system ses t t)))
-
- (defun sesman--format-link (link)
- (let* ((system (sesman--lnk-system-name link))
- (session (gethash (car link) sesman-sessions-hashmap)))
- (format "%s(%s) -> %s [%s]"
- (sesman--lnk-context-type link)
- (propertize (format "%s" (sesman--abbrev-path-maybe (sesman--lnk-value link)))
- 'face 'bold)
- (propertize (sesman--lnk-session-name link) 'face 'bold)
- (if session
- (sesman--format-session-objects system session)
- "invalid"))))
-
- (defun sesman--ask-for-link (prompt links &optional ask-all)
- (let* ((name.keys (mapcar (lambda (link)
- (cons (sesman--format-link link) link))
- links))
- (name.keys (append name.keys
- (when (and ask-all (> (length name.keys) 1))
- '(("*all*")))))
- (nms (mapcar #'car name.keys))
- (sel (completing-read prompt nms nil t nil nil (car nms))))
- (cond ((string= sel "*all*")
- links)
- (ask-all
- (list (cdr (assoc sel name.keys))))
- (t
- (cdr (assoc sel name.keys))))))
-
- (defun sesman--sort-sessions (system sessions)
- (seq-sort (lambda (x1 x2)
- (sesman-more-relevant-p system x1 x2))
- sessions))
-
- (defun sesman--sort-links (system links)
- (seq-sort (lambda (x1 x2)
- (sesman-more-relevant-p system
- (gethash (car x1) sesman-sessions-hashmap)
- (gethash (car x2) sesman-sessions-hashmap)))
- links))
-
- ;; link data structure accessors
- (defun sesman--lnk-system-name (lnk)
- (caar lnk))
- (defun sesman--lnk-session-name (lnk)
- (cdar lnk))
- (defun sesman--lnk-context-type (lnk)
- (cadr lnk))
- (defun sesman--lnk-value (lnk)
- (nth 2 lnk))
-
- ;;; User Interface
-
- (defun sesman-post-command-hook nil
- "Normal hook ran after every state-changing Sesman command.")
-
- ;;;###autoload
- (defun sesman-start ()
- "Start a Sesman session."
- (interactive)
- (let ((system (sesman--system)))
- (message "Starting new %s session ..." system)
- (prog1 (sesman-start-session system)
- (run-hooks 'sesman-post-command-hook))))
-
- ;;;###autoload
- (defun sesman-restart (&optional which)
- "Restart sesman session.
- When WHICH is nil, restart the current session; when a single universal
- argument or 'linked, restart all linked sessions; when a double universal
- argument, t or 'all, restart all sessions. For programmatic use, WHICH can also
- be a session or a name of the session, in which case that session is restarted."
- (interactive "P")
- (let* ((system (sesman--system))
- (sessions (sesman--on-C-u-u-sessions system which)))
- (if (null sessions)
- (message "No %s sessions found" system)
- (with-temp-message (format "Restarting %s %s %s" system
- (if (= 1 (length sessions)) "session" "sessions")
- (mapcar #'car sessions))
- (mapc (lambda (s)
- (sesman-restart-session system s))
- sessions))
- ;; restarting is not guaranteed to finish here, but what can we do?
- (run-hooks 'sesman-post-command-hook))))
-
- ;;;###autoload
- (defun sesman-quit (&optional which)
- "Terminate a Sesman session.
- When WHICH is nil, kill only the current session; when a single universal
- argument or 'linked, kill all linked sessions; when a double universal argument,
- t or 'all, kill all sessions. For programmatic use, WHICH can also be a session
- or a name of the session, in which case that session is killed."
- (interactive "P")
- (let* ((system (sesman--system))
- (sessions (sesman--on-C-u-u-sessions system which)))
- (if (null sessions)
- (message "No %s sessions found" system)
- (with-temp-message (format "Killing %s %s %s" system
- (if (= 1 (length sessions)) "session" "sessions")
- (mapcar #'car sessions))
- (mapc (lambda (s)
- (sesman-unregister system s)
- (sesman-quit-session system s))
- sessions))
- (run-hooks 'sesman-post-command-hook))))
-
- ;;;###autoload
- (defun sesman-info (&optional all)
- "Display info for all current sessions (`sesman-current-sessions').
- In the resulting minibuffer display linked sessions are numbered and the
- other (friendly) sessions are not. When ALL is non-nil, show info for all
- sessions."
- (interactive "P")
- (let* ((system (sesman--system))
- (i 1)
- (sessions (if all
- (sesman-sessions system t)
- (sesman-current-sessions system)))
- (empty-prefix (if (> (length sessions) 1) " " "")))
- (if sessions
- (message (mapconcat (lambda (ses)
- (let ((prefix (if (sesman-relevant-session-p system ses)
- (prog1 (format "%d " i)
- (setq i (1+ i)))
- empty-prefix)))
- (sesman--format-session system ses prefix)))
- sessions
- "\n"))
- (message "No %s%s sessions"
- (if all "" "current ")
- system))))
-
- ;;;###autoload
- (defun sesman-link-with-buffer (&optional buffer session)
- "Ask for SESSION and link with BUFFER.
- BUFFER defaults to current buffer. On universal argument, or if BUFFER is 'ask,
- ask for buffer."
- (interactive "P")
- (let ((buf (if (or (eq buffer 'ask)
- (equal buffer '(4)))
- (let ((this-system (sesman--system)))
- (read-buffer "Link buffer: " (current-buffer) t
- (lambda (buf-cons)
- (equal this-system
- (sesman--system-in-buffer (cdr buf-cons))))))
- (or buffer (current-buffer)))))
- (sesman--link-session-interactively session 'buffer buf)))
-
- ;;;###autoload
- (defun sesman-link-with-directory (&optional dir session)
- "Ask for SESSION and link with DIR.
- DIR defaults to `default-directory'. On universal argument, or if DIR is 'ask,
- ask for directory."
- (interactive "P")
- (let ((dir (if (or (eq dir 'ask)
- (equal dir '(4)))
- (read-directory-name "Link directory: ")
- (or dir default-directory))))
- (sesman--link-session-interactively session 'directory dir)))
-
- ;;;###autoload
- (defun sesman-link-with-project (&optional project session)
- "Ask for SESSION and link with PROJECT.
- PROJECT defaults to current project. On universal argument, or if PROJECT is
- 'ask, ask for the project. SESSION defaults to the current session."
- (interactive "P")
- (let* ((system (sesman--system))
- (project (expand-file-name
- (if (or (eq project 'ask)
- (equal project '(4)))
- ;; FIXME: should be a completion over all known projects for this system
- (read-directory-name "Project: " (sesman-project system))
- (or project (sesman-project system))))))
- (sesman--link-session-interactively session 'project project)))
-
- ;;;###autoload
- (defun sesman-link-with-least-specific (&optional session)
- "Ask for SESSION and link with the least specific context available.
- Normally the least specific context is the project. If not in a project, link
- with the `default-directory'. If `default-directory' is nil, link with current
- buffer."
- (interactive "P")
- (sesman--link-session-interactively session nil nil))
-
- ;;;###autoload
- (defun sesman-unlink ()
- "Break any of the previously created links."
- (interactive)
- (let* ((system (sesman--system))
- (links (or (sesman-current-links system)
- (user-error "No %s links found" system))))
- (mapc #'sesman--unlink
- (sesman--ask-for-link "Unlink: " links 'ask-all)))
- (run-hooks 'sesman-post-command-hook))
-
- (declare-function sesman-browser "sesman-browser")
- ;;;###autoload (autoload 'sesman-map "sesman" "Session management prefix keymap." t 'keymap)
- (defvar sesman-map
- (let (sesman-map)
- (define-prefix-command 'sesman-map)
- (define-key sesman-map (kbd "C-i") #'sesman-info)
- (define-key sesman-map (kbd "i") #'sesman-info)
- (define-key sesman-map (kbd "C-w") #'sesman-browser)
- (define-key sesman-map (kbd "w") #'sesman-browser)
- (define-key sesman-map (kbd "C-s") #'sesman-start)
- (define-key sesman-map (kbd "s") #'sesman-start)
- (define-key sesman-map (kbd "C-r") #'sesman-restart)
- (define-key sesman-map (kbd "r") #'sesman-restart)
- (define-key sesman-map (kbd "C-q") #'sesman-quit)
- (define-key sesman-map (kbd "q") #'sesman-quit)
- (define-key sesman-map (kbd "C-l") #'sesman-link-with-least-specific)
- (define-key sesman-map (kbd "l") #'sesman-link-with-least-specific)
- (define-key sesman-map (kbd "C-b") #'sesman-link-with-buffer)
- (define-key sesman-map (kbd "b") #'sesman-link-with-buffer)
- (define-key sesman-map (kbd "C-d") #'sesman-link-with-directory)
- (define-key sesman-map (kbd "d") #'sesman-link-with-directory)
- (define-key sesman-map (kbd "C-p") #'sesman-link-with-project)
- (define-key sesman-map (kbd "p") #'sesman-link-with-project)
- (define-key sesman-map (kbd "C-u") #'sesman-unlink)
- (define-key sesman-map (kbd " u") #'sesman-unlink)
- sesman-map)
- "Session management prefix keymap.")
-
- (defvar sesman-menu
- '("Sesman"
- ["Show Session Info" sesman-info]
- "--"
- ["Start" sesman-start]
- ["Restart" sesman-restart :active (sesman-current-session (sesman--system))]
- ["Quit" sesman-quit :active (sesman-current-session (sesman--system))]
- "--"
- ["Link with Buffer" sesman-link-with-buffer :active (sesman-current-session (sesman--system))]
- ["Link with Directory" sesman-link-with-directory :active (sesman-current-session (sesman--system))]
- ["Link with Project" sesman-link-with-project :active (sesman-current-session (sesman--system))]
- "--"
- ["Unlink" sesman-unlink :active (sesman-current-session (sesman--system))])
- "Sesman Menu.")
-
- (defun sesman-install-menu (map)
- "Install `sesman-menu' into MAP."
- (easy-menu-do-define 'seman-menu-open
- map
- (get 'sesman-menu 'variable-documentation)
- sesman-menu))
-
- ;;; System Generic
-
- (cl-defgeneric sesman-start-session (system)
- "Start and return SYSTEM SESSION.")
-
- (cl-defgeneric sesman-quit-session (system session)
- "Terminate SYSTEM SESSION.")
-
- (cl-defgeneric sesman-restart-session (system session)
- "Restart SYSTEM SESSION.
- By default, calls `sesman-quit-session' and then
- `sesman-start-session'."
- (let ((old-name (car session)))
- (sesman-quit-session system session)
- (let ((new-session (sesman-start-session system)))
- (setcar new-session old-name))))
-
- (cl-defgeneric sesman-session-info (_system session)
- "Return a plist with :objects key containing user \"visible\" objects.
- Optional :strings value is a list of string representations of objects. Optional
- :map key is a local keymap to place on every object in the session browser.
- Optional :buffers is a list of buffers which will be used for navigation from
- the session browser. If :buffers is missing, buffers from :objects are used
- instead."
- (list :objects (cdr session)))
-
- (cl-defgeneric sesman-project (_system)
- "Retrieve project root in current directory (`default-directory') for SYSTEM.
- Return a string or nil if no project has been found."
- nil)
-
- (cl-defgeneric sesman-more-relevant-p (_system session1 session2)
- "Return non-nil if SESSION1 should be sorted before SESSION2.
- By default, sort by session name. Systems should overwrite this method to
- provide a more meaningful ordering. If your system objects are buffers you can
- use `sesman-more-recent-p' utility in this method."
- (not (string-greaterp (car session1) (car session2))))
-
- (cl-defgeneric sesman-friendly-session-p (_system _session)
- "Return non-nil if SESSION is a friendly session in current context.
- The \"friendship\" is system dependent but usually means sessions running in
- dependent projects. Unless SYSTEM has defined a method for this generic, there
- are no friendly sessions."
- nil)
-
- (cl-defgeneric sesman-context-types (_system)
- "Return a list of context types understood by SYSTEM.
- Contexts must be sorted from most specific to least specific."
- '(buffer directory project))
-
- ;;; System API
-
- (defun sesman-session (system session-name)
- "Retrieve SYSTEM's session with SESSION-NAME from global hash."
- (let ((system (or system (sesman--system))))
- (gethash (cons system session-name) sesman-sessions-hashmap)))
-
- (defun sesman-sessions (system &optional sort type cxt-types)
- "Return a list of sessions registered with SYSTEM.
- When TYPE is either 'all or nil return all sessions registered with the SYSTEM,
- when 'linked, only linked to the current context sessions, when 'friendly - only
- friendly sessions. If SORT is non-nil, sessions are sorted in the relevance
- order with linked sessions leading the list. CXT-TYPES is a list of context
- types to consider for linked sessions."
- (let ((system (or system (sesman--system))))
- (cond
- ((eq type 'linked)
- (sesman--linked-sessions system sort cxt-types))
- ((eq type 'friendly)
- (sesman--friendly-sessions system sort))
- ((memq type '(all nil))
- (if sort
- (delete-dups
- (append (sesman--linked-sessions system 'sort cxt-types)
- (sesman--all-system-sessions system 'sort)))
- (sesman--all-system-sessions system)))
- (t (error "Invalid session TYPE argument %s" type)))))
-
- (defun sesman-current-sessions (system &optional cxt-types)
- "Return a list of SYSTEM sessions active in the current context.
- Sessions are ordered by the relevance order and linked sessions come first. If
- `sesman-use-friendly-sessions' current sessions consist of linked and friendly
- sessions, otherwise only of linked sessions. CXT-TYPES is a list of context
- types to consider. Defaults to the list returned from `sesman-context-types'."
- (if sesman-use-friendly-sessions
- (delete-dups
- (append (sesman--linked-sessions system 'sort cxt-types)
- (sesman--friendly-sessions system 'sort)))
- (sesman--linked-sessions system 'sort cxt-types)))
-
- (defun sesman-current-session (system &optional cxt-types)
- "Get the most relevant current session for the SYSTEM.
- CXT-TYPES is a list of context types to consider."
- (or (car (sesman--linked-sessions system 'sort cxt-types))
- (car (sesman--friendly-sessions system 'sort))))
-
- (defun sesman-ensure-session (system &optional cxt-types)
- "Get the most relevant linked session for SYSTEM or throw if none exists.
- CXT-TYPES is a list of context types to consider."
- (or (sesman-current-session system cxt-types)
- (user-error "No linked %s sessions" system)))
-
- (defun sesman-has-sessions-p (system)
- "Return t if there is at least one session registered with SYSTEM."
- (let ((system (or system (sesman--system)))
- (found))
- (condition-case nil
- (maphash (lambda (k _)
- (when (eq (car k) system)
- (setq found t)
- (throw 'found nil)))
- sesman-sessions-hashmap)
- (error))
- found))
-
- (defvar sesman--select-session-history nil)
- (defun sesman-ask-for-session (system prompt &optional sessions ask-new ask-all)
- "Ask for a SYSTEM session with PROMPT.
- SESSIONS defaults to value returned from `sesman-sessions'. If
- ASK-NEW is non-nil, offer *new* option to start a new session. If
- ASK-ALL is non-nil offer *all* option. If ASK-ALL is non-nil,
- return a list of sessions, otherwise a single session."
- (let* ((sessions (or sessions (sesman-sessions system)))
- (name.syms (mapcar (lambda (s)
- (let ((name (car s)))
- (cons (if (symbolp name) (symbol-name name) name)
- name)))
- sessions))
- (nr (length name.syms))
- (syms (if (and (not ask-new) (= nr 0))
- (error "No %s sessions found" system)
- (append name.syms
- (when ask-new '(("*new*")))
- (when (and ask-all (> nr 1))
- '(("*all*"))))))
- (def (caar syms))
- ;; (def (if (assoc (car sesman--select-session-history) syms)
- ;; (car sesman--select-session-history)
- ;; (caar syms)))
- (sel (completing-read
- prompt (mapcar #'car syms) nil t nil 'sesman--select-session-history def)))
- (cond
- ((string= sel "*new*")
- (let ((ses (sesman-start-session system)))
- (message "Started %s" (car ses))
- (if ask-all (list ses) ses)))
- ((string= sel "*all*")
- sessions)
- (t
- (let* ((sym (cdr (assoc sel syms)))
- (ses (assoc sym sessions)))
- (if ask-all (list ses) ses))))))
-
- (defvar sesman--cxt-abbrevs '(buffer "buf" project "proj" directory "dir"))
- (defun sesman--format-context (cxt-type cxt-val extra-face)
- (let* ((face (intern (format "sesman-%s-face" cxt-type)))
- (short-type (propertize (or (plist-get sesman--cxt-abbrevs cxt-type)
- (symbol-value cxt-type))
- 'face (list (if (facep face)
- face
- 'font-lock-function-name-face)
- extra-face))))
- (concat short-type
- (propertize (format "(%s)" cxt-val)
- 'face extra-face))))
-
- (defun sesman-grouped-links (system session &optional current-first as-string)
- "Retrieve all links for SYSTEM's SESSION from the global `sesman-links-alist'.
- Return an alist of the form
-
- ((buffer buffers..)
- (directory directories...)
- (project projects...)).
-
- When `CURRENT-FIRST' is non-nil, a cons of two lists as above is returned with
- car containing links relevant in current context and cdr all other links. If
- AS-STRING is non-nil, return an equivalent string representation."
- (let* ((system (or system (sesman--system)))
- (session (or session (sesman-current-session system)))
- (ses-name (car session))
- (links (thread-last sesman-links-alist
- (seq-filter (sesman--link-lookup-fn system ses-name))
- (sesman--sort-links system)
- (reverse)))
- (out (mapcar (lambda (x) (list x))
- (sesman-context-types system)))
- (out-rel (when current-first
- (copy-alist out))))
- (mapc (lambda (link)
- (let* ((type (sesman--lnk-context-type link))
- (entry (if (and current-first
- (sesman-relevant-link-p link))
- (assoc type out-rel)
- (assoc type out))))
- (when entry
- (setcdr entry (cons link (cdr entry))))))
- links)
- (let ((out (delq nil (mapcar (lambda (el) (and (cdr el) el)) out)))
- (out-rel (delq nil (mapcar (lambda (el) (and (cdr el) el)) out-rel))))
- (if as-string
- (let ((fmt-fn (lambda (typed-links)
- (let* ((type (car typed-links)))
- (mapconcat (lambda (lnk)
- (let ((val (sesman--abbrev-path-maybe
- (sesman--lnk-value lnk))))
- (sesman--format-context type val 'italic)))
- (cdr typed-links)
- ", ")))))
- (if out-rel
- (concat (mapconcat fmt-fn out-rel ", ")
- (when out " | ")
- (mapconcat fmt-fn out ", "))
- (mapconcat fmt-fn out ", ")))
- (if current-first
- (cons out-rel out)
- out)))))
-
- (defun sesman-link-session (system session &optional cxt-type cxt-val)
- "Link SYSTEM's SESSION to context give by CXT-TYPE and CXT-VAL.
- If CXT-TYPE is nil, use the least specific type available in the current
- context. If CXT-TYPE is non-nil, and CXT-VAL is not given, retrieve it with
- `sesman-context'. See also `sesman-link-with-project',
- `sesman-link-with-directory' and `sesman-link-with-buffer'."
- (let* ((ses-name (or (car-safe session)
- (error "SESSION must be a headed list")))
- (cxt-val (or cxt-val
- (or (if cxt-type
- (sesman-context cxt-type system)
- (let ((cxt (sesman--least-specific-context system)))
- (setq cxt-type (car cxt))
- (cdr cxt)))
- (error "No local context of type %s" cxt-type))))
- (cxt-val (if (stringp cxt-val)
- (expand-file-name cxt-val)
- cxt-val))
- (key (cons system ses-name))
- (link (list key cxt-type cxt-val)))
- (if (member cxt-type sesman-single-link-context-types)
- (thread-last sesman-links-alist
- (seq-remove (sesman--link-lookup-fn system nil cxt-type cxt-val))
- (cons link)
- (setq sesman-links-alist))
- (unless (seq-filter (sesman--link-lookup-fn system ses-name cxt-type cxt-val)
- sesman-links-alist)
- (setq sesman-links-alist (cons link sesman-links-alist))))
- (run-hooks 'sesman-post-command-hook)
- link))
-
- (defun sesman-links (system &optional session-or-name cxt-types sort)
- "Retrieve all links for SYSTEM, SESSION-OR-NAME and CXT-TYPES.
- SESSION-OR-NAME can be either a session or a name of the session. If SORT is
- non-nil links are sorted in relevance order and `sesman-current-links' lead the
- list, otherwise links are returned in the creation order."
- (let* ((ses-name (if (listp session-or-name)
- (car session-or-name)
- session-or-name))
- (lfn (sesman--link-lookup-fn system ses-name cxt-types)))
- (if sort
- (delete-dups (append
- (sesman-current-links system ses-name)
- (sesman--sort-links system (seq-filter lfn sesman-links-alist))))
- (seq-filter lfn sesman-links-alist))))
-
- (defun sesman-current-links (system &optional session-or-name sort cxt-types)
- "Retrieve all active links in current context for SYSTEM and SESSION-OR-NAME.
- SESSION-OR-NAME can be either a session or a name of the session. CXT-TYPES is a
- list of context types to consider. Returned links are a subset of
- `sesman-links-alist' sorted in order of relevance if SORT is non-nil."
- ;; mapcan is a built-in in 26.1; don't want to require cl-lib for one function
- (let ((ses-name (if (listp session-or-name)
- (car session-or-name)
- session-or-name)))
- (seq-mapcat
- (lambda (cxt-type)
- (let* ((lfn (sesman--link-lookup-fn system ses-name cxt-type))
- (links (seq-filter (lambda (l)
- (and (funcall lfn l)
- (sesman-relevant-context-p cxt-type (sesman--lnk-value l))))
- sesman-links-alist)))
- (if sort
- (sesman--sort-links system links)
- links)))
- (or cxt-types (sesman-context-types system)))))
-
- (defun sesman-has-links-p (system &optional cxt-types)
- "Return t if there is at least one linked session.
- CXT-TYPES defaults to `sesman-context-types' for current SYSTEM."
- (let ((cxt-types (or cxt-types (sesman-context-types system)))
- (found))
- (condition-case nil
- (mapc (lambda (l)
- (when (eq system (sesman--lnk-system-name l))
- (let ((cxt (sesman--lnk-context-type l)))
- (when (and (member cxt cxt-types)
- (sesman-relevant-context-p cxt (sesman--lnk-value l)))
- (setq found t)
- (throw 'found nil)))))
- sesman-links-alist)
- (error))
- found))
-
- (defun sesman-register (system session)
- "Register SESSION into `sesman-sessions-hashmap' and `sesman-links-alist'.
- SYSTEM defaults to current system. If a session with same name is already
- registered in `sesman-sessions-hashmap', change the name by appending \"#1\",
- \"#2\" ... to the name. This function should be called by system-specific
- connection initializers (\"run-xyz\", \"xyz-jack-in\" etc.)."
- (let* ((system (or system (sesman--system)))
- (ses-name (car session))
- (ses-name0 (car session))
- (i 1))
- (while (sesman-session system ses-name)
- (setq ses-name (format "%s#%d" ses-name0 i)
- i (1+ i)))
- (setq session (cons ses-name (cdr session)))
- (puthash (cons system ses-name) session sesman-sessions-hashmap)
- (sesman-link-session system session)
- session))
-
- (defun sesman-unregister (system session)
- "Unregister SESSION.
- SYSTEM defaults to current system. Remove session from
- `sesman-sessions-hashmap' and `sesman-links-alist'."
- (let ((ses-key (cons system (car session))))
- (remhash ses-key sesman-sessions-hashmap)
- (sesman--clear-links)
- session))
-
- (defun sesman-add-object (system session-name object &optional allow-new)
- "Add (destructively) OBJECT to session SESSION-NAME of SYSTEM.
- If ALLOW-NEW is nil and session with SESSION-NAME does not exist
- throw an error, otherwise register a new session with
- session (list SESSION-NAME OBJECT)."
- (let* ((system (or system (sesman--system)))
- (session (sesman-session system session-name)))
- (if session
- (setcdr session (cons object (cdr session)))
- (if allow-new
- (sesman-register system (list session-name object))
- (error "%s session '%s' does not exist"
- (sesman--cap-system-name system) session-name)))))
-
- (defun sesman-remove-object (system session-name object &optional auto-unregister no-error)
- "Remove (destructively) OBJECT from session SESSION-NAME of SYSTEM.
- If SESSION-NAME is nil, retrieve the session with
- `sesman-session-for-object'. If OBJECT is the last object in sesman
- session, `sesman-unregister' the session. If AUTO-UNREGISTER is non-nil
- unregister sessions of length 0 and remove all the links with the session.
- If NO-ERROR is non-nil, don't throw an error if OBJECT is not found in any
- session. This is useful if there are several \"concurrent\" parties which
- can remove the object."
- (let* ((system (or system (sesman--system)))
- (session (if session-name
- (sesman-session system session-name)
- (sesman-session-for-object system object no-error)))
- (new-session (delete object session)))
- (cond ((null new-session))
- ((= (length new-session) 1)
- (when auto-unregister
- (sesman-unregister system session)))
- (t
- (puthash (cons system (car session)) new-session sesman-sessions-hashmap)))))
-
- (defun sesman-session-for-object (system object &optional no-error)
- "Retrieve SYSTEM session which contains OBJECT.
- When NO-ERROR is non-nil, don't throw an error if OBJECT is not part of any
- session. In such case, return nil."
- (let* ((system (or system (sesman--system)))
- (sessions (sesman--all-system-sessions system)))
- (or (seq-find (lambda (ses)
- (seq-find (lambda (x) (equal object x)) (cdr ses)))
- sessions)
- (unless no-error
- (error "%s is not part of any %s sessions"
- object system)))))
-
- (defun sesman-session-name-for-object (system object &optional no-error)
- "Retrieve the name of the SYSTEM's session containing OBJECT.
- When NO-ERROR is non-nil, don't throw an error if OBJCECT is not part of
- any session. In such case, return nil."
- (car (sesman-session-for-object system object no-error)))
-
- (defun sesman-more-recent-p (bufs1 bufs2)
- "Return t if BUFS1 is more recent than BUFS2.
- BUFS1 and BUFS2 are either buffers or lists of buffers. When lists of
- buffers, most recent buffers from each list are considered. To be used
- primarily in `sesman-more-relevant-p' methods when session objects are
- buffers."
- (let ((bufs1 (if (bufferp bufs1) (list bufs1) bufs1))
- (bufs2 (if (bufferp bufs2) (list bufs2) bufs2)))
- (eq 1 (seq-some (lambda (b)
- (if (member b bufs1)
- 1
- (when (member b bufs2)
- -1)))
- (buffer-list)))))
-
- ;; path caching because file-truename is very slow
- (defvar sesman--path-cache (make-hash-table :test #'equal))
- (defun sesman-expand-path (path)
- "Expand PATH with optionally follow symlinks.
- Whether symlinks are followed is controlled by `sesman-follow-symlinks' custom
- variable. Always return the expansion without the trailing directory slash."
- (directory-file-name
- (if sesman-follow-symlinks
- (let ((true-name (or (gethash path sesman--path-cache)
- (puthash path (file-truename path) sesman--path-cache))))
- (if (or (eq sesman-follow-symlinks t)
- vc-follow-symlinks)
- true-name
- ;; sesman-follow-symlinks is 'vc but vc-follow-symlinks is nil
- (expand-file-name path)))
- (expand-file-name path))))
-
- ;;; Contexts
-
- (cl-defgeneric sesman-context (_cxt-type _system)
- "Given SYSTEM and context type CXT-TYPE return the context.")
- (cl-defmethod sesman-context ((_cxt-type (eql buffer)) _system)
- "Return current buffer."
- (current-buffer))
- (cl-defmethod sesman-context ((_cxt-type (eql directory)) _system)
- "Return current directory."
- (sesman-expand-path default-directory))
- (cl-defmethod sesman-context ((_cxt-type (eql project)) system)
- "Return current project."
- (let* ((default-directory (sesman-expand-path default-directory))
- (proj (or
- (sesman-project (or system (sesman--system)))
- ;; Normally we would use (project-roots (project-current)) but currently
- ;; project-roots fails on nil and doesn't work on custom `('foo .
- ;; "path/to/project"). So, use vc as a fallback and don't use project.el at
- ;; all for now.
- ;; NB: `vc-root-dir' doesn't work from symlinked files. Emacs Bug?
- (vc-root-dir))))
- (when proj
- (expand-file-name proj))))
-
- (cl-defgeneric sesman-relevant-context-p (_cxt-type cxt)
- "Non-nil if context CXT is relevant to current context of type CXT-TYPE.")
- (cl-defmethod sesman-relevant-context-p ((_cxt-type (eql buffer)) buf)
- "Non-nil if BUF is `current-buffer'."
- (eq (current-buffer) buf))
- (cl-defmethod sesman-relevant-context-p ((_cxt-type (eql directory)) dir)
- "Non-nil if DIR is the parent or equals the `default-directory'."
- (when (and dir default-directory)
- (string-match-p (concat "^" (sesman-expand-path dir))
- (sesman-expand-path default-directory))))
- (cl-defmethod sesman-relevant-context-p ((_cxt-type (eql project)) proj)
- "Non-nil if PROJ is the parent or equal to the `default-directory'."
- (when (and proj default-directory)
- (string-match-p (concat "^" (sesman-expand-path proj))
- (sesman-expand-path default-directory))))
-
- (defun sesman-relevant-link-p (link &optional cxt-types)
- "Return non-nil if LINK is relevant to the current context.
- If CXT-TYPES is non-nil, only check relevance for those contexts."
- (when (or (null cxt-types)
- (member (sesman--lnk-context-type link) cxt-types))
- (sesman-relevant-context-p
- (sesman--lnk-context-type link)
- (sesman--lnk-value link))))
-
- (defun sesman-relevant-session-p (system session &optional cxt-types)
- "Return non-nil if SYSTEM's SESSION is relevant to the current context.
- If CXT-TYPES is non-nil, only check relevance for those contexts."
- (seq-some #'sesman-relevant-link-p
- (sesman-links system session cxt-types)))
-
- (define-obsolete-function-alias 'sesman-linked-sessions 'sesman--linked-sessions "v0.3.2")
-
- (provide 'sesman)
-
- ;;; sesman.el ends here
|