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?

NickD
  • 27,023
  • 3
  • 23
  • 42
  • I saw this but not sure it is exactly what I am looking for https://adereth.github.io/blog/2014/05/29/custom-clojure-evaluation-keybindings-in-emacs/ – Anders Kitson Jan 05 '22 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 Jan 08 '22 at 12:40

2 Answers2

2

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.

Lorem Ipsum
  • 4,327
  • 2
  • 14
  • 35
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)
BuddhiLW
  • 257
  • 1
  • 7
  • 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 May 16 '23 at 13:00