emacs: Managing projects
Working on project a key part of my workflow when using GNU/Emacs. Almost everything I
work on can be part of a project. It might be simpler to give examples:
tektoncd/pipeline
checked out is a project, my ~/desktop/org
is another project. There
is only a handful of buffer in Emacs that I do not consider of any project, one example is
the org-mode agenda.
In a project, I want to be able to:
- List all files in that project
- optionally spiking the files from
.gitignore
- optionally spiking the files from
- Search in all project files
- using
ripgrep
or something else - possibly to a search and replace
- using
- Run commands on the project root folder
- could be a compilation, some tests, some random commands
- Manage the version control (using
magit
)- adding files, switch branches, …
- Switch between project buffers
Emacs 27.1 ships with a project
library that has some useful function. But prior to this,
the projectile
project has been the way to go for managing projects in
GNU/Emacs. projectile
is also quite extensible and integrates relatively well with a
bunch of other libraries.
The mnemonics key for the project is C-c p
, and thus, any project command will start with
that prefix.
Projectile
Let’s configure projectile
using use-package
.
(use-package projectile :commands (projectile-ack projectile-ag projectile-compile-project projectile-configure-project projectile-package-project projectile-install-project projectile-test-project projectile-run-project projectile-dired projectile-find-dir projectile-find-file projectile-find-file-dwim projectile-find-file-in-directory projectile-find-tag projectile-test-project projectile-grep projectile-invalidate-cache projectile-kill-buffers projectile-multi-occur projectile-project-p projectile-project-root projectile-recentf projectile-regenerate-tags projectile-replace projectile-replace-regexp projectile-run-async-shell-command-in-root projectile-run-shell-command-in-root projectile-switch-project projectile-switch-to-buffer projectile-vc projectile-commander) :bind-keymap ("C-c p" . projectile-command-map) :config <<projectile-completion>> <<projectile-variables>> <<projectile-compilation>> <<projectile-known-projects>> <<projectile-commander-methods>> <<projectile-custom-types>> (projectile-mode))
First thing first, let’s tell projectile
to use the default completion system instead of
ivy
, or helm
, or …
(setq-default projectile-completion-system 'default)
Let’s also configure some projectile
behavior.
- The default action when switch to a project should be the
commander
, as it allows to do different actions. This is done by settingprojectile-command
toproject-switch-project-action
. - When switching to tests (
C-c p t
), if there is no test files, create one. This is done by settingprojectile-create-missing-test-files
.
(setq-default projectile-switch-project-action #'projectile-commander
projectile-create-missing-test-files t)
In order to make sure we can have a living compilation per project, we need to modify the
buffer name to include the project name. This is easily do-able by writing a function for
compilation-buffer-name-function
.
(setq-default compilation-buffer-name-function (lambda (mode) (concat "*" (downcase mode) ": " (projectile-project-name) "*")))
Do not track known projects automatically, instead call projectile-add-known-project Remove dead projects when Emacs is idle
(setq-default projectile-track-known-projects-automatically nil)
(run-with-idle-timer 10 nil #'projectile-cleanup-known-projects)
(def-projectile-commander-method ?s "Open a *shell* buffer for the project" (projectile-run-eshell nil)) (def-projectile-commander-method ?c "Run `compile' in the project" (projectile-compile-project nil))
Custom project types
projectile
allows to add custom project type, in addition to the built-in project
types. See Projects - Projectile: The Project Interaction Library for Emacs for a little
bit more detail. It should be possible to configure the configure
, compile
, package
,
install
and test
commands. One a the hope of this section is to be able to define highly
customized project types so that doing C-p u
on, let’s say, tektoncd/pipeline
does the
right thing by default.
An example of custom project type is the following.
;; Ruby + RSpec (projectile-register-project-type 'ruby-rspec '("Gemfile" "lib" "spec") :project-file "Gemfile" :compile "bundle exec rake" :src-dir "lib/" :test "bundle exec rspec" :test-dir "spec/" :test-suffix "_spec")
One nice aspect of :compile
(and some others) is that it can take a symbolic reference to
a function, which means, you can define dynamic behavior. Based on the doc this works for
:compile
, :configure
, :compilation-dir
and :run
(but my hope is it would work for :test
and that a :package
and an :install
would exist).
TODO ko
First thing first, what makes a ko
project. In most cases, a .ko.yaml
will be present (at
the root folder of the project). Let’s also define a function do detect if a it’s a ko
project that uses the standard config
folder for yamls.
(defun projectile-ko-project-p () "Check if a project contains a .ko.yaml file." (projectile-verify-file ".ko.yaml")) (defun projectile-ko-with-config-project-p () "Check if a project is a ko project and has a config/ folder full of yaml" (and (projectile-ko-project-p) (projectile-verify-file-wildcard "config/*.yaml")))
Let’s register the ko
project (with config). Long-term, the idea is to make different
function for ko
and ko-with-config
projects.
(projectile-register-project-type 'ko-with-config #'projectile-ko-with-config-project-p :project-file ".ko.yaml" ; might not be required :configure 'projectile-ko-configure-command :compile 'projectile-ko-compile-command :test 'projectile-ko-test-command :run 'projectile-ko-run-command :package 'projectile-ko-package-command :install 'projectile-ko-install-command)
Let’s now dig a little bit more into the configure, compile, test, run, package and
install commands. As we can pass it a function, we can define behaviour depending on the
current opened buffer, etc. One assumption that we can make is that a ko
project is also a
go
project.
- configure
configure stands for
./configure
scripts usually. Let’s see what it could be forko
project. Most likely related to file generations.- default to
./hack/update-codegen.sh
if it is present.
(defun projectile-ko-configure-command () "define a configure command for a ko project, depending on the opened file" (cond ((projectile-file-exists-p "hack/update-codegen.sh") "./hack/update-codegen.sh")))
- default to
- compile
compile might be slightly different depending on the current major mode we are in, and maybe also depending on the folder.
- default to
go build -v ./...
go
file (go-mode
)- default to build the current package
- if it is a test file, tests the current package
(defun projectile-ko-compile-command () "define a compile command for a ko project, depending on the openend file " (cond ((eq major-mode 'go-mode) (projectile-ko-compile-command-go)) ((eq major-mode 'yaml-mode) "yamllint .") (t "go build -v ./...") )) (defun projectile-ko-compile-command-go () "compile command for a ko project if in a go file" (let* ((current-file (buffer-file-name (current-buffer))) (relative-current-file (file-relative-name current-file (projectile-project-root))) (relative-current-folder (file-name-directory relative-current-file))) (message relative-current-file) (cond ((string-suffix-p "_test.go" relative-current-file) (format "go test -c -v ./%s" relative-current-folder)) (t (format "go build -v ./%s" relative-current-folder)))))
- default to
- test
test might be slightly different depending on the current major mode we are in, and might depend on the folder.
- default to
go test -v ./...
go
file (go-mode
)- default to run tests on the current package
- if it is a test file, tests the current file (like
go-test-current-file
orgotest-ui-current-file
)
(defun projectile-ko-test-command () "define a test command for a ko project, depending on the openend file" (cond ((eq major-mode 'go-mode) (projectile-ko-test-command-go)) (t "go test -v ./..."))) (defun projectile-ko-test-command-go () "test command for a ko project if in a go file" (let* ((current-file (buffer-file-name (current-buffer))) (relative-current-file (file-relative-name current-file (projectile-project-root))) (relative-current-folder (file-name-directory relative-current-file))) (cond ((string-suffix-p "_test.go" relative-current-file) (projectile-ko-command-go-test relative-current-file)) (t (format "go test -v ./%s" relative-current-folder))))) (defun projectile-ko-command-go-test (current-file) "get the command for a go test" (cond ((gotest-module-available-p) (projectile-ko-command-go-test-gotest current-file)) (t (format "go test -v ./%s" current-file)))) (defun projectile-ko-command-go-test-gotest (current-file) "get the command for a go test with gotest module enabled" (message default-directory) (let ((data (go-test--get-current-file-testing-data))) (format "go test -run='%s' -v ./%s" data (file-name-directory current-file)))) (defun gotest-module-available-p () "is go-test module available" (fboundp 'go-test--get-current-file-data))
- default to
- run
run is usually about running the project binary or something.
(defun projectile-ko-run-command () "define a run command for a ko project, depending on the openend file " (cond ((eq major-mode 'go-mode) (projectile-ko-run-command-go)) ;; nothing by default ? )) (defun projectile-ko-run-command-go () "test command for a ko project if in a go file" (let* ((current-file (buffer-file-name (current-buffer))) (relative-current-file (file-relative-name current-file (projectile-project-root))) (relative-current-folder (file-name-directory relative-current-file))) (cond ((string-prefix-p "cmd/" relative-current-file) (format "go run ./%s" relative-current-folder)))))
- package
package is usually about generating a package, for a maven project this would be
mvn package
, for a project with aDockerfile
, this could be build the image(s). For ako
project this is about building and pushing the images that are going to be deployed. This is achieved by doing ako resolve
.(defun projectile-ko-package-command () "define a package command for a ko project, depending on the openend file " (cond ((eq major-mode 'go-mode) (projectile-ko-package-command-go)) (t "ko resolve --push=false --oci-layout-path=/tmp/oci -f config") )) (defun projectile-ko-package-command-go () "package command for a ko project if in a go file" (let* ((current-file (buffer-file-name (current-buffer))) (relative-current-file (file-relative-name current-file (projectile-project-root))) (relative-current-folder (file-name-directory relative-current-file))) (cond ((string-prefix-p "cmd/" relative-current-file) (format "ko publish --push=false ./%s" relative-current-folder)))))
- install
install is about installing the project artifact somewhere (usually
make install
)(defun projectile-ko-install-command () "define a install command for a ko project, depending on the openend file " "ko apply -f config/")
TODO Others
- Detect project type
.ko.yaml
=> run isko apply -f …
- is there a
Makefile
? tkn
andtekton
filehome
detection:systems
,users
,ci.nix
,shell.nix
,hosts.nix
,systems.nix
- if in
pkgs
, runnix-build pkgs -A …
, and try to detect the file derivations - if in
tools/emacs
(elisp), tangle files from~/desktop/org/notes
- detect
hostname
and act based on it:naruhodo
:make home-switch
, …wakasu
:make switch
, …- Could also detect using
nixos-version
- if in
- Hook projectile run/compile/test to multi-compile Group things together, so that I can either choose from a list of different compile options or run my command
asok/projectile-rails: Emacs Rails mode based on projectile is also quite interesting.
Configuration layout
Here we define the config-projects.el
file that gets generated by the source blocks in our Org
document. This is the file that actually gets loaded on startup. The placeholders in
angled brackets correspond to the NAME
directives above the SRC
blocks throughout this
document.
;;; config-projects.el --- -*- lexical-binding: t; -*- ;;; Commentary: ;;; Project related configuration. ;;; This is mainly using projectile now, but built-in projects module seems promising for long-term. ;;; Note: this file is autogenerated from an org-mode file. ;;; Code: <<projectile>> (provide 'config-projects) ;;; config-projects.el ends here