Skip to content

Commit 32f9752

Browse files
anildigitalckrusedragonwasrobot
committed
Add mix-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 32f9752

File tree

1 file changed

+231
-0
lines changed

1 file changed

+231
-0
lines changed

mix-format.el

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

0 commit comments

Comments
 (0)