Skip to content

Commit db42220

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 db42220

File tree

5 files changed

+329
-0
lines changed

5 files changed

+329
-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: 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: 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)