;;; alchemist-project.el --- API to identify Elixir mix projects. ;; Copyright © 2014-2015 Samuel Tonini ;; Author: Samuel Tonini . ;;; Commentary: ;; API to identify Elixir mix projects. ;;; Code: (require 'cl-lib) (require 'dash) (require 'alchemist-utils) (require 'alchemist-file) (defgroup alchemist-project nil "API to identify Elixir mix projects." :prefix "alchemist-help-" :group 'alchemist) (defconst alchemist-project-mix-project-indicator "mix.exs" "File which indicates the root directory of an Elixir Mix project.") (defconst alchemist-project-hex-pkg-indicator ".hex" "File which indicates the root directory of an Elixir Hex package.") (defun alchemist-project-elixir-p () "Return non-nil if `default-directory' is inside the Elixir source codebase." (stringp (alchemist-project-elixir-root))) (defun alchemist-project-elixir-root (&optional dir) "Return root directory of the Elixir source." (let* ((dir (file-name-as-directory (or dir (expand-file-name default-directory)))) (present-files (directory-files dir))) (cond ((alchemist-project-top-level-dir-p dir) nil) ((and (-contains-p present-files "eex") (-contains-p present-files "elixir") (-contains-p present-files "logger") (-contains-p present-files "mix") (-contains-p present-files "iex") (-contains-p present-files "ex_unit")) (file-name-directory (directory-file-name dir))) (t (alchemist-project-elixir-root (file-name-directory (directory-file-name dir))))))) (defun alchemist-project-p () "Return non-nil if `default-directory' is inside an Elixir Mix project." (stringp (alchemist-project-root))) (defun alchemist-project-top-level-dir-p (dir) "Return non-nil if DIR is the top level directory." (equal dir (file-name-directory (directory-file-name dir)))) (defun alchemist-project-root (&optional dir) "Return root directory of the current Elixir Mix project. It starts walking the directory tree to find the Elixir Mix root directory from `default-directory'. If DIR is non-nil it starts walking the directory from there instead." (let* ((dir (file-name-as-directory (or dir (expand-file-name default-directory)))) (present-files (directory-files dir))) (cond ((alchemist-project-top-level-dir-p dir) nil) ((-contains-p present-files alchemist-project-hex-pkg-indicator) (alchemist-project-root (file-name-directory (directory-file-name dir)))) ((-contains-p present-files alchemist-project-mix-project-indicator) dir) (t (alchemist-project-root (file-name-directory (directory-file-name dir))))))) (defun alchemist-project-root-or-default-dir () "Return the current Elixir mix project root or `default-directory'." (let* ((project-root (alchemist-project-root)) (dir (if project-root project-root default-directory))) dir)) (defun alchemist-project-toggle-file-and-tests-other-window () "Toggle between a file and its tests in other window." (interactive) (if (alchemist-utils-test-file-p) (alchemist-project-open-file-for-current-tests 'find-file-other-window) (alchemist-project-open-tests-for-current-file 'find-file-other-window))) (defun alchemist-project-toggle-file-and-tests () "Toggle between a file and its tests in the current window." (interactive) (if (alchemist-utils-test-file-p) (alchemist-project-open-file-for-current-tests 'find-file) (alchemist-project-open-tests-for-current-file 'find-file))) (defun alchemist-project-file-under-test (file directory) "Return the file which are tested by FILE. DIRECTORY is the place where the file under test is located." (let* ((filename (file-relative-name file (alchemist-project-root))) (filename (replace-regexp-in-string "^test" directory filename)) (filename (replace-regexp-in-string "_test\.exs$" "\.ex" filename))) (concat (alchemist-project-root) filename))) (defun alchemist-project-open-file-for-current-tests (opener) "Visit the implementation file for the current buffer with OPENER." (let* ((filename (alchemist-project-file-under-test (buffer-file-name) "web")) (filename (if (file-exists-p filename) filename (alchemist-project-file-under-test (buffer-file-name) "lib")))) (funcall opener filename))) (defun alchemist-project-open-tests-for-current-file (opener) "Visit the test file for the current buffer with OPENER." (let* ((filename (file-relative-name (buffer-file-name) (alchemist-project-root))) (filename (replace-regexp-in-string "^lib/" "test/" filename)) (filename (replace-regexp-in-string "^web/" "test/" filename)) (filename (replace-regexp-in-string "\.ex$" "_test\.exs" filename)) (filename (format "%s/%s" (alchemist-project-root) filename))) (if (file-exists-p filename) (funcall opener filename) (if (y-or-n-p "No test file found; create one now?") (alchemist-project--create-test-for-current-file filename (current-buffer)) (message "No test file found."))))) (defun alchemist-project--create-test-for-current-file (filename buffer) "Creates and populates a test module, FILENAME, for the code in BUFFER. The module name given to the test module is determined from the name of the first module defined in BUFFER." (let* ((directory-name (file-name-directory filename)) (module-name (alchemist-project--grok-module-name buffer)) (test-module-name (concat module-name "Test"))) (unless (file-exists-p directory-name) (make-directory (file-name-directory filename) t)) (alchemist-project--insert-test-boilerplate (find-file-other-window filename) test-module-name))) (defun alchemist-project--grok-module-name (buffer) "Determines the name of the first module defined in BUFFER." (save-excursion (with-current-buffer buffer (goto-char (point-min)) (re-search-forward "defmodule\\s-\\(.+?\\)\\s-?,?\\s-do") (match-string 1)))) (defun alchemist-project--insert-test-boilerplate (buffer module) "Inserts ExUnit boilerplate for MODULE in BUFFER. Point is left in a convenient location." (with-current-buffer buffer (insert (concat "defmodule " module " do\n" " use ExUnit.Case\n\n\n" "end\n")) (goto-char (point-min)) (beginning-of-line 4) (indent-according-to-mode))) (defun alchemist-project-run-tests-for-current-file () "Run the tests related to the current file." (interactive) (alchemist-project-open-tests-for-current-file 'alchemist-mix-test-file)) (defun alchemist-project-create-file () "Create a file under lib/ in the current project. The newly created buffer is filled with a module definition based on the file name." (interactive) (let ((root (alchemist-project-root))) (if (not root) (message "You're not in a Mix project") (let* ((lib-path (concat root "lib/")) (abs-path (read-file-name "New file in lib/: " lib-path)) (abs-path (alchemist-utils-add-ext-to-path-if-not-present abs-path ".ex")) (relative-path (file-relative-name abs-path lib-path))) (if (file-readable-p abs-path) (message "%s already exists" relative-path) (make-directory (file-name-directory abs-path) t) (find-file abs-path) (insert (concat "defmodule " (alchemist-utils-path-to-module-name relative-path) " do\n" " \n" "end\n")) (goto-char (point-min)) (beginning-of-line 2) (back-to-indentation)))))) (defun alchemist-project-name () "Return the name of the current Elixir Mix project." (if (alchemist-project-p) (car (cdr (reverse (split-string (alchemist-project-root) "/")))) "")) (defun alchemist-project-find-dir (directory) (unless (alchemist-project-p) (error "Could not find an Elixir Mix project root.")) (alchemist-file-find-files (alchemist-project-root) directory)) (defun alchemist-project-find-lib () (interactive) (alchemist-project-find-dir "lib")) (defun alchemist-project-find-test () (interactive) (alchemist-project-find-dir "test")) (provide 'alchemist-project) ;;; alchemist-project.el ends here