;; -*- 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 -AT") (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 (locate-dominating-file default-directory "spec") (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)