Archived
114 lines
4.2 KiB
EmacsLisp
Executable File
114 lines
4.2 KiB
EmacsLisp
Executable File
;;; init-project.el --- Project switching, scoped to ~/Projects -*- lexical-binding: t; -*-
|
|
|
|
;;; Commentary:
|
|
;; Built on built-in project.el, no third-party manager. Projects are the
|
|
;; direct children of ~/Projects, rescanned authoritatively each startup so
|
|
;; the list never accumulates stale entries. A "pin" keeps the current
|
|
;; project from hijacking to a dependency's checkout on jump-to-definition.
|
|
|
|
;;; Code:
|
|
|
|
(require 'project)
|
|
|
|
;; A directory is a project root if under VC or containing one of these.
|
|
(setq project-vc-extra-root-markers '("build.sh"))
|
|
|
|
(defvar my/project-search-dirs
|
|
'("~/Projects")
|
|
"Parent directories scanned for known projects.")
|
|
|
|
;;;; Project list: rebuilt from a fresh scan each startup
|
|
;; project-remember-project would APPEND to the persisted list and leak stale
|
|
;; entries (old paths, stdlib dirs, the config repo). Instead scan ~/Projects
|
|
;; and assign project--list directly; a real list also stops project.el
|
|
;; re-reading the stale file this session.
|
|
|
|
(defun my/scan-projects-under (parent)
|
|
"Return `project--list' entries for direct child projects of PARENT.
|
|
Ignore symlinks; don't recurse."
|
|
(when (file-directory-p parent)
|
|
(let (entries)
|
|
(dolist (dir (directory-files parent t directory-files-no-dot-files-regexp))
|
|
(when (and (file-directory-p dir)
|
|
(not (file-symlink-p dir)))
|
|
(let ((default-directory dir))
|
|
(when-let ((project (project-current nil)))
|
|
(push (list (abbreviate-file-name (project-root project)))
|
|
entries)))))
|
|
entries)))
|
|
|
|
(setq project--list
|
|
(sort (mapcan #'my/scan-projects-under
|
|
(mapcar #'expand-file-name my/project-search-dirs))
|
|
(lambda (a b)
|
|
(time-less-p
|
|
(file-attribute-modification-time (file-attributes (car b)))
|
|
(file-attribute-modification-time (file-attributes (car a)))))))
|
|
|
|
;; Persist so the on-disk file matches (no stale artifact left behind).
|
|
(project--write-project-list)
|
|
|
|
;;;; Sticky project detection
|
|
;; project.el recomputes the project from the visited buffer's directory, so
|
|
;; jumping into a dependency's VC checkout (via `gd') would make THAT the
|
|
;; project. Instead pin the last project under our roots and reuse it for any
|
|
;; buffer outside them.
|
|
|
|
(defvar my/pinned-project nil
|
|
"Last project resolved from a buffer under `my/project-search-dirs'.")
|
|
|
|
(defun my/own-project-dir-p (dir)
|
|
"Return non-nil when DIR lives under one of `my/project-search-dirs'."
|
|
(and dir
|
|
(seq-some (lambda (parent)
|
|
(file-in-directory-p dir (expand-file-name parent)))
|
|
my/project-search-dirs)))
|
|
|
|
(defun my/project-pinned (dir)
|
|
"Resolve DIR to the pinned project when DIR is outside our roots.
|
|
Runs ahead of `project-try-vc', so foreign buffers never become current."
|
|
(and my/pinned-project
|
|
(not (my/own-project-dir-p dir))
|
|
my/pinned-project))
|
|
|
|
(defun my/remember-pinned-project (project)
|
|
"Record PROJECT as the pin when the current buffer is one of ours."
|
|
(when (and project (my/own-project-dir-p default-directory))
|
|
(setq my/pinned-project project))
|
|
project)
|
|
|
|
(add-hook 'project-find-functions #'my/project-pinned) ; before project-try-vc
|
|
(advice-add 'project-current :filter-return #'my/remember-pinned-project)
|
|
|
|
;;;; Switching and cleanup
|
|
|
|
;; Skip the "what do you want to do" menu entirely.
|
|
(setq project-switch-commands #'project-dired)
|
|
|
|
;; Widen what counts as "belongs to this project" for cleanup.
|
|
(setq project-kill-buffer-conditions
|
|
'(buffer-file-name
|
|
(major-mode . compilation-mode)
|
|
(major-mode . dired-mode)
|
|
(major-mode . shell-mode)
|
|
(major-mode . eshell-mode)
|
|
(major-mode . term-mode)))
|
|
|
|
(defun my/project-switch-fresh ()
|
|
"Save and close the current project before switching."
|
|
(interactive)
|
|
(when (project-current nil)
|
|
(save-some-buffers t)
|
|
(project-kill-buffers t))
|
|
(call-interactively #'project-switch-project))
|
|
|
|
(global-set-key (kbd "C-c C-p") #'my/project-switch-fresh)
|
|
|
|
(with-eval-after-load 'which-key
|
|
(which-key-add-key-based-replacements
|
|
"C-c C-p" "switch project (fresh)"
|
|
"C-x p" "project"))
|
|
|
|
(provide 'init-project)
|
|
;;; init-project.el ends here
|