home

My NixOS systems configurations.
Log | Files | Refs | LICENSE

project-x.el (8980B)


      1 ;;; project-x.el --- Extra convenience features for project.el -*- lexical-binding: t -*-
      2 
      3 ;; Copyright (C) 2021  Karthik Chikmagalur
      4 
      5 ;; Author: Karthik Chikmagalur <karthik.chikmagalur@gmail.com>
      6 ;; URL: https://github.com/karthink/project-x
      7 ;; Version: 0.1.5
      8 ;; Package-Requires: ((emacs "27.1"))
      9 
     10 ;; This file is NOT part of GNU Emacs.
     11 
     12 ;; This file is free software; you can redistribute it and/or modify
     13 ;; it under the terms of the GNU General Public License as published by
     14 ;; the Free Software Foundation; either version 3, or (at your option)
     15 ;; any later version.
     16 ;;
     17 ;; This program is distributed in the hope that it will be useful,
     18 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
     19 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     20 ;; GNU General Public License for more details.
     21 ;;
     22 ;; For a full copy of the GNU General Public License
     23 ;; see <http://www.gnu.org/licenses/>.
     24 ;;
     25 ;;; Commentary:
     26 ;;
     27 ;; project-x provides some convenience features for project.el:
     28 ;; - Recognize any directory with a `.project' file as a project.
     29 ;; - Save and restore project files and window configurations across sessions
     30 ;;
     31 ;; COMMANDS:
     32 ;;
     33 ;; project-x-window-state-save : Save the window configuration of currently open project buffers
     34 ;; project-x-window-state-load : Load a previously saved project window configuration
     35 ;;
     36 ;; CUSTOMIZATION:
     37 ;;
     38 ;; `project-x-window-list-file': File to store project window configurations
     39 ;; `project-x-local-identifier': String matched against file names to decide if a
     40 ;; directory is a project
     41 ;; `project-x-save-interval': Interval in seconds between autosaves of the
     42 ;; current project.
     43 ;;
     44 ;; by Karthik Chikmagalur
     45 ;; <karthik.chikmagalur@gmail.com>
     46 
     47 ;;; Code:
     48 
     49 (require 'project)
     50 (eval-when-compile (require 'subr-x))
     51 (eval-when-compile (require 'seq))
     52 (defvar project-prefix-map)
     53 (defvar project-switch-commands)
     54 (declare-function project-prompt-project-dir "project")
     55 (declare-function project--buffer-list "project")
     56 (declare-function project-buffers "project")
     57 
     58 (defgroup project-x nil
     59   "Convenience features for the Project library."
     60   :group 'project)
     61 
     62 ;; Persistent project sessions
     63 ;; -------------------------------------
     64 (defcustom project-x-window-list-file
     65   (locate-user-emacs-file "project-window-list")
     66   "File in which to save project window configurations by default."
     67   :type 'file
     68   :group 'project-x)
     69 
     70 (defcustom project-x-save-interval nil
     71   "Saves the current project state with this interval.
     72 
     73 When set to nil auto-save is disabled."
     74   :type '(choice (const :tag "Disabled" nil)
     75                  integer)
     76   :group 'project-x)
     77 
     78 (defvar project-x-window-alist nil
     79   "Alist of window configurations associated with known projects.")
     80 
     81 (defvar project-x-save-timer nil
     82   "Timer for auto-saving project state.")
     83 
     84 (defun project-x--window-state-write (&optional file)
     85   "Write project window states to `project-x-window-list-file'.
     86 If FILE is specified, write to it instead."
     87   (when project-x-window-alist
     88     (require 'pp)
     89     (unless file (make-directory (file-name-directory project-x-window-list-file) t))
     90     (with-temp-file (or file project-x-window-list-file)
     91       (insert ";;; -*- lisp-data -*-\n")
     92       (let ((print-level nil) (print-length nil))
     93         (pp project-x-window-alist (current-buffer))))
     94     (message (format "Wrote project window state to %s" project-x-window-list-file))))
     95 
     96 (defun project-x--window-state-read (&optional file)
     97   "Read project window states from `project-x-window-list-file'.
     98 If FILE is specified, read from it instead."
     99   (and (or file
    100            (file-exists-p project-x-window-list-file))
    101        (with-temp-buffer
    102          (insert-file-contents (or file project-x-window-list-file))
    103          (condition-case nil
    104              (if-let ((win-state-alist (read (current-buffer))))
    105                  (setq project-x-window-alist win-state-alist)
    106                (message (format "Could not read %s" project-x-window-list-file)))
    107            (error (message (format "Could not read %s" project-x-window-list-file)))))))
    108 
    109 (defun project-x-window-state-save (&optional arg)
    110   "Save current window state of project.
    111 With optional prefix argument ARG, query for project."
    112   (interactive "P")
    113   (when-let* ((dir (cond (arg (project-prompt-project-dir))
    114                          ((project-current)
    115                           (project-root (project-current)))))
    116               (default-directory dir))
    117     (unless project-x-window-alist (project-x--window-state-read))
    118     (let ((file-list))
    119       ;; Collect file-list of all the open project buffers
    120       (dolist (buf
    121                (funcall (if (fboundp 'project--buffers-list)
    122                             #'project--buffers-list
    123                           #'project-buffers)
    124                         (project-current))
    125                file-list)
    126         (if-let ((file-name (or (buffer-file-name buf)
    127                                 (with-current-buffer buf
    128                                   (and (derived-mode-p 'dired-mode)
    129                                        dired-directory)))))
    130             (push file-name file-list)))
    131       (setf (alist-get dir project-x-window-alist nil nil 'equal)
    132             (list (cons 'files file-list)
    133                   (cons 'windows (window-state-get nil t)))))
    134     (message (format "Saved project state for %s" dir))))
    135 
    136 (defun project-x-window-state-load (dir)
    137   "Load the saved window state for project with directory DIR.
    138 If DIR is unspecified query the user for a project instead."
    139   (interactive (list (project-prompt-project-dir)))
    140   (unless project-x-window-alist (project-x--window-state-read))
    141   (if-let* ((project-x-window-alist)
    142             (project-state (alist-get dir project-x-window-alist
    143                                       nil nil 'equal)))
    144       (let ((file-list (alist-get 'files project-state))
    145             (window-config (alist-get 'windows project-state)))
    146         (dolist (file-name file-list nil)
    147           (find-file file-name))
    148         (window-state-put window-config nil 'safe)
    149         (message (format "Restored project state for %s" dir)))
    150     (message (format "No saved window state for project %s" dir))))
    151 
    152 (defun project-x-windows ()
    153   "Restore the last saved window state of the chosen project."
    154   (interactive)
    155   (project-x-window-state-load (project-root (project-current))))
    156 
    157 ;; Recognize directories as projects by defining a new project backend `local'
    158 ;; -------------------------------------
    159 (defcustom project-x-local-identifier ".project"
    160   "Filename(s) that identifies a directory as a project.
    161 
    162 You can specify a single filename or a list of names."
    163   :type '(choice (string :tag "Single file")
    164                  (repeat (string :tag "Filename")))
    165   :group 'project-x)
    166 
    167 (cl-defmethod project-root ((project (head local)))
    168   "Return root directory of current PROJECT."
    169   (cdr project))
    170 
    171 (defun project-x-try-local (dir)
    172   "Determine if DIR is a non-VC project.
    173 DIR must include a .project file to be considered a project."
    174   (if-let ((root (if (listp project-x-local-identifier)
    175                      (seq-some (lambda (n)
    176                                  (locate-dominating-file dir n))
    177                                project-x-local-identifier)
    178                    (locate-dominating-file dir project-x-local-identifier))))
    179       (cons 'local root)))
    180 
    181 ;;;###autoload
    182 (define-minor-mode project-x-mode
    183   "Minor mode to enable extra convenience features for project.el.
    184 When enabled, save and load project window states.
    185 Recognize any directory that contains (or whose parent
    186 contains) a special file as a project."
    187   :global t
    188   :version "0.10"
    189   :lighter ""
    190   :group 'project-x
    191   (if project-x-mode
    192       ;;Turning the mode ON
    193       (progn
    194         (add-hook 'project-find-functions 'project-x-try-local 90)
    195         (add-hook 'kill-emacs-hook 'project-x--window-state-write)
    196         (project-x--window-state-read)
    197         (define-key project-prefix-map (kbd "w") 'project-x-window-state-save)
    198         (define-key project-prefix-map (kbd "j") 'project-x-window-state-load)
    199         (if (listp project-switch-commands)
    200             (add-to-list 'project-switch-commands
    201                          '(?j "Restore windows" project-x-windows) t)
    202           (message "`project-switch-commands` is not a list, not adding 'restore windows' command"))
    203         (when project-x-save-interval
    204           (setq project-x-save-timer
    205                 (run-with-timer 0 (max project-x-save-interval 5)
    206                                 #'project-x-window-state-save))))
    207     (remove-hook 'project-find-functions 'project-x-try-local 90)
    208     (remove-hook 'kill-emacs-hook 'project-x--window-state-write)
    209     (define-key project-prefix-map (kbd "w") nil)
    210     (define-key project-prefix-map (kbd "j") nil)
    211     (when (listp project-switch-commands)
    212       (delete '(?j "Restore windows" project-x-windows) project-switch-commands))
    213     (when (timerp project-x-save-timer)
    214       (cancel-timer project-x-save-timer))))
    215 
    216 (provide 'project-x)
    217 ;;; project-x.el ends here