2

I have cider working just fine in Emacs with my Clojure code. I am following a tutorial that uses Cursive as the IDE; in the tutorial they were able to define a key binding to a line of code in a specific namespace.

The command I want to run in the cider REPL and the namespace user is

(go)

When I hit C-M-S-g, it should fire in the REPL (I have a key that hits C-M-S all at once on my keyboard).

This command is for starting my server and I will have others for halting and resetting, but it would be nice instead of going to the REPL and typing them in to have a key binding to do so.

So how do I define key bindings that would run specific commands in the REPL?

2
  • I saw this but not sure it is exactly what I am looking for adereth.github.io/blog/2014/05/29/… Commented Jan 5, 2022 at 16:30
  • I edited to make the question specific, but you should check to make sure that I have not distorted your question.
    – NickD
    Commented Jan 8, 2022 at 12:40

2 Answers 2

2
+50

As I understand it, you basically want to run a command in the REPL. That is, I think you're saying you want to press C-M-S-g and have it execute (go) in the REPL.

Emacs interacts with processes through a comint, or command interpreter. For example,

Shell mode is a derivative of Comint mode, a general-purpose mode for communicating with interactive subprocesses. Most of the features of Shell mode actually come from Comint mode...

https://www.gnu.org/software/emacs/manual/html_node/emacs/Shell-Mode.html

The cider REPL must be communicating with a process. Therefore, it must be a comint.

I know next to nothing about Clojure or CIDER. Rather than try to give an answer tailored to those, I will give something general which should address the problem, as I understand it, and be useful elsewhere. This is from my init and is one of my favorite hacks.

A common task for me is copying text from one buffer to another. Sometimes that buffer is just a regular buffer, other times it's a comint. Often, I don't know where I'll need to copy text to. The target must be determined "on demand".

The following defines a new variable, xc/on-demand-window. It defines the target for where text should go. Note that this is defined in terms of windows1. This makes the target location fixed.

(defvar xc/on-demand-window nil
  "Target on-demand window.

An on-demand window is one which you wish to return to within the
current Emacs session but whose importance doesn't warrant a
permanent binding.")

The next function makes defining the target window more user friendly. Call xc/on-demand-window to set the target to the currently selected window. In your case, once the REPL is up, select that window and call this.

(defun xc/on-demand-window-set ()
  "Set the value of the on-demand window to current window."
  (interactive)
  (setq xc/on-demand-window (selected-window))
  (message "Set on-demand window to: %s" xc/on-demand-window))

It's handy to jump between the source window and the target. Call xc/on-demand-window-goto to do that.

(defun xc/on-demand-window-goto ()
  "Goto the `xc/on-demand-window'."
  (interactive)
  (let ((win xc/on-demand-window))
    (unless win (error "No on-demand window set! See `xc/on-demand-window-set'."))
    (if (eq (selected-window) xc/on-demand-window)
        (error "Already in `xc/on-demand-window'"))
    (let ((frame (window-frame win)))
      (raise-frame frame)
      (select-frame frame)
      (select-window win))))

Ultimately, you want to send text to the REPL. The function xc/send-line-or-region is for interactively sending text. It's a bit of overkill but like I said, it was already in my init. It will send a line or region to the on-demand window (or current window if the on-demand window is not set). If the on-demand window's buffer has no process, the line is simply inserted. However, if the buffer has a process, as should be the case with the REPL, the line will be inserted and sent to the process. It has the added option to insert a new line for non-text buffers.

(defun xc/send-line-or-region (&optional advance buff beg end)
  "Send region or line to BUFF.

If buffer has a process, insert and send line to the process. If
no process, then simply insert text at point.  Create a new line
when ADVANCE is non-nil.  Use current region if BEG and END not
provided.  If no region provided, send entire line.  Default BUFF
is the buffer associated with `xc/on-demand-window'."
  (interactive (if (use-region-p)
                   (list nil nil (region-beginning) (region-end))
                 (list nil nil nil nil)))
  (let* ((beg (or beg (if (use-region-p) (region-beginning)) nil))
         (end (or end (if (use-region-p) (region-end)) nil))
         (substr (string-trim
                  (or (and beg end (buffer-substring-no-properties beg end))
                      (buffer-substring-no-properties (line-beginning-position) (line-end-position)))))
         (buff (or buff (window-buffer xc/on-demand-window)))
         (proc (get-buffer-process buff)))
    (if substr
        (with-selected-window (get-buffer-window buff t)
          (let ((window-point-insertion-type t))  ; advance marker on insert
            (cond (proc
                   (goto-char (process-mark proc))
                   (insert substr)
                   (comint-send-input nil t))
                  (t
                   (insert substr)
                   (if advance
                       (progn
                         (end-of-line)
                         (newline-and-indent)))))))
      (error "Invalid selection"))))

Send a string to the on-demand window with xc/send-string. This is probably more to your question. It works the same way as xc/send-line-or-region, but acts on a fixed string. When called with M-x, it will prompt for what string to send. I also added a history so that you can press up/down or M-p/M-n to see previously sent strings.

(defvar xc--send-string-history nil
  "History of strings sent via `xc/send-string'")

(defun xc/send-string (string &optional advance buff)
  "Send STRING to BUFF'.

Default BUFF is the buffer associated with
`xc/on-demand-window' (or current window if not set).  If BUFF
has an associated process, send region as input, otherwise just
insert the region.  Create a new line when ADVANCE is non-nil."
  (interactive
   (let* ((prompt (format "Send string to % s: " (window-buffer xc/on-demand-window)))
          (cmd (read-string prompt "" 'xc--on-demand-send-string-history)))
   (list cmd nil nil)))

  (let* ((buff (or buff (window-buffer xc/on-demand-window)))
         (proc (get-buffer-process buff)))
    (with-selected-window (get-buffer-window buff t)
      (let ((window-point-insertion-type t))  ; advance marker on insert
        (cond (proc
               (goto-char (process-mark proc))
               (insert string)
               (comint-send-input nil t))
              (t
               (insert string)
               (if advance
                   (progn
                     (end-of-line)
                     (newline-and-indent)))))))))

You can then do what you want with something like:

(global-set-key (kbd "C-M-S-g") '(lambda () (interactive) (xc/send-string "(go)")))

Hopefully that helps! Happy hacking!

NOTE The xc/send-line-or-region and xc/send-string commands use string-trim which doesn't seem to autoload. You may need to call (require 'subr-x) first.

EDIT The previous code handles sending to the REPL. The link you gave on the 5th gives a clue about handling namespaces.

Since namespaces are part of the language used by the comint/REPL, they must be handled within the string sent to the comint/REPL.

I've not used Clojure, but modifying the "essential pattern", the string that needs to be sent is (or is something like):

"(require 'user) (user/go (go))"

So, using xc/send-string,

(global-set-key (kbd "C-M-S-g") '(lambda () (interactive) (xc/send-string "(require 'user) (user/go (go))"))

Honestly, though, it looks like this function from the article you linked would do something comparable if the sexp before point were (go) (and the appropriate some-namespace/some-namespace-fn replacement made):

(defun custom-eval-last-sexp ()
  (interactive)
  (cider-interactive-eval
    (format "(require 'some-namespace)
             (some-namespace/some-fn %s)"
            (cider-last-sexp))))

Hopefully that's enough for you to modify things to your need.

1 Although we need to send text to a buffer, I've found that it's actually the window I want to interact with. It's somewhat arbitrary since it's possible to get the buffer from a window and vice-versa. I've done it both ways and this is what has survived in my init. Feel free, of course, to try it the other way.

0

Evaluate:

(defun custom-eval-user-go ()
  (interactive)
  (cider-eval-file "/path-to/dev/src/user.clj")
  (cider-interactive-eval
    (format "(go)"
            (cider-last-sexp))))

(define-key cider-mode-map (kbd "C-c c") 'custom-eval-user-go)

Then, start cider inside your project. Use, C-c c, and be happy.

You could also use a macro, and then define functions for different commands (Create a function that returns a new function definition):

(defmacro lw/define-user-eval (fn-name command)
  `(defun ,fn-name ()
    (interactive)
    (cider-eval-file "/path-to/dev/src/user.clj")
    (cider-interactive-eval
      (format (concat "(" ,command ")")
              (cider-last-sexp)))))

(lw/define-user-eval lw/eval-go "go")
(define-key cider-mode-map (kbd "C-c g") 'lw/eval-go)

(lw/define-user-eval lw/eval-halt "halt")
(define-key cider-mode-map (kbd "C-c h") 'lw/eval-halt)

This would be equivalent to

(defun eval-go ()
  (interactive)
  (cider-eval-file "/path-to/dev/src/user.clj")
  (cider-interactive-eval
    (format "(go)"
            (cider-last-sexp))))

(defun eval-halt ()
  (interactive)
  (cider-eval-file "/path-to/dev/src/user.clj")
  (cider-interactive-eval
    (format "(halt)"
            (cider-last-sexp))))

(define-key cider-mode-map (kbd "C-c c") 'eval-go)
(define-key cider-mode-map (kbd "C-c h") 'eval-halt)
1
  • What I did was to set a path variable, in .bashrc pointing to my project, then I specified the path to the file I wanted to execute those commands. (cider-eval-file (format (concat (getenv "CLJ_PLAYGROUND") "dev/src/user.clj"))) ;; equivalent to the "/path-to/dev/src/user.clj" So, the full code for my case simulates what Jack Schae did in the course with Calva. At least, it worked for me.
    – BuddhiLW
    Commented May 16, 2023 at 13:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.