Skip to content

Commit 0ff5cd8

Browse files
anildigitalckrusedragonwasrobot
committed
Add elixir-format function to format Elixir 1.6 files.
Co-Authored-By: Christian Kruse <[email protected]> Co-Authored-By: Peter Urbak <[email protected]>
1 parent e928aaf commit 0ff5cd8

File tree

4 files changed

+348
-0
lines changed

4 files changed

+348
-0
lines changed

README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,70 @@ You can use [web-mode.el](http://web-mode.org) to edit elixir templates (eex fil
152152
This mode is based on the
153153
[Emacs mode by secondplanet](https://github.com/secondplanet/elixir-mode).
154154

155+
156+
## Elixir Format
157+
158+
### Setup of elixir-format
159+
Customize the elixir and mix pathes
160+
161+
In Emacs, run following command to customize option
162+
``` elisp
163+
M-x customize-option
164+
165+
Customize-variable: elixir-format-elixir-path
166+
```
167+
and set your elixir executable path there. After that run:
168+
``` elisp
169+
M-x customize-option
170+
171+
Customize-variable: elixir-format-mix-path
172+
```
173+
and set your mix executable path there.
174+
175+
Your machine's elixir and mix executable paths can be found with `which` command as shown below
176+
177+
``` shell
178+
$ which mix
179+
/usr/local/bin/mix
180+
```
181+
182+
183+
### Usage of elixir-format
184+
``` elisp
185+
;; require from Emacs
186+
(require 'elixir-format)
187+
188+
;; Use it
189+
M-x elixir-format
190+
```
191+
192+
### Add elixir-mode hook to run elixir format on file save
193+
194+
``` elisp
195+
;; elixir-mode hook
196+
(add-hook 'elixir-mode-hook
197+
(lambda () (add-hook 'before-save-hook 'elixir-format-before-save)))
198+
199+
```
200+
201+
To use a `.formatter.exs` you can either set `elixir-format-arguments` globally to a path like this:
202+
203+
``` elisp
204+
(setq elixir-format-arguments (list "--dot-formatter" "/path/to/.formatter.exs"))
205+
```
206+
207+
or you set `elixir-format-arguments` in a hook like this:
208+
209+
```elisp
210+
(add-hook elixir-format-hook '(lambda ()
211+
(if (projectile-project-p)
212+
(setq elixir-format-arguments (list "--dot-formatter" (concat (projectile-project-root) "/.formatter.exs")))
213+
(setq elixir-format-arguments nil))))
214+
```
215+
216+
In this example we use [Projectile](https://github.com/bbatsov/projectile) to get the project root and set `elixir-format-arguments` accordingly.
217+
218+
155219
## Contributing
156220

157221
Please read [CONTRIBUTING.md](https://github.com/elixir-lang/emacs-elixir/blob/master/CONTRIBUTING.md) for guidelines on how to contribute to this project.

elixir-format.el

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
;;; elixir-format.el --- Emacs plugin to mix format Elixir files
2+
3+
;; Copyright 2017-2018 Anil Wadghule, Christian Kruse
4+
5+
;; This file is NOT part of GNU Emacs.
6+
7+
;; This program is free software; you can redistribute it and/or modify
8+
;; it under the terms of the GNU General Public License as published by
9+
;; the Free Software Foundation; either version 2, or (at your option)
10+
;; any later version.
11+
12+
;; This program is distributed in the hope that it will be useful,
13+
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
;; GNU General Public License for more details.
16+
17+
;;; Commentary:
18+
19+
;; The elixir-format function formats the elixir files with Elixir's `mix format`
20+
;; command
21+
22+
;; e.g.
23+
;;
24+
;; (require 'elixir-format)
25+
;; M-x elixir-format
26+
;;
27+
28+
(defcustom elixir-format-elixir-path "elixir"
29+
"Path to the Elixir interpreter."
30+
:type 'string
31+
:group 'elixir-format)
32+
33+
(defcustom elixir-format-mix-path "/usr/bin/mix"
34+
"Path to the 'mix' executable."
35+
:type 'string
36+
:group 'elixir-format)
37+
38+
(defcustom elixir-format-arguments nil
39+
"Additional arguments to 'mix format'"
40+
:type '(repeat string)
41+
:group 'elixir-format)
42+
43+
(defcustom elixir-format-hook nil
44+
"Hook called by `elixir-format'."
45+
:type 'hook
46+
:group 'elixir-format)
47+
48+
49+
;;; Code
50+
51+
;;;###autoload
52+
(defun elixir-format-before-save ()
53+
"Add this to .emacs to run mix format on the current buffer when saving:
54+
\(add-hook 'before-save-hook 'elixir-format-before-save).
55+
56+
Note that this will cause ‘elixir-mode’ to get loaded the first time
57+
you save any file, kind of defeating the point of autoloading."
58+
59+
(interactive)
60+
(when (eq major-mode 'elixir-mode) (elixir-format)))
61+
62+
63+
(defun elixir-format--goto-line (line)
64+
(goto-char (point-min))
65+
(forward-line (1- line)))
66+
67+
(defun elixir-format--delete-whole-line (&optional arg)
68+
"Delete the current line without putting it in the `kill-ring'.
69+
Derived from function `kill-whole-line'. ARG is defined as for that
70+
function.
71+
72+
Shamelessly stolen from go-mode (https://github.com/dominikh/go-mode.el)"
73+
(setq arg (or arg 1))
74+
(if (and (> arg 0)
75+
(eobp)
76+
(save-excursion (forward-visible-line 0) (eobp)))
77+
(signal 'end-of-buffer nil))
78+
(if (and (< arg 0)
79+
(bobp)
80+
(save-excursion (end-of-visible-line) (bobp)))
81+
(signal 'beginning-of-buffer nil))
82+
(cond ((zerop arg)
83+
(delete-region (progn (forward-visible-line 0) (point))
84+
(progn (end-of-visible-line) (point))))
85+
((< arg 0)
86+
(delete-region (progn (end-of-visible-line) (point))
87+
(progn (forward-visible-line (1+ arg))
88+
(unless (bobp)
89+
(backward-char))
90+
(point))))
91+
(t
92+
(delete-region (progn (forward-visible-line 0) (point))
93+
(progn (forward-visible-line arg) (point))))))
94+
95+
(defun elixir-format--apply-rcs-patch (patch-buffer)
96+
"Apply an RCS-formatted diff from PATCH-BUFFER to the current buffer.
97+
Shamelessly stolen from go-mode (https://github.com/dominikh/go-mode.el)"
98+
99+
(let ((target-buffer (current-buffer))
100+
;; Relative offset between buffer line numbers and line numbers
101+
;; in patch.
102+
;;
103+
;; Line numbers in the patch are based on the source file, so
104+
;; we have to keep an offset when making changes to the
105+
;; buffer.
106+
;;
107+
;; Appending lines decrements the offset (possibly making it
108+
;; negative), deleting lines increments it. This order
109+
;; simplifies the forward-line invocations.
110+
(line-offset 0))
111+
(save-excursion
112+
(with-current-buffer patch-buffer
113+
(goto-char (point-min))
114+
(while (not (eobp))
115+
(unless (looking-at "^\\([ad]\\)\\([0-9]+\\) \\([0-9]+\\)")
116+
(error "Invalid rcs patch or internal error in elixir-format--apply-rcs-patch"))
117+
(forward-line)
118+
(let ((action (match-string 1))
119+
(from (string-to-number (match-string 2)))
120+
(len (string-to-number (match-string 3))))
121+
(cond
122+
((equal action "a")
123+
(let ((start (point)))
124+
(forward-line len)
125+
(let ((text (buffer-substring start (point))))
126+
(with-current-buffer target-buffer
127+
(cl-decf line-offset len)
128+
(goto-char (point-min))
129+
(forward-line (- from len line-offset))
130+
(insert text)))))
131+
((equal action "d")
132+
(with-current-buffer target-buffer
133+
(elixir-format--goto-line (- from line-offset))
134+
(cl-incf line-offset len)
135+
(elixir-format--delete-whole-line len)))
136+
(t
137+
(error "Invalid rcs patch or internal error in elixir-format--apply-rcs-patch"))))))))
138+
)
139+
140+
;;;###autoload
141+
(defun elixir-format (&optional is-interactive)
142+
(interactive "p")
143+
144+
(let ((outbuff (get-buffer-create "*elixir-format-output*"))
145+
(errbuff (get-buffer-create "*elixir-format-errors*"))
146+
(tmpfile (make-temp-file "elixir-format" nil ".ex"))
147+
(our-elixir-format-arguments (list elixir-format-mix-path "format"))
148+
(output nil))
149+
150+
(unwind-protect
151+
(save-restriction
152+
(with-current-buffer outbuff
153+
(erase-buffer))
154+
155+
(with-current-buffer errbuff
156+
(setq buffer-read-only nil)
157+
(erase-buffer))
158+
159+
(write-region nil nil tmpfile)
160+
161+
(run-hooks 'elixir-format-hook)
162+
163+
(when elixir-format-arguments
164+
(setq our-elixir-format-arguments (append our-elixir-format-arguments elixir-format-arguments)))
165+
(setq our-elixir-format-arguments (append our-elixir-format-arguments (list tmpfile)))
166+
167+
(if (zerop (apply #'call-process elixir-format-elixir-path nil errbuff nil our-elixir-format-arguments))
168+
(progn
169+
(if (zerop (call-process-region (point-min) (point-max) "diff" nil outbuff nil "-n" "-" tmpfile))
170+
(message "File is already formatted")
171+
(progn
172+
(elixir-format--apply-rcs-patch outbuff)
173+
(message "mix format applied")))
174+
(kill-buffer errbuff))
175+
176+
(progn
177+
(with-current-buffer errbuff
178+
(setq buffer-read-only t)
179+
(ansi-color-apply-on-region (point-min) (point-max))
180+
(special-mode))
181+
182+
(if is-interactive
183+
(display-buffer errbuff)
184+
(error "elixir-format failed: see %s" (buffer-name errbuff)))))
185+
186+
(delete-file tmpfile)
187+
(kill-buffer outbuff)))))
188+
189+
(provide 'elixir-format)
190+
191+
;;; elixir-format.el ends here

test/elixir-format-test.el

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
;;; elixir-format-test.el --- Basic tests for elixir-format
2+
3+
4+
;;; Code:
5+
6+
(ert-deftest indents-a-buffer ()
7+
(ert-with-test-buffer (:name "(Expected)indents-a-buffer")
8+
(insert elixir-format-test-example)
9+
(elixir-format)
10+
(should (equal (buffer-string) elixir-format-formatted-test-example))))
11+
12+
(ert-deftest indents-a-buffer-and-undoes-changes ()
13+
(ert-with-test-buffer ()
14+
(buffer-enable-undo)
15+
(setq buffer-undo-list nil)
16+
17+
(insert elixir-format-test-example)
18+
19+
(undo-boundary)
20+
(elixir-format)
21+
22+
(should (equal (buffer-string) elixir-format-formatted-test-example))
23+
(undo 0)
24+
(should (equal (buffer-string) elixir-format-test-example))))
25+
26+
(ert-deftest elixir-format-before-save-formats-buffer ()
27+
(ert-with-test-buffer ()
28+
(insert elixir-format-test-example)
29+
(setq major-mode 'elixir-mode)
30+
(elixir-format-before-save)
31+
(should (equal (buffer-string) elixir-format-formatted-test-example))))
32+
33+
(ert-deftest elixir-format-before-save-should-not-format-buffer ()
34+
(ert-with-test-buffer ()
35+
(insert elixir-format-test-example)
36+
(elixir-format-before-save)
37+
(should (equal (buffer-string) elixir-format-test-example))))
38+
39+
(ert-deftest elixir-format-should-run-hook-before-formatting ()
40+
(ert-with-test-buffer ()
41+
(let ((has-been-run nil))
42+
(insert elixir-format-test-example)
43+
(add-hook 'elixir-format-hook (lambda () (setq has-been-run t)))
44+
(elixir-format)
45+
(should (equal has-been-run t)))))
46+
47+
(ert-deftest elixir-format-should-message-on-error ()
48+
(ert-with-test-buffer ()
49+
(insert elixir-format-wrong-test-example)
50+
(should-error
51+
(elixir-format))))
52+
53+
(provide 'elixir-format-test)
54+
55+
;;; elixir-format-test.el ends here

test/test-helper.el

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
;; Load the elixir-mode under test
1919
(require 'elixir-mode)
2020

21+
;; Load elixir-format under test
22+
(require 'elixir-format)
23+
2124
;; Helpers
2225

2326
(cl-defmacro elixir-deftest (name args &body body)
@@ -63,4 +66,39 @@
6366
(defun ert-runner/run-tests-batch-and-exit (selector)
6467
(ert-run-tests-interactively selector)))
6568

69+
70+
(cond
71+
((file-exists-p "/usr/bin/elixir")
72+
(setq elixir-format-elixir-path "/usr/bin/elixir")
73+
(setq elixir-format-mix-path "/usr/bin/mix"))
74+
((file-exists-p "/usr/local/bin/elixir")
75+
(setq elixir-format-elixir-path "/usr/local/bin/elixir")
76+
(setq elixir-format-mix-path "/usr/local/bin/mix"))
77+
(t
78+
(error "We need elixir and mix!")))
79+
80+
81+
(defconst elixir-format-test-example "defmodule Foo do
82+
use GenServer.Behaviour
83+
def foobar do
84+
if true, do: IO.puts \"yay\"
85+
end
86+
end")
87+
88+
(defconst elixir-format-wrong-test-example "defmodule Foo do
89+
use GenServer.Behaviour
90+
def foobar do
91+
if true, do: IO.puts \"yay\"
92+
end")
93+
94+
(defconst elixir-format-formatted-test-example
95+
"defmodule Foo do
96+
use GenServer.Behaviour
97+
98+
def foobar do
99+
if true, do: IO.puts(\"yay\")
100+
end
101+
end
102+
")
103+
66104
;;; test-helper.el ends here

0 commit comments

Comments
 (0)