Skip to content

Add elixir-format function to format Elixir 1.6 files. #406

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
May 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ Provides font-locking, indentation and navigation support for the
- [Keymapping](#keymapping)
- [Notes](#notes)
- [Elixir Tooling Integration](#elixir-tooling-integration)
- [Elixir Format](#elixir-format)
- [History](#history)
- [Contributing](#contributing)
- [License](#license)
Expand Down Expand Up @@ -147,6 +148,74 @@ If you looking for elixir tooling integration for Emacs, check: [alchemist.el](h

You can use [web-mode.el](http://web-mode.org) to edit elixir templates (eex files).


## Elixir Format

### Setup of elixir-format
Customize the elixir and mix paths

In Emacs, run following command to customize option
``` elisp
M-x customize-option

Customize-variable: elixir-format-elixir-path
```
and set your elixir executable path there. After that run:
``` elisp
M-x customize-option

Customize-variable: elixir-format-mix-path
```
and set your mix executable path there.

Your machine's elixir and mix executable paths can be found with `which` command as shown below

``` shell
$ which elixir
/usr/local/bin/elixir

$ which mix
/usr/local/bin/mix
```
Alternavively you can define variables as below

``` elisp
(setq elixir-format-elixir-path "/usr/local/bin/elixir")
(setq elixir-format-mix-path "/usr/local/bin/mix")
```

### Use it

``` elisp
M-x elixir-format
```

### Add elixir-mode hook to run elixir format on file save

``` elisp
;; Create a buffer-local hook to run elixir-format on save, only when we enable elixir-mode.
(add-hook 'elixir-mode-hook
(lambda () (add-hook 'before-save-hook 'elixir-format nil t)))
```

To use a `.formatter.exs` you can either set `elixir-format-arguments` globally to a path like this:

``` elisp
(setq elixir-format-arguments (list "--dot-formatter" "/path/to/.formatter.exs"))
```

or you set `elixir-format-arguments` in a hook like this:

``` elisp
(add-hook elixir-format-hook '(lambda ()
(if (projectile-project-p)
(setq elixir-format-arguments (list "--dot-formatter" (concat (projectile-project-root) "/.formatter.exs")))
(setq elixir-format-arguments nil))))
```

In this example we use [Projectile](https://github.com/bbatsov/projectile) to get the project root and set `elixir-format-arguments` accordingly.


## History

This mode is based on the
Expand Down
179 changes: 179 additions & 0 deletions elixir-format.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
;;; elixir-format.el --- Emacs plugin to mix format Elixir files

;; Copyright 2017-2018 Anil Wadghule, Christian Kruse

;; This file is NOT part of GNU Emacs.

;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 2, or (at your option)
;; any later version.

;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.

;;; Commentary:

;; The elixir-format function formats the elixir files with Elixir's `mix format`
;; command

;; e.g.
;; M-x elixir-format
;;

(require 'ansi-color)

(defcustom elixir-format-elixir-path "elixir"
"Path to the Elixir interpreter."
:type 'string
:group 'elixir-format)

(defcustom elixir-format-mix-path "/usr/bin/mix"
"Path to the 'mix' executable."
:type 'string
:group 'elixir-format)

(defcustom elixir-format-arguments nil
"Additional arguments to 'mix format'"
:type '(repeat string)
:group 'elixir-format)

(defcustom elixir-format-hook nil
"Hook called by `elixir-format'."
:type 'hook
:group 'elixir-format)


;;; Code

(defun elixir-format--goto-line (line)
(goto-char (point-min))
(forward-line (1- line)))

(defun elixir-format--delete-whole-line (&optional arg)
"Delete the current line without putting it in the `kill-ring'.
Derived from function `kill-whole-line'. ARG is defined as for that
function.

Shamelessly stolen from go-mode (https://github.com/dominikh/go-mode.el)"
(setq arg (or arg 1))
(if (and (> arg 0)
(eobp)
(save-excursion (forward-visible-line 0) (eobp)))
(signal 'end-of-buffer nil))
(if (and (< arg 0)
(bobp)
(save-excursion (end-of-visible-line) (bobp)))
(signal 'beginning-of-buffer nil))
(cond ((zerop arg)
(delete-region (progn (forward-visible-line 0) (point))
(progn (end-of-visible-line) (point))))
((< arg 0)
(delete-region (progn (end-of-visible-line) (point))
(progn (forward-visible-line (1+ arg))
(unless (bobp)
(backward-char))
(point))))
(t
(delete-region (progn (forward-visible-line 0) (point))
(progn (forward-visible-line arg) (point))))))

(defun elixir-format--apply-rcs-patch (patch-buffer)
"Apply an RCS-formatted diff from PATCH-BUFFER to the current buffer.
Shamelessly stolen from go-mode (https://github.com/dominikh/go-mode.el)"

(let ((target-buffer (current-buffer))
;; Relative offset between buffer line numbers and line numbers
;; in patch.
;;
;; Line numbers in the patch are based on the source file, so
;; we have to keep an offset when making changes to the
;; buffer.
;;
;; Appending lines decrements the offset (possibly making it
;; negative), deleting lines increments it. This order
;; simplifies the forward-line invocations.
(line-offset 0))
(save-excursion
(with-current-buffer patch-buffer
(goto-char (point-min))
(while (not (eobp))
(unless (looking-at "^\\([ad]\\)\\([0-9]+\\) \\([0-9]+\\)")
(error "Invalid rcs patch or internal error in elixir-format--apply-rcs-patch"))
(forward-line)
(let ((action (match-string 1))
(from (string-to-number (match-string 2)))
(len (string-to-number (match-string 3))))
(cond
((equal action "a")
(let ((start (point)))
(forward-line len)
(let ((text (buffer-substring start (point))))
(with-current-buffer target-buffer
(cl-decf line-offset len)
(goto-char (point-min))
(forward-line (- from len line-offset))
(insert text)))))
((equal action "d")
(with-current-buffer target-buffer
(elixir-format--goto-line (- from line-offset))
(cl-incf line-offset len)
(elixir-format--delete-whole-line len)))
(t
(error "Invalid rcs patch or internal error in elixir-format--apply-rcs-patch"))))))))
)

;;;###autoload
(defun elixir-format (&optional is-interactive)
(interactive "p")

(let ((outbuff (get-buffer-create "*elixir-format-output*"))
(errbuff (get-buffer-create "*elixir-format-errors*"))
(tmpfile (make-temp-file "elixir-format" nil ".ex"))
(our-elixir-format-arguments (list elixir-format-mix-path "format"))
(output nil))

(unwind-protect
(save-restriction
(with-current-buffer outbuff
(erase-buffer))

(with-current-buffer errbuff
(setq buffer-read-only nil)
(erase-buffer))

(write-region nil nil tmpfile)

(run-hooks 'elixir-format-hook)

(when elixir-format-arguments
(setq our-elixir-format-arguments (append our-elixir-format-arguments elixir-format-arguments)))
(setq our-elixir-format-arguments (append our-elixir-format-arguments (list tmpfile)))

(if (zerop (apply #'call-process elixir-format-elixir-path nil errbuff nil our-elixir-format-arguments))
(progn
(if (zerop (call-process-region (point-min) (point-max) "diff" nil outbuff nil "-n" "-" tmpfile))
(message "File is already formatted")
(progn
(elixir-format--apply-rcs-patch outbuff)
(message "mix format applied")))
(kill-buffer errbuff))

(progn
(with-current-buffer errbuff
(setq buffer-read-only t)
(ansi-color-apply-on-region (point-min) (point-max))
(special-mode))

(if is-interactive
(display-buffer errbuff)
(error "elixir-format failed: see %s" (buffer-name errbuff)))))

(delete-file tmpfile)
(kill-buffer outbuff)))))

(provide 'elixir-format)

;;; elixir-format.el ends here
1 change: 1 addition & 0 deletions elixir-mode.el
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
(require 'easymenu) ; Elixir Mode menu definition
(require 'elixir-smie) ; Syntax and indentation support
(require 'pkg-info) ; Display Elixir Mode package version
(require 'elixir-format) ; Elixir Format functions

(defgroup elixir nil
"Major mode for editing Elixir code."
Expand Down
45 changes: 45 additions & 0 deletions test/elixir-format-test.el
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
;;; elixir-format-test.el --- Basic tests for elixir-format

;;; Code:

(ert-deftest indents-a-buffer ()
(when elixir-formatter-supported
(ert-with-test-buffer (:name "(Expected)indents-a-buffer")
(insert elixir-format-test-example)
(elixir-format)
(should (equal (buffer-string) elixir-format-formatted-test-example)))))

(ert-deftest indents-a-buffer-and-undoes-changes ()
(when elixir-formatter-supported
(ert-with-test-buffer ()
(buffer-enable-undo)
(setq buffer-undo-list nil)

(insert elixir-format-test-example)

(undo-boundary)
(elixir-format)

(should (equal (buffer-string) elixir-format-formatted-test-example))
(undo 0)
(should (equal (buffer-string) elixir-format-test-example)))))

(ert-deftest elixir-format-should-run-hook-before-formatting ()
(when elixir-formatter-supported
(ert-with-test-buffer ()
(let ((has-been-run nil))
(insert elixir-format-test-example)
(add-hook 'elixir-format-hook (lambda () (setq has-been-run t)))
(elixir-format)
(should (equal has-been-run t))))))

(ert-deftest elixir-format-should-message-on-error ()
(when elixir-formatter-supported
(ert-with-test-buffer ()
(insert elixir-format-wrong-test-example)
(should-error
(elixir-format)))))

(provide 'elixir-format-test)

;;; elixir-format-test.el ends here.
37 changes: 37 additions & 0 deletions test/test-helper.el
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@
;; Load the elixir-mode under test
(require 'elixir-mode)

;; Load elixir-format under test
(require 'elixir-format)

;; Helpers

(cl-defmacro elixir-deftest (name args &body body)
Expand Down Expand Up @@ -63,4 +66,38 @@
(defun ert-runner/run-tests-batch-and-exit (selector)
(ert-run-tests-interactively selector)))

(setq elixir-format-elixir-path (executable-find "elixir"))
(setq elixir-format-mix-path (executable-find "mix"))

(defconst elixir-format-test-example "defmodule Foo do
use GenServer.Behaviour
def foobar do
if true, do: IO.puts \"yay\"
end
end")

(defconst elixir-format-wrong-test-example "defmodule Foo do
use GenServer.Behaviour
def foobar do
if true, do: IO.puts \"yay\"
end")

(setq elixir-version (let ((str (shell-command-to-string (concat elixir-format-elixir-path " --version"))))
(car (when (string-match "\s\\(.+[.].+[.].+\\)[\s\n]" str)
(list (match-string 1 str))))))

(defconst elixir-formatter-supported
(>= (string-to-number elixir-version) (string-to-number "1.6"))
)

(defconst elixir-format-formatted-test-example
"defmodule Foo do
use GenServer.Behaviour

def foobar do
if true, do: IO.puts(\"yay\")
end
end
")

;;; test-helper.el ends here