Skip to content

Commit 8721118

Browse files
anildigitalTrevoke
authored andcommitted
Add elixir-format function to format Elixir 1.6 files. (#406)
* Add elixir-format function to format Elixir 1.6 files. Co-Authored-By: Christian Kruse <[email protected]> Co-Authored-By: Peter Urbak <[email protected]> * Require ansi-color explicitly
1 parent e9deded commit 8721118

File tree

5 files changed

+331
-0
lines changed

5 files changed

+331
-0
lines changed

README.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ Provides font-locking, indentation and navigation support for the
1818
- [Keymapping](#keymapping)
1919
- [Notes](#notes)
2020
- [Elixir Tooling Integration](#elixir-tooling-integration)
21+
- [Elixir Format](#elixir-format)
2122
- [History](#history)
2223
- [Contributing](#contributing)
2324
- [License](#license)
@@ -147,6 +148,74 @@ If you looking for elixir tooling integration for Emacs, check: [alchemist.el](h
147148

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

151+
152+
## Elixir Format
153+
154+
### Setup of elixir-format
155+
Customize the elixir and mix paths
156+
157+
In Emacs, run following command to customize option
158+
``` elisp
159+
M-x customize-option
160+
161+
Customize-variable: elixir-format-elixir-path
162+
```
163+
and set your elixir executable path there. After that run:
164+
``` elisp
165+
M-x customize-option
166+
167+
Customize-variable: elixir-format-mix-path
168+
```
169+
and set your mix executable path there.
170+
171+
Your machine's elixir and mix executable paths can be found with `which` command as shown below
172+
173+
``` shell
174+
$ which elixir
175+
/usr/local/bin/elixir
176+
177+
$ which mix
178+
/usr/local/bin/mix
179+
```
180+
Alternavively you can define variables as below
181+
182+
``` elisp
183+
(setq elixir-format-elixir-path "/usr/local/bin/elixir")
184+
(setq elixir-format-mix-path "/usr/local/bin/mix")
185+
```
186+
187+
### Use it
188+
189+
``` elisp
190+
M-x elixir-format
191+
```
192+
193+
### Add elixir-mode hook to run elixir format on file save
194+
195+
``` elisp
196+
;; Create a buffer-local hook to run elixir-format on save, only when we enable elixir-mode.
197+
(add-hook 'elixir-mode-hook
198+
(lambda () (add-hook 'before-save-hook 'elixir-format nil t)))
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+
150219
## History
151220

152221
This mode is based on the

elixir-format.el

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

test/test-helper.el

Lines changed: 37 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,38 @@
6366
(defun ert-runner/run-tests-batch-and-exit (selector)
6467
(ert-run-tests-interactively selector)))
6568

69+
(setq elixir-format-elixir-path (executable-find "elixir"))
70+
(setq elixir-format-mix-path (executable-find "mix"))
71+
72+
(defconst elixir-format-test-example "defmodule Foo do
73+
use GenServer.Behaviour
74+
def foobar do
75+
if true, do: IO.puts \"yay\"
76+
end
77+
end")
78+
79+
(defconst elixir-format-wrong-test-example "defmodule Foo do
80+
use GenServer.Behaviour
81+
def foobar do
82+
if true, do: IO.puts \"yay\"
83+
end")
84+
85+
(setq elixir-version (let ((str (shell-command-to-string (concat elixir-format-elixir-path " --version"))))
86+
(car (when (string-match "\s\\(.+[.].+[.].+\\)[\s\n]" str)
87+
(list (match-string 1 str))))))
88+
89+
(defconst elixir-formatter-supported
90+
(>= (string-to-number elixir-version) (string-to-number "1.6"))
91+
)
92+
93+
(defconst elixir-format-formatted-test-example
94+
"defmodule Foo do
95+
use GenServer.Behaviour
96+
97+
def foobar do
98+
if true, do: IO.puts(\"yay\")
99+
end
100+
end
101+
")
102+
66103
;;; test-helper.el ends here

0 commit comments

Comments
 (0)