-
-
Notifications
You must be signed in to change notification settings - Fork 76
/
nix-repl.el
221 lines (193 loc) · 8.1 KB
/
nix-repl.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
;;; nix-repl.el --- Nix repl -*- lexical-binding: t -*-
;; This file is NOT part of GNU Emacs.
;; Homepage: https://github.com/NixOS/nix-mode
;; This file is NOT part of GNU Emacs.
;;; Commentary:
;;; Code:
(defvar nix-prompt-regexp "nix-repl> ")
(require 'comint)
(require 'nix)
(defgroup nix-repl nil
"Nix-repl customizations."
:group 'nix)
(defcustom nix-repl-executable-args '("repl")
"Arguments to provide to nix-repl."
:type '(repeat string))
(defvar nix-repl-completion-redirect-buffer
" *nix-repl completions redirect*"
"Buffer to be used to redirect output of readline commands.")
(defcustom nix-repl-completion-output-timeout 1.0
"Time in seconds to wait for completion output before giving up."
:type 'float)
(defvar nix-repl-mode-map
(let ((map (make-sparse-keymap)))
(define-key map "\t" 'completion-at-point)
map))
(defun nix-repl-save-all-histories ()
"Call `comint-write-input-ring' for all `nix-repl-mode' buffers."
(dolist (buf (buffer-list))
(with-current-buffer buf
(when (eq major-mode 'nix-repl-mode)
(comint-write-input-ring)))))
(define-derived-mode nix-repl-mode comint-mode "Nix-REPL"
"Interactive prompt for Nix."
:interactive nil
(setq-local comint-prompt-regexp nix-prompt-regexp)
(setq-local comint-prompt-read-only t)
(let* ((is-remote (file-remote-p default-directory))
(maybe-xdg-data-home (if is-remote
(shell-command-to-string "echo -n $XDG_DATA_HOME")
(or (getenv "XDG_DATA_HOME")
"")))
(path-prefix (if (string-empty-p maybe-xdg-data-home)
"~/.local/share"
maybe-xdg-data-home))
(history-path (concat
is-remote
path-prefix
"/nix/repl-history")))
(setq-local comint-input-ring-file-name history-path))
(comint-read-input-ring t)
(add-hook 'kill-buffer-hook #'comint-write-input-ring nil 'local)
(add-hook 'kill-emacs-hook #'nix-repl-save-all-histories nil 'local)
(add-hook 'completion-at-point-functions
#'nix-repl-completion-at-point nil 'local))
(defmacro nix--with-temp-process-filter (proc &rest body)
"Use temp process PROC filter on BODY."
(declare (indent defun))
`(let* ((buf (generate-new-buffer " *temp-process-output*"))
(proc-filter-saved (process-filter ,proc))
(proc-marker (with-current-buffer buf (point-marker))))
(set-process-filter ,proc (nix--process-filter buf proc-marker))
(unwind-protect
(with-current-buffer buf
,@body)
(set-process-filter ,proc proc-filter-saved)
(kill-buffer buf))))
;;;###autoload
(defun nix-repl ()
"Load the Nix-REPL."
(interactive)
(pop-to-buffer-same-window
(get-buffer-create "*Nix-REPL*"))
(unless (comint-check-proc (current-buffer))
(nix--make-repl-in-buffer (current-buffer))
(nix-repl-mode)))
(defalias 'nix-repl-show 'nix-repl)
(defun nix--make-repl-in-buffer (buffer)
"Make Nix Repl in BUFFER."
(apply
'make-comint-in-buffer
(append `("Nix-REPL" ,buffer ,nix-executable nil)
nix-repl-executable-args)))
(defun nix-get-completions (process input)
"Get completions for INPUT using native readline for PROCESS."
(with-current-buffer (process-buffer process)
(let* ((original-filter-fn (process-filter process))
(redirect-buffer (get-buffer-create
nix-repl-completion-redirect-buffer))
(trigger "\t")
(new-input (concat input trigger))
(input-length
(save-excursion
(+ (- (point-max) (comint-bol)) (length new-input))))
(delete-line-command (make-string input-length ?\b))
(input-to-send (concat new-input delete-line-command)))
;; Ensure restoring the process filter, even if the user quits
;; or there's some other error.
(unwind-protect
(with-current-buffer redirect-buffer
;; Cleanup the redirect buffer
(erase-buffer)
;; Mimic `comint-redirect-send-command', unfortunately it
;; can't be used here because it expects a newline in the
;; command and that's exactly what we are trying to avoid.
(let ((comint-redirect-echo-input nil)
(comint-redirect-completed nil)
(comint-redirect-perform-sanity-check nil)
(comint-redirect-insert-matching-regexp t)
(comint-redirect-finished-regexp nix-prompt-regexp)
(comint-redirect-output-buffer redirect-buffer))
(set-process-filter
process (apply-partially
#'comint-redirect-filter original-filter-fn))
(process-send-string process input-to-send)
;; Grab output until our dummy completion used as
;; output end marker is found.
(when (nix--accept-process-output
process nix-repl-completion-output-timeout
comint-redirect-finished-regexp)
(beginning-of-line)
(if (eq (char-after) ?\r)
(cdr
(split-string
(buffer-substring-no-properties
(line-beginning-position) (point-min))
"[ \f\t\n\r\v]+" t))
(search-forward "" nil t)
(backward-char)
(if (eq (char-before) ?\a)
nil
(list (buffer-substring-no-properties (line-beginning-position) (point))))))))
(set-process-filter process original-filter-fn)))))
(defun nix--accept-process-output (process &optional timeout regexp)
"Accept PROCESS output with TIMEOUT until REGEXP is found.
Optional argument TIMEOUT is the timeout argument to
`accept-process-output' calls. Optional argument REGEXP
overrides the regexp to match the end of output, defaults to
`comint-prompt-regexp'. Returns non-nil when output was
properly captured.
This utility is useful in situations where the output may be
received in chunks, since `accept-process-output' gives no
guarantees they will be grabbed in a single call."
(let ((regexp (or regexp comint-prompt-regexp)))
(catch 'found
(while t
(when (not (accept-process-output process timeout))
(throw 'found nil))
(when (progn (re-search-backward regexp nil t))
(throw 'found t))))))
;;;###autoload
(defun nix-repl-completion-at-point ()
"Completion at point function for Nix using \"nix-repl\".
See `completion-at-point-functions'."
(save-excursion
(let* ((proc (get-buffer-process (current-buffer)))
(prefix (and (derived-mode-p 'nix-repl-mode)
proc
(executable-find nix-executable)
(nix--prefix-bounds))))
(pcase prefix
(`(,beg . ,end)
(list beg end
(nix-get-completions
proc
(buffer-substring beg end))
:exclusive 'no))))))
(defun nix--prefix-bounds ()
"Get bounds of Nix attribute path at point as a (BEG . END) pair, or nil."
(save-excursion
(when (< (skip-chars-backward "a-zA-Z0-9'\\-_\\.") 0)
(cons (point) (+ (point) (skip-chars-forward "a-zA-Z0-9'\\-_\\."))))))
(defun nix--send-repl (input &optional process mute)
"Send INPUT to PROCESS.
MUTE if true then don’t alert user."
(let ((proc (or process (get-buffer-process (current-buffer)))))
(if mute
(nix--with-temp-process-filter proc
(process-send-string proc input))
(process-send-string proc input))))
(defun nix--char-with-ctrl (char)
"Generate control character CHAR."
(char-to-string (logand #b10011111 char)))
(defun nix--process-filter (buf marker)
"Process filter for Nix-rel buffer BUF at MARKER."
(lambda (_proc string)
(when (buffer-live-p buf)
(with-current-buffer buf
(save-excursion
(goto-char marker)
(insert string)
(set-marker marker (point)))))))
(provide 'nix-repl)
;;; nix-repl.el ends here