Skip to content

Include user function to create a socket repl inside emacs/inf-clojure #210

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* [#202](https://github.com/clojure-emacs/inf-clojure/issues/202): Add ClojureCLR support.
* [#204](https://github.com/clojure-emacs/inf-clojure/issues/204): Scroll repl buffer on insert commands
* [#208](https://github.com/clojure-emacs/inf-clojure/pull/208) Display message after setting repl.
* [#210](https://github.com/clojure-emacs/inf-clojure/pull/210) Include `inf-clojure-socket-repl` to create a socket REPL and connect to it from inside Emacs.


## 3.2.1 (2022-07-22)
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ common startup forms. You can select one of these or type in your own
custom startup. This will start a REPL process for the current project
and you can start interacting with it.

If you want to use a socket REPL server, use `M-x inf-clojure-socket-repl`
which will start a socket server and connect to it for you.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

socket REPL server


If you've already started a socket REPL server, use `M-x inf-clojure-connect`
and enter its host and port numbers.

Expand Down
178 changes: 152 additions & 26 deletions inf-clojure.el
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
;; URL: http://github.com/clojure-emacs/inf-clojure
;; Keywords: processes, comint, clojure
;; Version: 3.2.1
;; Package-Requires: ((emacs "25.1") (clojure-mode "5.11"))
;; Package-Requires: ((emacs "26.2") (clojure-mode "5.11"))

;; This file is not part of GNU Emacs.

Expand Down Expand Up @@ -74,7 +74,7 @@
(defvar inf-clojure-startup-forms '((lein . "lein repl")
(boot . "boot repl")
(clojure . "clojure")
(cljs . "clojure -m cljs.main -r")
(cljs . "clojure -M -m cljs.main -r")
(lein-clr . "lein clr repl")
(planck . "planck -d")
(babashka . "bb")
Expand Down Expand Up @@ -140,6 +140,17 @@
(set-ns . "(clojure.core/in-ns '%s)")
(macroexpand . "(clojure.core/macroexpand '%s)")
(macroexpand-1 . "(clojure.core/macroexpand-1 '%s)")))
(node-babashka . ((load . "(clojure.core/load-file \"%s\")")
(doc . "(clojure.repl/doc %s)")
(source . "(clojure.repl/source %s)")
(arglists .
"(try (-> '%s clojure.core/resolve clojure.core/meta :arglists)
(catch Throwable e nil))")
(apropos . "(doseq [var (sort (clojure.repl/apropos \"%s\"))] (println (str var)))")
(ns-vars . "(clojure.repl/dir %s)")
(set-ns . "(clojure.core/in-ns '%s)")
(macroexpand . "(clojure.core/macroexpand '%s)")
(macroexpand-1 . "(clojure.core/macroexpand-1 '%s)")))
(clojure . ((load . "(clojure.core/load-file \"%s\")")
(doc . "(clojure.repl/doc %s)")
(source . "(clojure.repl/source %s)")
Expand Down Expand Up @@ -641,33 +652,34 @@ Customization: Entry to this mode runs the hooks on `comint-mode-hook' and

You can send text to the inferior Clojure process from other buffers containing
Clojure source.
`inf-clojure-switch-to-repl' switches the current buffer to the Clojure process buffer.
`inf-clojure-switch-to-repl' switches the current buffer to the Clojure
process buffer.
`inf-clojure-eval-defun' sends the current defun to the Clojure process.
`inf-clojure-eval-region' sends the current region to the Clojure process.

Prefixing the inf-clojure-eval/defun/region commands with
a \\[universal-argument] causes a switch to the Clojure process buffer after sending
the text.
a \\[universal-argument] causes a switch to the Clojure process buffer after
sending the text.

Commands:\\<inf-clojure-mode-map>
\\[comint-send-input] after the end of the process' output sends the text from the
end of process to point.
\\[comint-send-input] before the end of the process' output copies the sexp ending at point
to the end of the process' output, and sends it.
\\[comint-copy-old-input] copies the sexp ending at point to the end of the process' output,
allowing you to edit it before sending it.
If `comint-use-prompt-regexp' is nil (the default), \\[comint-insert-input] on old input
copies the entire old input to the end of the process' output, allowing
you to edit it before sending it. When not used on old input, or if
`comint-use-prompt-regexp' is non-nil, \\[comint-insert-input] behaves according to
its global binding.
\\[comint-send-input] after the end of the process' output sends the text from
the end of process to point.
\\[comint-send-input] before the end of the process' output copies the sexp
ending at point to the end of the process' output, and sends it.
\\[comint-copy-old-input] copies the sexp ending at point to the end of the
process' output,allowing you to edit it before sending it.
If `comint-use-prompt-regexp' is nil (the default), \\[comint-insert-input] on
old input copies the entire old input to the end of the process' output,
allowing you to edit it before sending it. When not used on old input, or if
`comint-use-prompt-regexp' is non-nil, \\[comint-insert-input] behaves
according to its global binding.
\\[backward-delete-char-untabify] converts tabs to spaces as it moves back.
\\[clojure-indent-line] indents for Clojure; with argument, shifts rest
of expression rigidly with the current line.
\\[indent-sexp] does \\[clojure-indent-line] on each line starting within following expression.
Paragraphs are separated only by blank lines. Semicolons start comments.
If you accidentally suspend your process, use \\[comint-continue-subjob]
to continue it."
\\[indent-sexp] does \\[clojure-indent-line] on each line starting within
following expression. Paragraphs are separated only by blank lines.
Semicolons start comments. If you accidentally suspend your process,
use \\[comint-continue-subjob] to continue it."
(setq comint-input-sender 'inf-clojure--send-string)
(setq comint-prompt-regexp inf-clojure-comint-prompt-regexp)
(setq mode-line-process '(":%s"))
Expand Down Expand Up @@ -807,9 +819,11 @@ process buffer for a list of commands.)"
nil
'confirm-after-completion))))
(let* ((project-dir (clojure-project-dir))
(process-buffer-name (if project-dir
(format "inf-clojure %s" (inf-clojure--project-name project-dir))
"inf-clojure"))
(process-buffer-name (or
inf-clojure-custom-repl-name
(if project-dir
(format "inf-clojure %s" (inf-clojure--project-name project-dir))
"inf-clojure")))
;; comint adds the asterisks to both sides
(repl-buffer-name (format "*%s*" process-buffer-name)))
;; Create a new comint buffer if needed
Expand All @@ -819,10 +833,11 @@ process buffer for a list of commands.)"
(cmdlist (if (consp cmd)
(list cmd)
(split-string-and-unquote cmd)))
(repl-type (or (unless prefix-arg
(repl-type (or inf-clojure-socket-repl-type
(unless prefix-arg
inf-clojure-custom-repl-type)
(car (rassoc cmd inf-clojure-startup-forms))
(inf-clojure--prompt-repl-type))))
(car (rassoc cmd inf-clojure-startup-forms))
(inf-clojure--prompt-repl-type))))
(message "Starting Clojure REPL via `%s'..." cmd)
(with-current-buffer (apply #'make-comint
process-buffer-name (car cmdlist) nil (cdr cmdlist))
Expand All @@ -843,6 +858,117 @@ HOST is the host the process is running on, PORT is where it's listening."
(interactive "shost: \nnport: ")
(inf-clojure (cons host port)))

(defvar-local inf-clojure-socket-callback nil
"Used to transfer state between the socket process buffer & REPL buffer.")

(defvar-local inf-clojure-socket-buffer nil
"Used to kill the associated socket buffer when it's REPL buffer is killed.")

(defun inf-clojure-socket-filter (process output)
"A filter that gets triggered each time the socket receives new OUTPUT.
This function prints out the output received but also
watches for a prompt using the `inf-clojure-prompt' regexp, once
this happens a callback is triggered if available. The callback
is intended to be used to trigger a `inf-clojure-connect' once we
can determine that a socket REPL is ready to receive a
connection.

PROCESS is the process object that is being filtered.

OUTPUT is the latest data received from the process"
(let ((server-buffer (process-buffer process)))
(when (buffer-live-p server-buffer)
(with-current-buffer server-buffer
(insert output)))
(let ((prompt-displayed (string-match inf-clojure-prompt output)))
(when prompt-displayed
(message (format "Socket REPL startup detected for %s" (process-name process)))
(with-current-buffer server-buffer
(when inf-clojure-socket-callback
(funcall inf-clojure-socket-callback)))))))

(defun inf-clojure-socket-repl-sentinel (process event)
"Ensures socket REPL are cleaned up when the REPL buffer is closed.

PROCESS is the process object that is connected to a socket REPL.

EVENT is the event that triggered this function to be called."
(when (not (process-live-p process))
(let ((repl-buffer (process-buffer process)))
(with-current-buffer repl-buffer
(when inf-clojure-socket-buffer
(kill-buffer inf-clojure-socket-buffer))))))

(defvar inf-clojure-socket-repl-startup-forms
'((lein . "JVM_OPTS='-Dclojure.server.repl={:port %d :accept clojure.core.server/repl}' lein repl")
(boot . "export BOOT_JVM_OPTIONS='-Dclojure.server.repl=\"{:port %d :accept clojure.core.server/repl}\"' boot repl")
(clojure . "clojure -J-Dclojure.server.repl=\"{:port %d :accept clojure.core.server/repl}\"")
(cljs . "clojure -J-Dclojure.server.repl=\"{:port %d :accept cljs.server.browser/repl}\"")
(lein-clr . "JVM_OPTS='-Dclojure.server.repl={:port %d :accept clojure.core.server/repl}' lein clr repl")
(planck . "planck -n %d")
(babashka . "bb socket-repl %d")))

(defcustom inf-clojure-socket-repl-port
nil
"Port to be used when creating a socket REPL via `inf-clojure-socket-repl'.
If left as nil a random port will be selected between 5500-6000."
:type '(choice integer (const nil))
:package-version '(inf-clojure . "3.3"))

;;;###autoload
(defun inf-clojure-socket-repl (cmd)
"Start a socket REPL server and connect to it via `inf-clojure'.
CMD is the command line used to start the socket REPL, if this
isn't provided you will be prompted to select from the defaults
provided in `inf-clojure-socket-repl-startup-forms' or
`inf-clojure-custom-startup' if this is defined."
(interactive (list (or (unless current-prefix-arg
inf-clojure-custom-startup)
(completing-read "Select Clojure socket REPL startup command: "
(mapcar #'cdr inf-clojure-socket-repl-startup-forms)
nil
'confirm-after-completion))))
(let* ((host "localhost")
(port (or inf-clojure-socket-repl-port (+ 5500 (random 500))))
(project-dir (clojure-project-dir))
(repl-type (or (unless prefix-arg
inf-clojure-custom-repl-type)
(car (rassoc cmd inf-clojure-socket-repl-startup-forms))
(inf-clojure--prompt-repl-type)))
(project-name (inf-clojure--project-name (or project-dir "standalone")))
(socket-process-name (format "*%s-%s-socket-server*" project-name repl-type))
(socket-buffer-name (format "*%s-%s-socket*" project-name repl-type))
(socket-buffer (get-buffer-create socket-buffer-name))
(repl-buffer-name (format "%s-%s-repl" project-name repl-type))
(socket-form (or cmd
(cdr (assoc repl-type inf-clojure-socket-repl-startup-forms))
inf-clojure-custom-startup))
(socket-cmd (format socket-form port))
(sock (let ((default-directory (or project-dir default-directory)))
(start-file-process-shell-command
socket-process-name socket-buffer
socket-cmd))))
(with-current-buffer socket-buffer
(setq-local
inf-clojure-socket-callback
(lambda ()
(let ((with-process-repl-buffer-name (concat "*" repl-buffer-name "*")))
(setq inf-clojure-socket-repl-type
repl-type
inf-clojure-custom-repl-name
repl-buffer-name
repl-buffer
(get-buffer-create with-process-repl-buffer-name))
(inf-clojure-connect host port)
(with-current-buffer with-process-repl-buffer-name
(setq inf-clojure-socket-buffer socket-buffer))
(set-process-sentinel
(get-buffer-process (get-buffer with-process-repl-buffer-name))
#'inf-clojure-socket-repl-sentinel)))))
(set-process-filter sock #'inf-clojure-socket-filter)
(message "Starting %s socket REPL server at %s:%d with %s" repl-type host port socket-cmd)))


(defun inf-clojure--forms-without-newlines (str)
"Remove newlines between toplevel forms.
STR is a string of contents to be evaluated. When sending
Expand Down