;;; 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) (require 'json) (defgroup alchemist-project nil "API to identify Elixir mix projects." :prefix "alchemist-help-" :group 'alchemist) (defcustom alchemist-project-config-filename ".alchemist" "Name of the file which holds the Elixir project setup." :type 'string :group 'alchemist) (defcustom alchemist-project-compile-when-needed nil "When `t', it compiles the Elixir project codebase when needed. For example: If documentation lookup or completion for code is made, it first tries to compile the current Elixir project codebase. This makes sure that the documentation and completion is always up to date with the codebase. Please be aware that when the compilation fails, no documentation or completion will be work. " :type 'string :group 'alchemist) (defun alchemist-project-toggle-compile-when-needed () "" (interactive) (if alchemist-project-compile-when-needed (setq alchemist-project-compile-when-needed nil) (setq alchemist-project-compile-when-needed t)) (if alchemist-project-compile-when-needed (message "Compilation of project when needed is enabled") (message "Compilation of project when needed is disabled"))) (defun alchemist-project--load-compile-when-needed-setting () (let ((config (gethash "compile-when-needed" (alchemist-project-config)))) (if config (intern config) alchemist-project-compile-when-needed))) (defun alchemist-project--config-filepath () "Return the path to the config file." (format "%s/%s" (alchemist-project-root) alchemist-project-config-filename)) (defun alchemist-project--config-exists-p () "Check if project config file exists." (file-exists-p (alchemist-project--config-filepath))) (defun alchemist-project-config () "Return the current Elixir project configs." (let* ((json-object-type 'hash-table) (config (if (alchemist-project--config-exists-p) (json-read-from-string (with-temp-buffer (insert-file-contents (alchemist-project--config-filepath)) (buffer-string))) (make-hash-table :test 'equal)))) config)) (defvar alchemist-project-root-indicators '("mix.exs") "list of file-/directory-names which indicate a root of a elixir project") (defvar alchemist-project-deps-indicators '(".hex") "list of file-/directory-names which indicate a root of a elixir project") (defun alchemist-project-p () "Returns whether alchemist has access to a elixir project root or not" (stringp (alchemist-project-root))) (defun alchemist-project-parent-directory (a-directory) "Returns the directory of which a-directory is a child" (file-name-directory (directory-file-name a-directory))) (defun alchemist-project-root-directory-p (a-directory) "Returns t if a-directory is the root" (equal a-directory (alchemist-project-parent-directory a-directory))) (defun alchemist-project-root (&optional directory) "Finds the root directory of the project by walking the directory tree until it finds a project root indicator." (let* ((directory (file-name-as-directory (or directory (expand-file-name default-directory)))) (present-files (directory-files directory))) (cond ((alchemist-project-root-directory-p directory) nil) ((> (length (intersection present-files alchemist-project-deps-indicators :test 'string=)) 0) (alchemist-project-root (file-name-directory (directory-file-name directory)))) ((> (length (intersection present-files alchemist-project-root-indicators :test 'string=)) 0) directory) (t (alchemist-project-root (file-name-directory (directory-file-name directory))))))) (defun alchemist-project--establish-root-directory () "Set the default-directory to the Elixir project root." (let ((project-root (alchemist-project-root))) (when project-root (setq default-directory project-root)))) (defun alchemist-project-open-tests-for-current-file () "Opens the appropriate test file for the current buffer file in a new window." (interactive) (let* ((filename (file-relative-name (buffer-file-name) (alchemist-project-root))) (filename (replace-regexp-in-string "^lib/" "test/" filename)) (filename (replace-regexp-in-string "\.ex$" "_test\.exs" filename)) (filename (format "%s/%s" (alchemist-project-root) filename))) (if (file-exists-p filename) (find-file-other-window 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 (set-buffer buffer) (goto-line 1) (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." (set-buffer buffer) (insert (concat "defmodule " module " do\n" " use ExUnit.Case\n" "\n" "end\n")) (goto-char (point-min)) (beginning-of-line 3)) (defun alchemist-project-find-test () "Open project test directory and list all test files." (interactive) (when (alchemist-project-p) (find-file (alchemist-project--open-directory-files "test")))) (defun alchemist-project--open-directory-files (directory) (let ((directory (concat (replace-regexp-in-string "\/?$" "" (concat (alchemist-project-root) directory) "/")))) (message directory) (concat directory "/" (completing-read (concat directory ": ") (mapcar (lambda (path) (replace-regexp-in-string (concat "^" (regexp-quote directory) "/") "" path)) (split-string (shell-command-to-string (concat "find \"" directory "\" -type f | grep \"_test\.exs\" | grep -v \"/.git/\" | grep -v \"/.yardoc/\"")))))))) (defun alchemist-project-name () "Return the name of the current Elixir project." (if (alchemist-project-p) (car (cdr (reverse (split-string (alchemist-project-root) "/")))) "")) (provide 'alchemist-project) ;;; alchemist-project.el ends here