dotfiles/emacs/.emacs.d/config/init-run-command.el

390 lines
18 KiB
EmacsLisp

;; -*- lexical-binding: t; -*-
;; A handy package for running external commands
(use-package run-command
:init
(defvar-local run-command-local-commands nil)
(put 'run-command-local-commands 'safe-local-variable (lambda (_) t))
(defvar-local run-command-project-local-commands nil)
(put 'run-command-project-local-commands 'safe-local-variable (lambda (_) t))
:config
(defun run-command-recipe-terraform ()
(when (directory-files default-directory
nil
(rx (one-or-more any)
".tf"
eol))
(list
(list :command-name "init"
:command-line "terraform init")
(list :command-name "plan"
:command-line "terraform plan")
(list :command-name "apply"
:command-line "terraform apply")
(list :command-name "destroy"
:command-line "terraform destroy"))))
(defun run-command-recipe-local ()
(when run-command-local-commands
(mapcar (cl-function
(lambda ((name . spec))
(let ((name (if (symbolp name) (symbol-name name) name)))
(list :command-name name
:command-line spec))))
run-command-local-commands)))
(defun run-command-recipe-package-json--get-scripts (package-json-file)
"Extract NPM scripts from `package-json-file'."
(with-temp-buffer
(insert-file-contents package-json-file)
(let* ((json-data (json-parse-buffer))
(script-hash (gethash "scripts" json-data))
(scripts '()))
(when script-hash
(maphash (lambda (key _value) (push key scripts)) script-hash))
scripts)))
(defun run-command-recipe-package-json ()
(when-let* ((project-dir
(locate-dominating-file default-directory "package.json"))
(scripts
(run-command-recipe-package-json--get-scripts (concat project-dir "package.json")))
(script-runner
(if (file-exists-p (concat project-dir "yarn.lock")) "yarn" "npm")))
(mapcar (lambda (script)
(list :command-name script
:command-line (concat script-runner " run " script)
:display script
:working-dir project-dir))
scripts)))
(defun makefile-target-list-default (makefile)
"Return the target list for MAKEFILE by parsing it."
(let (targets)
(with-temp-buffer
(insert-file-contents makefile)
(goto-char (point-min))
(while (re-search-forward "^\\([^: \n]+\\) *:\\(?: \\|$\\)" nil t)
(let ((str (match-string 1)))
(unless (string-match "^\\." str)
(push str targets)))))
(nreverse targets)))
(defun run-command-recipe-makefile ()
(when-let* ((project-dir (locate-dominating-file default-directory "Makefile"))
(makefile (concat project-dir "Makefile"))
(targets (makefile-target-list-default makefile)))
(mapcar (lambda (target)
(list :command-name target
:command-line (concat "make " target)
:display target
:working-dir project-dir))
targets)))
(defun run-command-recipe-project ()
(when (projectile-project-root)
(mapcar (lambda (cmd)
(when-let ((cmd-val (symbol-value
(intern
(format "projectile-project-%s-cmd" cmd)))))
(list :command-name cmd
:command-line cmd-val
:working-dir (projectile-compilation-dir))))
'("test" "run" "compilation" "configure" "install" "package"))))
(defun run-command-recipe-project-local ()
(when (and run-command-project-local-commands (projectile-project-root))
(mapcar (cl-function
(lambda ((name . spec))
(let ((name (if (symbolp name) (symbol-name name) name)))
(list :command-name name
:command-line spec
:working-dir (projectile-compilation-dir)))))
run-command-project-local-commands)))
(defun run-command-recipe-executables ()
(let* ((buffer-file (buffer-file-name))
(executable-p (and buffer-file (file-executable-p buffer-file))))
(list
(when executable-p
(list
:command-name "run-buffer-file"
:command-line buffer-file
:display "Run this buffer's file"))
(when (and executable-p (executable-find "entr"))
(list
:command-name "run-buffer-file-watch"
:command-line (format "echo %s | entr -c /_" buffer-file)
:display "Run this buffer's file (re-run on each save)")))))
(defun run-command-recipe-obelix ()
(when-let* ((config (or (locate-dominating-file default-directory "obelix.json")
(locate-dominating-file default-directory "obelix.edn")))
(dir (file-name-directory config)))
(list
(list :command-name "build"
:command-line "obelix build"
:working-dir dir)
(list :command-name "serve"
:command-line "obelix serve"
:working-dir dir))))
(defun run-command-recipe-kustomize ()
(when-let* ((kustomization (locate-dominating-file default-directory "kustomization.yaml"))
(dir (file-name-directory kustomization)))
(list
(list :command-name "build"
:command-line "kustomize build --enable_alpha_plugins"
:working-dir dir))))
(defun run-command-recipe-sops ()
(when (save-excursion
(goto-char (point-min))
(search-forward-regexp "sops:" nil t))
(list
(list :command-name "edit"
:command-line (format "sops %s" (buffer-file-name))))))
(defun run-command-recipe-pytest ()
(when (and (derived-mode-p 'python-mode)
(= 0 (call-process "python" nil nil nil "-c" "import pytest")))
(let ((test-file-p (string-match-p "test" (or (buffer-file-name) ""))))
(list
(when (and (projectile-project-root)
(file-exists-p (concat (file-name-as-directory (projectile-project-root))
"tests")))
(list :command-name "test all"
:command-line "pytest tests"
:working-dir (projectile-project-root)))
(when test-file-p
(list :command-name "test this file"
:command-line (format "pytest %s" (buffer-file-name))))
(when (and test-file-p (python-info-current-defun))
(list :command-name "test this function"
:command-line (format "pytest %s::%s"
(buffer-file-name)
(replace-regexp-in-string
"\\."
"::"
(python-info-current-defun)))))))))
(defun run-command-recipe-nosetests()
(when (and (derived-mode-p 'python-mode)
(= 0 (call-process "python" nil nil nil "-c" "import nose")))
(let ((test-file-p (string-match-p "test" (or (buffer-file-name) ""))))
(list
(when (and (projectile-project-root)
(file-exists-p (concat (file-name-as-directory (projectile-project-root))
"tests")))
(list :command-name "test all"
:command-line "nosetests -w tests"
:working-dir (projectile-project-root)))
(when test-file-p
(list :command-name "test this file"
:command-line (format "nosetests %s" (buffer-file-name))))
(when (and test-file-p (python-info-current-defun))
(list :command-name "test this function"
:command-line (format "nosetests %s:%s"
(buffer-file-name)
(python-info-current-defun))))))))
(defun run-command-recipe-web-ext ()
(when-let* ((_ (executable-find "web-ext"))
(manifest (locate-dominating-file default-directory "manifest.json"))
(dir (file-name-directory manifest)))
(list
(list :command-name "run"
:command-line "web-ext run"
:working-dir dir)
(list :command-name "build"
:command-line "web-ext build"
:working-dir dir))))
(defun run-command-recipe-pip ()
(when (and (buffer-file-name)
(string-match-p ".*requirements.*txt$" (buffer-file-name)))
(list
(list :command-name "install"
:command-line (format "pip install -r %s" (buffer-file-name))))))
(defun run-command-recipe-maven ()
(when-let* ((root-dir (or (projectile-project-root) default-directory))
(local-pom-dir (locate-dominating-file default-directory "pom.xml"))
(project-dir (locate-dominating-file root-dir "pom.xml"))
(commands (list
(list :command-name "validate"
:command-line "mvn validate"
:working-dir local-pom-dir)
(list :command-name "compile"
:command-line "mvn compile"
:working-dir local-pom-dir)
(list :command-name "clean compile"
:command-line "mvn clean compile"
:working-dir local-pom-dir)
(list :command-name "test"
:command-line "mvn test -DfailIfNoTests=false"
:working-dir local-pom-dir)
(list :command-name "package"
:command-line "mvn package"
:working-dir local-pom-dir)
(list :command-name "verify"
:command-line "mvn verify"
:working-dir local-pom-dir)
(list :command-name "install"
:command-line "mvn install"
:working-dir local-pom-dir)
(list :command-name "install (skip tests)"
:command-line "mvn install -DskipTests"
:working-dir local-pom-dir)
(list :command-name "deploy"
:command-line "mvn deploy"
:working-dir local-pom-dir)
(list :command-name "clean"
:command-line "mvn clean"
:working-dir local-pom-dir)
(list :command-name "clean verify"
:command-line "mvn clean verify"
:working-dir local-pom-dir)
(list :command-name "exec:java"
:command-line "mvn exec:java"
:working-dir local-pom-dir)
(list :command-name "clean compile exec:java"
:command-line "mvn clean compile exec:java"
:working-dir local-pom-dir)
(when-let ((test-class (and (buffer-file-name)
(let ((case-fold-search nil))
(string-match-p ".*\\(Test\\|IT\\).*\\.java$"
(buffer-file-name)))
(tree-sitter-fully-qualified-class-name
(point)))))
(list :command-name "test this class"
:command-line (format "mvn test -DfailIfNoTests=false -Dtest=%s" test-class)
:working-dir local-pom-dir))
(when-let* ((test-class (tree-sitter-fully-qualified-class-name (point)))
(method (tree-sitter-get-enclosing-function-name (point)))
(test-method (and (buffer-file-name)
(let ((case-fold-search nil))
(string-match-p ".*\\(Test\\|IT\\).*\\.java$"
(buffer-file-name)))
(member
"Test"
(tree-sitter-get-enclosing-annotations
(point)))
(format "%s#%s" test-class method))))
(list :command-name "test this method"
:command-line (format "mvn test -DfailIfNoTests=false -Dtest=%s" test-method)
:working-dir local-pom-dir)))))
(if (s-equals? local-pom-dir project-dir)
commands
(-concat commands
(-map (lambda (cmd)
(when cmd
(-> (-copy cmd)
(plist-put :command-name
(format "%s (root POM)"
(plist-get cmd :command-name)))
(plist-put :working-dir project-dir))))
commands)))))
(defun get-cargo-commands (dir)
(when (executable-find "cargo")
(with-temp-buffer
(cd dir)
(-as-> (shell-command-to-string "cargo --list") v
(split-string v "\n")
(cdr v)
(-filter (-not (-partial 's-contains-p "alias")) v)
(-map 's-trim v)
(-map (-partial 's-split " ") v)
(-map 'car v)
(-filter (-not 's-blank?) v)))))
(defun run-command-recipe-cargo ()
(when-let* ((project-dir (locate-dominating-file default-directory "Cargo.toml"))
(cargo-cmds (get-cargo-commands project-dir)))
(-concat (-map
(lambda (cmd)
(list :command-name cmd
:command-line (format "cargo %s" cmd)
:working-dir project-dir))
cargo-cmds)
(list
(list :command-name "doc --open"
:command-line "cargo doc --open"
:working-dir project-dir)
(list :command-name "build --release"
:command-line "cargo build --release"
:working-dir project-dir)))))
(defun run-command-recipe-rake ()
(when-let* ((rake-dir (or (locate-dominating-file default-directory "Rakefile")
(locate-dominating-file default-directory "Rakefile.rb")))
(cmds (->> (shell-command-to-string "rake -T")
(s-split "\n")
(-map (lambda (s) (s-split-up-to " " s 2)))
(-map (lambda (l) (s-join " " (-take 2 l))))
(-filter (-not 'string-empty-p)))))
(-map (lambda (cmd)
(list :command-name (cadr (s-split " " cmd))
:command-line cmd
:working-dir rake-dir))
cmds)))
(defvar run-command-recipe-scripts--script-dirs '("." "bin" "scripts"))
(defun get-script-run-command (file)
(with-temp-buffer
(insert-file-contents-literally file)
(when-let* ((line (thing-at-point 'line t))
(line-trimmed (s-trim line)))
(cadr (s-match (rx "#!" (group (1+ anychar))) line-trimmed)))))
(defun run-command-recipe-scripts ()
(let* ((root-dir (expand-file-name (or (projectile-project-root) default-directory)))
(shell (or (getenv "SHELL") "/usr/bin/env bash"))
(scripts (-mapcat
(lambda (dir)
(let ((dir (f-join root-dir dir)))
(when (f-dir? dir)
(f-files
dir
(lambda (file)
(or (file-executable-p file)
(get-script-run-command file)))))))
run-command-recipe-scripts--script-dirs)))
(-map
(lambda (file)
(list :command-name (f-relative file root-dir)
:command-line (if (file-executable-p file)
file
(format "%s %s" (get-script-run-command file) file))
:working-dir root-dir))
scripts)))
:general
(leader-map "\"" #'run-command)
:custom
(run-command-default-runner 'run-command-runner-compile)
(run-command-completion-method 'completing-read)
(run-command-recipes '(run-command-recipe-terraform
run-command-recipe-local
run-command-recipe-package-json
run-command-recipe-makefile
run-command-recipe-project
run-command-recipe-project-local
run-command-recipe-executables
run-command-recipe-obelix
run-command-recipe-kustomize
run-command-recipe-sops
run-command-recipe-pytest
run-command-recipe-nosetests
run-command-recipe-web-ext
run-command-recipe-pip
run-command-recipe-maven
run-command-recipe-cargo
run-command-recipe-rake
run-command-recipe-scripts)))
(provide 'init-run-command)