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
ripgrepor 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-commandtoproject-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
./configurescripts usually. Let’s see what it could be forkoproject. Most likely related to file generations.- default to
./hack/update-codegen.shif 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 ./... gofile (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 ./... gofile (go-mode)- default to run tests on the current package
- if it is a test file, tests the current file (like
go-test-current-fileorgotest-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 akoproject 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? tknandtektonfilehomedetection: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
hostnameand 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