-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy patharei.el
389 lines (330 loc) · 13.3 KB
/
arei.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
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
;;; arei.el --- Asynchronous Reliable Extensible IDE -*- lexical-binding:t; coding:utf-8 -*-
;; Copyright © 2023, 2024 Andrew Tropin
;; Copyright © 2024 Nikita Domnitskii
;; Author: Andrew Tropin <[email protected]>
;; Nikita Domnitskii <[email protected]>
;;
;; Version: 0.9.5
;; Homepage: https://trop.in/rde
;; Package-Requires: ((emacs "29") (eros "0.1.0") (sesman "0.3.2") (queue "0.2"))
;; Keywords: languages, guile, scheme, nrepl
;; 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 of the License, 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 program. If not, see <https://www.gnu.org/licenses/>.
;;; Commentary:
;; An Interactive Development Environment for Guile
;;; Code:
(require 'arei-client)
(require 'arei-nrepl)
(require 'arei-eldoc)
(require 'arei-xref)
(require 'arei-completion)
(require 'arei-spinner)
(require 'arei-evaluation)
(require 'arei-module)
(require 'arei-macroexpansion)
(require 'scheme)
(require 'sesman)
(eval-when-compile (require 'subr-x))
(eval-when-compile (require 'pcase))
(eval-when-compile (require 'map))
(defgroup arei nil
"Asynchronous Reliable Extensible IDE."
:prefix "arei-"
:group 'applications)
(defcustom arei-mode-auto t
"Whether `arei-mode' should be active by default in all scheme buffers."
:type 'boolean)
(defcustom arei-connection-buffer-display-function 'display-buffer
"Controls how to display the connection buffer on connect.
When set to nil the buffer will only be created, and not
displayed. When it set to function the buffer will be displayed
using this function. `display-buffer' and `pop-to-buffer' are
most obvious candidates here."
:type '(choice (function :tag "Function")
(const :tag "None" nil)))
(defun arei--get-command-keybindings (command)
"Return key bindings for COMMAND as a comma-separated string."
(let ((keys (mapcar 'key-description (where-is-internal command nil nil t))))
(if keys
(mapconcat 'identity keys ", ")
"M-x ... RET")))
(defcustom arei-get-greeting-message
(lambda ()
(apply
'format
"Commands for session and connection management:
- `sesman-start' (%s) to connect to nrepl server.
- `universal-argument' (%s) to select a connection endpoint (host and port).
- `sesman-quit' (%s) to close the connection.
Development related and other commands:
- `arei-evaluate-last-sexp' (%s) to evaluate sexp before point.
- `arei-evaluate-buffer' (%s) to evaluate the whole buffer.
- `arei-evaluate-sexp' (%s) to interactively evaluate the expression."
(mapcar 'arei--get-command-keybindings
`(sesman-start
universal-argument
sesman-quit
arei-evaluate-last-sexp
arei-evaluate-buffer
arei-evaluate-sexp))))
"A function returning a message shown on connection creation"
:type 'function)
;;;
;;; Sesman
;;;
;; (defun arei-sesman-sessions ()
;; "Return a list of all active Arei sesman sessions."
;; (sesman-sessions 'Arei))
(defun arei--sesman--format-link (link)
"Like original `sesman--format-link', but wraps values into format
to ensure that they are string and propertize can handle them."
(let* ((system (sesman--lnk-system-name link))
(session (gethash (car link) sesman-sessions-hashmap)))
(format "%s(%s) -> %s [%s]"
(sesman--lnk-context-type link)
(propertize
(format "%s" (sesman--abbrev-path-maybe (sesman--lnk-value link)))
'face 'bold)
(propertize
(format "%s" (sesman--lnk-session-name link))
'face 'bold)
(if session
(sesman--format-session-objects system session)
"invalid"))))
(advice-add 'sesman--format-link :override 'arei--sesman--format-link)
(cl-defmethod sesman-project ((_system (eql Arei)))
"Find project directory."
(when (project-current)
(project-root (project-current))))
(cl-defmethod sesman-start-session ((_system (eql Arei)))
"Start a connection."
(call-interactively #'arei--start))
(cl-defmethod sesman-friendly-session-p ((_system (eql Arei)) session)
;; We can't use `arei-connection-buffer' and
;; `arei--tooling-session-id' here, because it will lead to infinite
;; recursion.
(let* ((conn (cadr session))
(file (buffer-file-name))
(tooling-session (with-current-buffer conn
(gethash "tooling" arei-client--nrepl-sessions)))
(load-path (thread-first
(with-current-buffer conn
(arei-nrepl-dict
"id" (number-to-string
(cl-incf arei-client--request-counter))
"op" "ares.guile.utils/load-path"))
(arei-client--send-sync-request conn tooling-session)
(arei-nrepl-dict-get "load-path"))))
(seq-find (lambda (path) (string-prefix-p path file)) load-path)))
(cl-defmethod sesman-quit-session ((_system (eql Arei)) session)
"Quit an Arei SESSION."
(let ((kill-buffer-query-functions nil))
(kill-buffer (cadr session))))
(cl-defmethod sesman-restart-session ((system (eql Arei)) session)
"Just quit and new start session."
(sesman-quit-session system session)
(sesman-start-session system))
;;;
;;; Overlay
;;;
(defun arei--comment-string (str)
"Add comment-prefix to "
(let ((lines (split-string str "\n"))
(comment-prefix ";;")
(commented-lines '()))
(dolist (line lines)
(push (concat comment-prefix " " line) commented-lines))
(string-join (reverse commented-lines) "\n")))
(defun arei--insert-greeting-message (buffer)
"Insert greeting message into buffer."
(let ((initial-buffer (current-buffer)))
(with-current-buffer buffer
(insert
(propertize
(arei--comment-string
(with-current-buffer initial-buffer
(funcall arei-get-greeting-message)))
'face
'((t (:inherit font-lock-comment-face)))))
(insert "\n"))))
(defun arei--create-params-plist (arg)
"Create initial PARAMS plist based on ARG vlaue."
(cond ((equal arg '(4)) (list :select-endpoint t))
(t nil)))
(defun arei--nrepl-port-string-to-number (s)
"Converts `S' from string to number when suitable."
(when (string-match "^\\([0-9]+\\)" s)
(string-to-number (match-string 0 s))))
(defun arei--read-nrepl-port-from-file (file)
"Attempts to read port from a file named by FILE.
Discards it if it can be determined that the port is not active."
(when (file-exists-p file)
(when-let* ((port-string (with-temp-buffer
(insert-file-contents file)
(buffer-string)))
(port-number (arei--nrepl-port-string-to-number port-string)))
(let ((lsof (executable-find "lsof")))
(if (and lsof port-number)
(unless (equal ""
(shell-command-to-string
(format "%s -i:%s" lsof port-number)))
port-number)
port-number)))))
(defun arei--try-find-nrepl-port-around ()
"Try to find a `.nrepl-port' file and read a port number from
it. Start with a file in current directory, fallback to the one
in a project root.
Uses `arei--read-nrepl-port-from-file', so if there is `lsof' and
it shows that there is no process attached to the port, the port
will be ignored and function will try to find the next one.
Return nil if nothing found."
(let ((possible-nrepl-port-files
(list
(expand-file-name ".nrepl-port")
(when (project-current)
(expand-file-name
".nrepl-port"
(file-name-as-directory (project-root (project-current))))))))
(seq-some
(lambda (f)
(when-let (port (and f (arei--read-nrepl-port-from-file f)))
port))
possible-nrepl-port-files)))
(defun arei--select-endpoint (params)
"Read HOST and PORT from minibuffer and put them into plist."
(let* ((nrepl-port-nearby (arei--try-find-nrepl-port-around))
(default-host (or (plist-get params :host) "localhost"))
(default-port (or (plist-get params :port) nrepl-port-nearby 7888)))
(if (plist-get params :select-endpoint)
(list
:host
(read-string (format "host (default %s): " default-host)
nil 'arei-host default-host)
:port
(read-number "port: " default-port 'arei-port))
(list :host default-host :port default-port))))
(defun arei--start (arg)
"Connect to remote endpoint using provided :HOST and :PORT
from PARAMS plist. Initialize sesman session. Read values from
minibuffer if called with prefix argument. Users should not call
this function directly."
(interactive "P")
(when-let ((connection-buffer
(arei-client--connect
(thread-first
(arei--create-params-plist arg)
(arei--select-endpoint)))))
(arei--insert-greeting-message connection-buffer)
(when (fboundp arei-connection-buffer-display-function)
(funcall arei-connection-buffer-display-function connection-buffer))
connection-buffer))
;;;
;;; arei-mode
;;;
(defun arei--modeline-request-count ()
(and arei-client--pending-requests
(list (number-to-string
(hash-table-count arei-client--pending-requests)))))
(defun arei--modeline-connection-info ()
(list
"["
(save-current-buffer
(when-let* ((buff (arei-connection-buffer))) (set-buffer buff))
(cond
((not (arei-connected-p))
"disconnected")
((hash-table-empty-p arei-client--pending-requests)
(arei-spinner-stop)
"connected")
((null (arei-spinner-modeline))
(arei-spinner-start))
(t
(list
(arei-spinner-modeline)
" "
(arei--modeline-request-count)))))
"]"))
(defvar arei-mode-line
'(" arei" (:eval (arei--modeline-connection-info))) "\
Mode line lighter for arei mode.
The value of this variable is a mode line template as in
`mode-line-format'. See Info Node `(elisp)Mode Line Format' for details
about mode line templates.
Customize this variable to change how arei mode displays its status in the
mode line. The default value displays the current connection. Set this
variable to nil to disable the mode line entirely.")
(put 'arei-mode-line 'risky-local-variable t)
(defvar-keymap arei-mode-map
"C-c C-z" #'arei-switch-to-connection-buffer
"C-c C-e" arei-evaluation-keymap
"C-x C-e" #'arei-evaluate-last-sexp
"C-c C-k" #'arei-evaluate-buffer
"C-c C-i" #'arei-interrupt-evaluation
"C-c M-m" arei-module-map
"C-c C-m" arei-macroexpansion-map
"C-M-x" #'arei-evaluate-enclosing-outer-form
"C-c C-c" #'arei-evaluate-enclosing-inner-form)
;;;###autoload
(define-minor-mode arei-mode
"Minor mode for REPL interaction from a buffer.
\\{arei-mode-map}"
:init-value nil
:lighter arei-mode-line
:keymap arei-mode-map
(if arei-mode
(progn
(setq sesman-system 'Arei)
(add-hook 'eldoc-documentation-functions #'arei-eldoc-arglist nil t)
(add-hook 'completion-at-point-functions #'arei-complete-at-point nil t)
(add-hook 'xref-backend-functions #'arei--xref-backend nil t)
(add-hook 'kill-buffer-hook #'arei-client-remove-from-sesman-session-cache nil t)
(add-hook 'sesman-post-command-hook #'arei-client-clear-sesman-session-cache nil t))
(remove-hook 'completion-at-point-functions #'arei-complete-at-point t)
(remove-hook 'xref-backend-functions #'arei--xref-backend t)
(remove-hook 'eldoc-documentation-functions #'arei-eldoc-arglist t)
(remove-hook 'kill-buffer-hook #'arei-client-remove-from-sesman-session-cache t)
(remove-hook 'sesman-post-command-hook #'arei-client-clear-sesman-session-cache t)))
;;;###autoload
(defun arei-mode--maybe-activate ()
"Activates `arei-mode' if `arei-mode-auto' is t."
(when arei-mode-auto
(arei-mode)))
;;;###autoload
(defun arei ()
"Connect to nrepl server."
(interactive)
(if (sesman-current-session 'Arei)
(sesman-restart)
(sesman-start)))
;;;###autoload
(defun arei--enable-on-existing-scheme-buffers ()
"Enable Arei's minor mode on existing Scheme buffers.
See command `arei-mode'."
(let ((scm-buffers (seq-filter
(lambda (buffer)
(with-current-buffer buffer
(derived-mode-p 'scheme-mode)))
(buffer-list))))
(dolist (buffer scm-buffers)
(with-current-buffer buffer
(unless arei-mode
(arei-mode--maybe-activate))))))
;;;###autoload
(with-eval-after-load 'scheme
(keymap-set scheme-mode-map "C-c C-a" #'arei)
(keymap-set scheme-mode-map "C-c C-s" 'sesman-map)
(require 'sesman) ; sesman-install-menu is not autoloaded
(sesman-install-menu scheme-mode-map)
(add-hook 'scheme-mode-hook 'arei-mode--maybe-activate)
(arei--enable-on-existing-scheme-buffers))
;; TODO: Scratch buffer
(provide 'arei)
;;; arei.el ends here