;; -*- 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)