init-project.el 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. ;;; init-project.el --- Project (and Perspective) Configuration File -*- lexical-binding: t -*-
  2. ;;; Commentary:
  3. ;;; Code:
  4. (require 'subr-x)
  5. (use-package project
  6. :ensure (:ref "87db670d045bea2d90139b1f741eea8db7c193ea" :pin t)
  7. :config
  8. (defun project--clojure-switch-to-test (filename project-root)
  9. (let* ((project-src-file (string-remove-prefix project-root filename))
  10. (project-test-file (replace-regexp-in-string "\.clj$" "_test.clj"
  11. (replace-regexp-in-string "^src/" "test/" project-src-file))))
  12. (find-file (expand-file-name project-test-file project-root))))
  13. (defun project--clojure-switch-to-src (test-filename project-root)
  14. (let* ((project-test-file (string-remove-prefix project-root test-filename))
  15. (project-src-file (replace-regexp-in-string "_test\.clj$" ".clj"
  16. (replace-regexp-in-string "^test/" "src/" project-test-file))))
  17. (find-file (expand-file-name project-src-file project-root))))
  18. (defun project-clojure-test-switch ()
  19. (interactive)
  20. (let ((filename (buffer-file-name))
  21. (project-root (expand-file-name (project-root (project-current)))))
  22. (cond ((string-match (concat "^" project-root "test/.*_test\.clj") filename)
  23. (project--clojure-switch-to-src filename project-root))
  24. ((string-match (concat "^" project-root "src/.*\.clj") filename)
  25. (project--clojure-switch-to-test filename project-root)))))
  26. (defun project-recentf ()
  27. "Show a list of recently visited files in a project."
  28. (interactive)
  29. (if (boundp 'recentf-list)
  30. ;; Use expand-file-name for project-root and later recentf-list to ensure consistency
  31. (let* ((project-root (expand-file-name (project-root (project-current))))
  32. (project-recentf-files (mapcar
  33. (lambda (f) (file-relative-name f project-root))
  34. (seq-filter (apply-partially 'string-prefix-p project-root)
  35. (mapcar 'expand-file-name recentf-list)))))
  36. (find-file (expand-file-name
  37. (funcall project-read-file-name-function
  38. "Find recent project files"
  39. project-recentf-files
  40. nil
  41. 'file-name-history)
  42. project-root)))
  43. (message "recentf is not enabled")))
  44. (defun project-switch-src-project ()
  45. (interactive)
  46. (let ((default-directory "~/src/"))
  47. (call-interactively #'project-switch-project)))
  48. (defun project-switch-consult-project-extra-find ()
  49. (interactive)
  50. (progn
  51. (setq unread-command-events (listify-key-sequence "f "))
  52. (consult-project-extra-find)))
  53. (add-to-list 'project-switch-commands '(?h "Recentf" project-recentf) t)
  54. (add-to-list 'project-switch-commands '(?r "consult-ripgrep" consult-ripgrep) t)
  55. (add-to-list 'project-switch-commands '(?p "consult-project-extra-find" project-switch-consult-project-extra-find) t)
  56. (add-to-list 'project-switch-commands '(?m "Magit" magit-status) t)
  57. (add-to-list 'project-switch-commands '(?q "Replace Regexp" project-query-replace-regexp) t)
  58. ;; project-root and project-try-local copied/modified from https://github.com/karthink/project-x/blob/master/project-x.el
  59. (cl-defmethod project-root ((project (head local)))
  60. "Return root directory of current PROJECT."
  61. (cdr project))
  62. (defun project-try-local (dir)
  63. "Treat DIR as a project if it contains a .project file."
  64. (if-let ((root (locate-dominating-file dir ".project")))
  65. (cons 'local root)))
  66. ;; Add this hook last so so that vc takes precedence over local
  67. (add-hook 'project-find-functions 'project-try-local 90)
  68. :commands project-prompt-project-dir
  69. :bind
  70. ("C-x p P" . project-switch-src-project)
  71. ("C-x p M-p" . project-switch-project)
  72. ("C-x f" . project-recentf))
  73. (use-package perspective
  74. :custom (persp-mode-prefix-key (kbd "C-c x"))
  75. :config
  76. ;; Based on jao-buffer-same-mode (https://jao.io/blog/2021-09-08-high-signal-to-noise-emacs-command.html)
  77. (defun persp-switch-buffer-same-mode ()
  78. "Switch to a buffer with the same major mode as the current buffer, respecting
  79. the current perspective."
  80. (interactive)
  81. (let* ((mode major-mode)
  82. (pred (lambda (b)
  83. (let ((b (get-buffer (if (consp b) (car b) b))))
  84. (eq (buffer-local-value 'major-mode b) mode)))))
  85. (pop-to-buffer (persp-read-buffer "Buffer: " nil t pred))))
  86. (defun persp-previous-buffer-same-mode (&optional switch-to-buffer-function)
  87. "Switch to the previous buffer in the current perspective, with the same major
  88. mode as the current buffer (or do nothing)."
  89. (interactive)
  90. (let* ((switch-buffer-fn (or switch-to-buffer-function #'switch-to-buffer))
  91. (persp-buffers (seq-filter 'persp-is-current-buffer (buffer-list)))
  92. (mode major-mode)
  93. (mode-pred (lambda (b)
  94. (let ((b (get-buffer (if (consp b) (car b) b))))
  95. (and (eq (buffer-local-value 'major-mode b) mode)
  96. (not (eq b (current-buffer)))
  97. (not (get-buffer-window b))))))
  98. (persp-buffers-in-mode (seq-filter mode-pred persp-buffers)))
  99. (when (not (seq-empty-p persp-buffers-in-mode))
  100. (funcall switch-buffer-fn (car persp-buffers-in-mode)))))
  101. (defun persp-previous-buffer-same-mode-other-window ()
  102. "Variant of persp-previous-buffer-same-mode, which opens in other window."
  103. (interactive)
  104. (persp-previous-buffer-same-mode #'switch-to-buffer-other-window))
  105. ;; Inspired by crux-switch-to-previous-buffer
  106. (defun persp-switch-to-previous-buffer ()
  107. "Switch to the previous buffer in the current perspective."
  108. (interactive)
  109. (switch-to-buffer (persp-other-buffer (current-buffer) 1)))
  110. (defun persp-current-project-root ()
  111. "Return the current project root, falling back to finding it by the perpsective"
  112. (if-let (project (project-current))
  113. (project-root project)
  114. (when-let (persp (persp-name (persp-curr)))
  115. (car (seq-filter (lambda (pr) (string-match-p persp pr)) (project-known-project-roots))))))
  116. (defun switch-project (proj)
  117. "Switch to project or already open project perspective."
  118. (interactive (list (let ((default-directory "~/src/")) ;; TODO make this customisable
  119. (project-prompt-project-dir))))
  120. (let* ((persp-name (file-name-nondirectory (directory-file-name proj)))
  121. (persp (gethash persp-name (perspectives-hash))))
  122. (unless (equal persp (persp-curr))
  123. (unwind-protect
  124. (progn
  125. ;; Create or switch to a perspective named after the project
  126. (persp-switch persp-name)
  127. ;; If the perspective did not exist, switch to the project
  128. (when (not persp)
  129. (project-switch-project proj)))
  130. ;; If the only buffer is the persp scratch buffer, it's safe to kill the perspective if switching project was cancelled
  131. (when (seq-empty-p
  132. (seq-filter
  133. (lambda (b) (not (equal (buffer-name b) (persp-scratch-buffer))))
  134. (persp-buffers (persp-curr))))
  135. (persp-kill persp-name))))))
  136. :bind
  137. ("C-x p p" . switch-project)
  138. ("C-c C-M-b" . persp-switch-to-previous-buffer)
  139. ("C-x C-b" . persp-previous-buffer-same-mode)
  140. ("C-x 4 C-b" . persp-previous-buffer-same-mode-other-window)
  141. ("C-x C-S-b" . persp-switch-buffer-same-mode)
  142. ("C-c x x" . persp-switch-last)
  143. ("C-c x ." . persp-switch-quick)
  144. :hook (elpaca-after-init . persp-mode))
  145. (provide 'init-project)
  146. ;;; init-project.el ends here