-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathinf-elixir.el
336 lines (269 loc) · 12.5 KB
/
inf-elixir.el
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
;;; inf-elixir.el --- Run an interactive Elixir shell -*- lexical-binding: t -*-
;; Copyright © 2019–2021 Jonathan Arnett <[email protected]>
;; Author: Jonathan Arnett <[email protected]>
;; URL: https://github.com/J3RN/inf-elixir
;; Keywords: languages, processes, tools
;; Version: 2.1.2
;; Package-Requires: ((emacs "25.1"))
;; This file is NOT part of GNU Emacs.
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation; either version 3, or (at your option)
;; any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this package. If not, see https://www.gnu.org/licenses.
;;; Commentary:
;; Provides access to an IEx shell buffer, optionally running a
;; specific command (e.g. iex -S mix, iex -S mix phx.server, etc)
;;; Code:
(require 'comint)
(require 'subr-x)
(require 'map)
;;; Customization
(defgroup inf-elixir nil
"Ability to interact with an Elixir REPL."
:prefix "inf-elixir-"
:group 'languages)
(defcustom inf-elixir-prefer-umbrella t
"Whether or not to prefer running the REPL from the umbrella, if it exists.
If there is no umbrella project, the value of this variable is irrelevant."
:type 'boolean
:group 'inf-elixir)
(defcustom inf-elixir-base-command "iex"
"The command that forms the base of all REPL commands.
Should be able to be run without any arguments."
:type 'string
:group 'inf-elixir)
(defcustom inf-elixir-project-command "iex -S mix"
"The command used to start a REPL in the context of the current project."
:type 'string
:group 'inf-elixir)
(defcustom inf-elixir-open-command "emacsclient --no-wait +__LINE__ __FILE__"
"Value to be populated into the `ELIXIR_EDITOR' environment variable.
The `ELIXIR_EDITOR' is used by the IEx `open/1' helper to open files from the
REPL. Run `h(open)' in an IEx shell for more information about `ELIXIR_EDITOR'.
NOTE: Changing this variable will not affect running REPLs."
:type 'string
:group 'inf-elixir)
(defcustom inf-elixir-switch-to-repl-on-send t
"If t, switch to the corresponding repl window on any send command."
:type 'boolean
:group 'inf-elixir)
;;; Mode definitions and configuration
(defvar inf-elixir-repl-buffer nil
"Override for what REPL buffer code snippets should be sent to.
If this variable is set and the corresponding REPL buffer exists
and has a living process, all `inf-elixir-send-*' commands will
send to it. If this variable is unset (the default) or the
indicated buffer is dead or has a dead process, a warning will be
printed instead.")
(defvar inf-elixir-project-buffers (make-hash-table :test 'equal)
"A mapping of projects to buffers with running Elixir REPL subprocesses.")
(defvar inf-elixir-unaffiliated-buffers '()
"A list of Elixir REPL buffers unaffiliated with any project.")
;;;###autoload
(define-minor-mode inf-elixir-minor-mode
"Minor mode for Elixir buffers that allows interacting with the REPL.")
;;;###autoload
(define-derived-mode inf-elixir-mode comint-mode "Inf-Elixir"
"Major mode for interacting with an Elixir REPL."
(setq-local comint-prompt-read-only t))
;;; Private functions
(defun inf-elixir--up-directory (dir)
"Return the directory above DIR."
(file-name-directory (directory-file-name dir)))
(defun inf-elixir--find-umbrella-root (start-dir)
"Traverse upwards from START-DIR until highest mix.exs file is discovered."
(when-let ((project-dir (locate-dominating-file start-dir "mix.exs")))
(or (inf-elixir--find-umbrella-root (inf-elixir--up-directory project-dir))
project-dir)))
(defun inf-elixir--find-project-root ()
"Find the root of the current Elixir project."
(if inf-elixir-prefer-umbrella
(inf-elixir--find-umbrella-root default-directory)
(locate-dominating-file default-directory "mix.exs")))
(defun inf-elixir--get-project-buffer (dir)
"Find the REPL buffer for project DIR."
(gethash dir inf-elixir-project-buffers))
(defun inf-elixir--set-project-buffer (dir buf)
"Set BUF to be the REPL buffer for project DIR."
(puthash dir buf inf-elixir-project-buffers))
(defun inf-elixir--get-project-process (dir)
"Find the process for project DIR."
(get-buffer-process (inf-elixir--get-project-buffer dir)))
(defun inf-elixir--project-name (dir)
"Determine a human-readable name for DIR."
(if dir
(file-name-nondirectory (directory-file-name dir))
""))
(defun inf-elixir--buffer-name (dir)
"Generate a REPL buffer name for DIR."
(if dir
(concat "Inf-Elixir - " (inf-elixir--project-name dir))
"Inf-Elixir"))
(defun inf-elixir--maybe-kill-repl (dir)
"If a REPL is already running in DIR, ask user if they want to kill it."
(let ((proc (inf-elixir--get-project-process dir))
(name (inf-elixir--project-name dir)))
(when (and
proc
(yes-or-no-p
(concat "An Elixir REPL is already running in " name ". Kill it? ")))
(delete-process proc))))
(defun inf-elixir--maybe-clear-repl (dir)
"Clear the REPL buffer for project DIR if the process is dead."
(when-let ((buf (inf-elixir--get-project-buffer dir)))
(if (and
(buffer-live-p buf)
(not (process-live-p (get-buffer-process buf))))
(with-current-buffer buf (let ((inhibit-read-only t)) (erase-buffer))))))
(defun inf-elixir--maybe-start-repl (dir cmd)
"If no REPL is running in DIR, start one with CMD.
Always returns a REPL buffer for DIR."
(let ((buf-name (inf-elixir--buffer-name dir)))
(if (process-live-p (inf-elixir--get-project-process dir))
(inf-elixir--get-project-buffer dir)
(setenv "ELIXIR_EDITOR" inf-elixir-open-command)
(with-current-buffer
(apply #'make-comint-in-buffer buf-name nil (car cmd) nil (cdr cmd))
(inf-elixir-mode)
(if dir
(inf-elixir--set-project-buffer dir (current-buffer))
(add-to-list 'inf-elixir-unaffiliated-buffers (current-buffer)))
(current-buffer)))))
(defun inf-elixir--send (cmd)
"Determine where to send CMD and send it."
(when-let ((buf (inf-elixir--determine-repl-buf)))
(with-current-buffer buf
(comint-add-to-input-history cmd)
(comint-send-string buf (concat cmd "\n")))
(if inf-elixir-switch-to-repl-on-send
(pop-to-buffer buf))))
(defun inf-elixir--determine-repl-buf ()
"Determines where to send a cmd when `inf-elixir-send-*' are used."
(if inf-elixir-repl-buffer
(if (process-live-p (get-buffer-process inf-elixir-repl-buffer))
inf-elixir-repl-buffer
(inf-elixir--prompt-repl-buffers "`inf-elixir-repl-buffer' is dead, please choose another REPL buffer: "))
(if-let ((proj-dir (inf-elixir--find-project-root)))
(inf-elixir--determine-project-repl-buf proj-dir)
(inf-elixir--prompt-repl-buffers))))
(defun inf-elixir--determine-project-repl-buf (proj-dir)
"Determines where to send a cmd when `inf-elixir-send-*' are used inside the PROJ-DIR Mix project."
(if-let ((proj-buf (inf-elixir--get-project-buffer proj-dir)))
(if (process-live-p (get-buffer-process proj-buf))
proj-buf
(if (y-or-n-p "A project REPL buffer exists, but the process is dead. Start a new one? ")
(inf-elixir-project)
(inf-elixir--prompt-repl-buffers)))
(if (y-or-n-p "No REPL exists for this project yet. Start one? ")
(inf-elixir-project)
(inf-elixir--prompt-repl-buffers))))
(defun inf-elixir--prompt-repl-buffers (&optional prompt)
"Prompt the user to select an inf-elixir REPL buffers or create an new one.
Returns the select buffer (as a buffer object).
If PROMPT is supplied, it is used as the prompt for a REPL buffer.
When the user selects a REPL, it is set as `inf-elixir-repl-buffer' locally in
the buffer so that the choice is remembered for that buffer."
;; Cleanup
(setq inf-elixir-unaffiliated-buffers (seq-filter 'buffer-live-p inf-elixir-unaffiliated-buffers))
(setq inf-elixir-project-buffers
(map-into
(map-filter (lambda (_dir buf) (buffer-live-p buf)) inf-elixir-project-buffers)
'(hash-table :test equal)))
;; Actual functionality
(let* ((repl-buffers (append
'("Create new")
(mapcar (lambda (buf) `(,(buffer-name buf) . buf)) inf-elixir-unaffiliated-buffers)
(mapcar (lambda (buf) `(,(buffer-name buf) . buf)) (hash-table-values inf-elixir-project-buffers))))
(prompt (or prompt "Which REPL?"))
(selected-buf (completing-read prompt repl-buffers (lambda (_) t) t)))
(setq-local inf-elixir-repl-buffer (if (equal selected-buf "Create new")
(inf-elixir)
selected-buf))))
(defun inf-elixir--matches-in-buffer (regexp &optional buffer)
"return a list of matches of REGEXP in BUFFER or the current buffer if not given."
(let ((matches))
(save-match-data
(save-excursion
(with-current-buffer (or buffer (current-buffer))
(save-restriction
(widen)
(goto-char 1)
(while (search-forward-regexp regexp nil t 1)
(push (match-string 1) matches)))))
matches)))
;;; Public functions
;;;###autoload
(defun inf-elixir-run-cmd (dir cmd)
"Create a new IEx buffer and run CMD in project DIR.
DIR should be an absolute path to the root level of a Mix project (where the
mix.exs file is). A value of nil for DIR indicates that the REPL should not
belong to any project."
(inf-elixir--maybe-kill-repl dir)
(inf-elixir--maybe-clear-repl dir)
(pop-to-buffer (inf-elixir--maybe-start-repl dir (split-string cmd))))
;;;###autoload
(defun inf-elixir (&optional cmd)
"Create an Elixir REPL, using CMD if given.
When called from ELisp, an argument (CMD) can be passed which will be the
command run to start the REPL. The default is provided by
`inf-elixir-base-command'.
When called interactively with a prefix argument, the user will
be prompted for the REPL command. The default is provided by
`inf-elixir-base-command'."
(interactive)
(let ((cmd (cond
(cmd cmd)
(current-prefix-arg (read-from-minibuffer "Command: " inf-elixir-base-command nil nil 'inf-elixir))
(t inf-elixir-base-command))))
(inf-elixir-run-cmd nil cmd)))
;;;###autoload
(defun inf-elixir-project (&optional cmd)
"Create a REPL in the context of the current project, using CMD if given.
If an existing REPL already exists for the current project, the user will be
asked whether they want to keep the running REPL or replace it.
When called from ELisp, an argument (CMD) can be passed which will be the
command run to start the REPL. The default is provided by
`inf-elixir-project-command'.
When called interactively with a prefix argument, the user will
be prompted for the REPL command. The default is provided by
`inf-elixir-project-command'."
(interactive)
(if-let ((default-directory (inf-elixir--find-project-root)))
(let ((cmd (cond
(cmd cmd)
(current-prefix-arg (read-from-minibuffer "Project command: " inf-elixir-project-command nil nil 'inf-elixir-project))
(t inf-elixir-project-command))))
(inf-elixir-run-cmd default-directory cmd))
(message "Could not find project root! Try `inf-elixir' instead.")))
(defun inf-elixir-set-repl ()
"Select which REPL to use for this buffer."
(interactive)
(inf-elixir--prompt-repl-buffers))
(defun inf-elixir-send-line ()
"Send the region to the REPL buffer and run it."
(interactive)
(inf-elixir--send (buffer-substring (line-beginning-position) (line-end-position))))
(defun inf-elixir-send-region ()
"Send the region to the REPL buffer and run it."
(interactive)
(inf-elixir--send (buffer-substring (point) (mark))))
(defun inf-elixir-send-buffer ()
"Send the buffer to the REPL buffer and run it."
(interactive)
(inf-elixir--send (buffer-string)))
(defun inf-elixir-reload-module ()
"Send command to reload module using `IEx.Helpers.r/1` and run it.
It will recompile all modules defined in the current file"
(interactive)
(inf-elixir--send
(format "r(%s)" (nth 0 (inf-elixir--matches-in-buffer "defmodule \\([A-Z][A-Za-z0-9\._]+\\)\s+")))))
(provide 'inf-elixir)
;;; inf-elixir.el ends here