515 lines
25 KiB
EmacsLisp
515 lines
25 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
|
|
;; Prevent run-command-runner-vterm from clobbering globals
|
|
(defun run-command-runner-vterm-advice (orig-fun command-line buffer-base-name output-buffer &rest args)
|
|
(with-current-buffer output-buffer
|
|
(make-local-variable 'change-major-mode-hook)
|
|
(make-local-variable 'buffer-read-only))
|
|
(apply orig-fun command-line buffer-base-name output-buffer args))
|
|
|
|
(advice-add 'run-command-runner-vterm :around #'run-command-runner-vterm-advice)
|
|
|
|
;; Add ability to edit command line with prefix arg
|
|
(defvar run-command-edit? nil)
|
|
|
|
(defun run-command-core-run-advice (orig-fun command-spec &rest args)
|
|
(let* ((buffer-base-name
|
|
(format "%s[%s]"
|
|
(map-elt command-spec :command-name)
|
|
(map-elt command-spec :scope-name)))
|
|
(buffer (get-buffer-create (concat "*" buffer-base-name "*")))
|
|
(spec (if run-command-edit?
|
|
(plist-put command-spec :command-line (read-string "Command: " (plist-get command-spec :command-line)))
|
|
command-spec)))
|
|
(when buffer (kill-buffer buffer))
|
|
(apply orig-fun spec args)))
|
|
|
|
(defun run-command-advice (orig-fun &rest args)
|
|
(let ((run-command-edit? (not (null current-prefix-arg))))
|
|
(apply orig-fun args)))
|
|
|
|
(advice-add 'run-command :around #'run-command-advice)
|
|
(advice-add 'run-command-core-run :around #'run-command-core-run-advice)
|
|
|
|
(defun run-command-with-runner (runner)
|
|
(interactive (list (completing-read "Runner: "
|
|
'(run-command-runner-compile
|
|
run-command-runner-vterm
|
|
run-command-runner-term
|
|
run-command-runner-eat)
|
|
nil t)))
|
|
(let ((run-command-default-runner (intern runner)))
|
|
(call-interactively #'run-command)))
|
|
|
|
(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")
|
|
(list :command-name "format"
|
|
:command-line "terraform fmt -recursive"))))
|
|
|
|
(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)
|
|
;; TODO: get this working using built-in treesit instead of tree-sitter
|
|
;; (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)))))
|
|
|
|
(defvar run-command-rake--cached-cmds nil)
|
|
(defun run-command-rake--cache-cmds (project cmds)
|
|
(setq run-command-rake--cached-cmds
|
|
(plist-put run-command-rake--cached-cmds project cmds #'equal))
|
|
(plist-get run-command-rake--cached-cmds project #'equal))
|
|
(defun run-command-rake--get-cmds (project)
|
|
(plist-get run-command-rake--cached-cmds project #'equal))
|
|
(defun run-command-rake--reset-cache ()
|
|
(interactive)
|
|
(if-let ((rake-dir (or (locate-dominating-file default-directory "Rakefile")
|
|
(locate-dominating-file default-directory "Rakefile.rb"))))
|
|
(setq run-command-rake--cached-cmds
|
|
(plist-put run-command-rake--cached-cmds rake-dir nil #'equal))
|
|
(setq run-command-rake--cached-cmds nil)))
|
|
|
|
(defun run-command-recipe-rake ()
|
|
(when-let* ((rake-dir (or (locate-dominating-file default-directory "Rakefile")
|
|
(locate-dominating-file default-directory "Rakefile.rb")))
|
|
(cmds (if-let ((cmds (run-command-rake--get-cmds rake-dir)))
|
|
cmds
|
|
(run-command-rake--cache-cmds
|
|
rake-dir
|
|
(->> (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)))
|
|
|
|
(defun run-command-recipe-rspec ()
|
|
(when
|
|
(and (or (derived-mode-p 'ruby-mode)
|
|
(derived-mode-p 'ruby-ts-mode))
|
|
(save-excursion
|
|
(goto-char (point-min))
|
|
(re-search-forward "RSpec" nil t)))
|
|
(let ((root-dir (or (projectile-project-root) default-directory)))
|
|
(-concat
|
|
(list
|
|
(list :command-name "test this file"
|
|
:command-line (format "rspec %s" (buffer-file-name))
|
|
:working-dir root-dir)
|
|
(when-let ((example-line (treesit-node-start-line-number (treesit-rspec-example-at-point))))
|
|
(list :command-name "test this example"
|
|
:command-line (format "rspec %s:%s" (buffer-file-name) example-line)
|
|
:working-dir root-dir))
|
|
(when-let ((example-group-line (treesit-node-start-line-number (treesit-rspec-example-group-at-point))))
|
|
(list :command-name "test this example group"
|
|
:command-line (format "rspec %s:%s" (buffer-file-name) example-group-line)
|
|
:working-dir root-dir))
|
|
(when-let ((context-line (treesit-node-start-line-number (treesit-rspec-context-at-point))))
|
|
(list :command-name "test this context"
|
|
:command-line (format "rspec %s:%s" (buffer-file-name) context-line)
|
|
:working-dir root-dir)))
|
|
(-map (lambda (node)
|
|
(list :command-name (format "test example '%s'" (treesit-rspec-method-name node))
|
|
:command-line (format "rspec %s:%s"
|
|
(buffer-file-name)
|
|
(treesit-node-start-line-number node))
|
|
:working-dir root-dir))
|
|
(treesit-rspec-all-examples))
|
|
(-map (lambda (node)
|
|
(list :command-name (format "test example group '%s'" (treesit-rspec-method-name node))
|
|
:command-line (format "rspec %s:%s"
|
|
(buffer-file-name)
|
|
(treesit-node-start-line-number node))
|
|
:working-dir root-dir))
|
|
(treesit-rspec-all-example-groups))
|
|
(-map (lambda (node)
|
|
(list :command-name (format "test context '%s'" (treesit-rspec-method-name node))
|
|
:command-line (format "rspec %s:%s"
|
|
(buffer-file-name)
|
|
(treesit-node-start-line-number node))
|
|
:working-dir root-dir))
|
|
(treesit-rspec-all-contexts))))))
|
|
|
|
(defun run-command-recipe-hummingbird ()
|
|
(let* ((project (projectile-project-root))
|
|
(project-name (f-filename project)))
|
|
(append (when (equal project-name "hummingbird-rails")
|
|
(->> (f-files (f-join project "workbench" "bin"))
|
|
(-filter (lambda (f) (not (s-starts-with? "_" (f-base f)))))
|
|
(-map (lambda (f)
|
|
(list :command-name (format "workbench %s" (f-base f))
|
|
:command-line f
|
|
:working-dir project
|
|
:runner 'run-command-runner-vterm))))))))
|
|
|
|
:general
|
|
(leader-map "'" #'run-command)
|
|
(leader-map "\"" #'run-command-with-runner)
|
|
: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
|
|
run-command-recipe-rspec
|
|
run-command-recipe-hummingbird)))
|
|
|
|
(provide 'init-run-command)
|