Initial commit for v1.0 of Simply Emacs.

This commit is contained in:
2026-07-24 09:47:24 +02:00
commit 7774e58c79
13 changed files with 1244 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
;;; 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