Skip to content

Commit 1c7a3db

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 e9deded commit 1c7a3db

File tree

5 files changed

+338
-0
lines changed

5 files changed

+338
-0
lines changed

README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,73 @@ 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 paths
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 elixir
179+
/usr/local/bin/elixir
180+
181+
$ which mix
182+
/usr/local/bin/mix
183+
```
184+
Alternavively you can define variables as below
185+
186+
``` elisp
187+
(setq elixir-format-elixir-path "/usr/local/bin/elixir")
188+
(setq elixir-format-mix-path "/usr/local/bin/mix")
189+
```
190+
191+
;; Use it
192+
M-x elixir-format
193+
```
194+
195+
### Add elixir-mode hook to run elixir format on file save
196+
197+
``` elisp
198+
;; Create a buffer-local hook to run elixir-format on save, only when we enable elixir-mode.
199+
(add-hook 'elixir-mode-hook
200+
(lambda () (add-hook 'before-save-hook 'elixir-format nil t)))
201+
202+
```
203+
204+
To use a `.formatter.exs` you can either set `elixir-format-arguments` globally to a path like this:
205+
206+
``` elisp
207+
(setq elixir-format-arguments (list "--dot-formatter" "/path/to/.formatter.exs"))
208+
```
209+
210+
or you set `elixir-format-arguments` in a hook like this:
211+
212+
``` elisp
213+
(add-hook elixir-format-hook '(lambda ()
214+
(if (projectile-project-p)
215+
(setq elixir-format-arguments (list "--dot-formatter" (concat (projectile-project-root) "/.formatter.exs")))
216+
(setq elixir-format-arguments nil))))
217+
```
218+
219+
In this example we use [Projectile](https://github.com/bbatsov/projectile) to get the project root and set `elixir-format-arguments` accordingly.
220+
221+
155222
## Contributing
156223

157224
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: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
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+
;; M-x elixir-format
24+
;;
25+
26+
(defcustom elixir-format-elixir-path "elixir"
27+
"Path to the Elixir interpreter."
28+
:type 'string
29+
:group 'elixir-format)
30+
31+
(defcustom elixir-format-mix-path "/usr/bin/mix"
32+
"Path to the 'mix' executable."
33+
:type 'string
34+
:group 'elixir-format)
35+
36+
(defcustom elixir-format-arguments nil
37+
"Additional arguments to 'mix format'"
38+
:type '(repeat string)
39+
:group 'elixir-format)
40+
41+
(defcustom elixir-format-hook nil
42+
"Hook called by `elixir-format'."
43+
:type 'hook
44+
:group 'elixir-format)
45+
46+
47+
;;; Code
48+
49+
(defun elixir-format--goto-line (line)
50+
(goto-char (point-min))
51+
(forward-line (1- line)))
52+
53+
(defun elixir-format--delete-whole-line (&optional arg)
54+
"Delete the current line without putting it in the `kill-ring'.
55+
Derived from function `kill-whole-line'. ARG is defined as for that
56+
function.
57+
58+
Shamelessly stolen from go-mode (https://github.com/dominikh/go-mode.el)"
59+
(setq arg (or arg 1))
60+
(if (and (> arg 0)
61+
(eobp)
62+
(save-excursion (forward-visible-line 0) (eobp)))
63+
(signal 'end-of-buffer nil))
64+
(if (and (< arg 0)
65+
(bobp)
66+
(save-excursion (end-of-visible-line) (bobp)))
67+
(signal 'beginning-of-buffer nil))
68+
(cond ((zerop arg)
69+
(delete-region (progn (forward-visible-line 0) (point))
70+
(progn (end-of-visible-line) (point))))
71+
((< arg 0)
72+
(delete-region (progn (end-of-visible-line) (point))
73+
(progn (forward-visible-line (1+ arg))
74+
(unless (bobp)
75+
(backward-char))
76+
(point))))
77+
(t
78+
(delete-region (progn (forward-visible-line 0) (point))
79+
(progn (forward-visible-line arg) (point))))))
80+
81+
(defun elixir-format--apply-rcs-patch (patch-buffer)
82+
"Apply an RCS-formatted diff from PATCH-BUFFER to the current buffer.
83+
Shamelessly stolen from go-mode (https://github.com/dominikh/go-mode.el)"
84+
85+
(let ((target-buffer (current-buffer))
86+
;; Relative offset between buffer line numbers and line numbers
87+
;; in patch.
88+
;;
89+
;; Line numbers in the patch are based on the source file, so
90+
;; we have to keep an offset when making changes to the
91+
;; buffer.
92+
;;
93+
;; Appending lines decrements the offset (possibly making it
94+
;; negative), deleting lines increments it. This order
95+
;; simplifies the forward-line invocations.
96+
(line-offset 0))
97+
(save-excursion
98+
(with-current-buffer patch-buffer
99+
(goto-char (point-min))
100+
(while (not (eobp))
101+
(unless (looking-at "^\\([ad]\\)\\([0-9]+\\) \\([0-9]+\\)")
102+
(error "Invalid rcs patch or internal error in elixir-format--apply-rcs-patch"))
103+
(forward-line)
104+
(let ((action (match-string 1))
105+
(from (string-to-number (match-string 2)))
106+
(len (string-to-number (match-string 3))))
107+
(cond
108+
((equal action "a")
109+
(let ((start (point)))
110+
(forward-line len)
111+
(let ((text (buffer-substring start (point))))
112+
(with-current-buffer target-buffer
113+
(cl-decf line-offset len)
114+
(goto-char (point-min))
115+
(forward-line (- from len line-offset))
116+
(insert text)))))
117+
((equal action "d")
118+
(with-current-buffer target-buffer
119+
(elixir-format--goto-line (- from line-offset))
120+
(cl-incf line-offset len)
121+
(elixir-format--delete-whole-line len)))
122+
(t
123+
(error "Invalid rcs patch or internal error in elixir-format--apply-rcs-patch"))))))))
124+
)
125+
126+
;;;###autoload
127+
(defun elixir-format (&optional is-interactive)
128+
(interactive "p")
129+
130+
(let ((outbuff (get-buffer-create "*elixir-format-output*"))
131+
(errbuff (get-buffer-create "*elixir-format-errors*"))
132+
(tmpfile (make-temp-file "elixir-format" nil ".ex"))
133+
(our-elixir-format-arguments (list elixir-format-mix-path "format"))
134+
(output nil))
135+
136+
(unwind-protect
137+
(save-restriction
138+
(with-current-buffer outbuff
139+
(erase-buffer))
140+
141+
(with-current-buffer errbuff
142+
(setq buffer-read-only nil)
143+
(erase-buffer))
144+
145+
(write-region nil nil tmpfile)
146+
147+
(run-hooks 'elixir-format-hook)
148+
149+
(when elixir-format-arguments
150+
(setq our-elixir-format-arguments (append our-elixir-format-arguments elixir-format-arguments)))
151+
(setq our-elixir-format-arguments (append our-elixir-format-arguments (list tmpfile)))
152+
153+
(if (zerop (apply #'call-process elixir-format-elixir-path nil errbuff nil our-elixir-format-arguments))
154+
(progn
155+
(if (zerop (call-process-region (point-min) (point-max) "diff" nil outbuff nil "-n" "-" tmpfile))
156+
(message "File is already formatted")
157+
(progn
158+
(elixir-format--apply-rcs-patch outbuff)
159+
(message "mix format applied")))
160+
(kill-buffer errbuff))
161+
162+
(progn
163+
(with-current-buffer errbuff
164+
(setq buffer-read-only t)
165+
(ansi-color-apply-on-region (point-min) (point-max))
166+
(special-mode))
167+
168+
(if is-interactive
169+
(display-buffer errbuff)
170+
(error "elixir-format failed: see %s" (buffer-name errbuff)))))
171+
172+
(delete-file tmpfile)
173+
(kill-buffer outbuff)))))
174+
175+
(provide 'elixir-format)
176+
177+
;;; elixir-format.el ends here

elixir-mode.el

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
(require 'easymenu) ; Elixir Mode menu definition
4040
(require 'elixir-smie) ; Syntax and indentation support
4141
(require 'pkg-info) ; Display Elixir Mode package version
42+
(require 'elixir-format) ; Elixir Format functions
4243

4344
(defgroup elixir nil
4445
"Major mode for editing Elixir code."

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)