sanel / monroe

Clojure nREPL client for Emacs
161 stars 21 forks source link

Question: Is there a better way to do this? #46

Closed lukaszkorecki closed 12 months ago

lukaszkorecki commented 1 year ago

A bit of background - I'm trying out Portal - it's a web-based Clojure data viewer/visualizer. Think clojure.inspector but modern. I want to integrate it with my Monroe-based workflow. Portal comes with a basic Emacs integration but requires running Emacs server and I think it shouldn't be required IMHO.

I wrote this Elisp function to start a portal session and open xwidgets-webkit buffer with the Portal URL:

(defun lk/monroe-portal ()
  (interactive)
  (let* ((project-root (monroe-get-directory))
         (default-directory project-root)
         (portal-url-file (format "%s.portal-url" project-root)))
     ;; start Portal session and write the session URL to a file
    (monroe-input-sender
     (get-buffer-process (monroe-repl-buffer))
     (format
      "(require 'portal.api)
         (spit \"%s\"
           (portal.api/url
               (portal.api/open {:window-title \"monroe portal\" :launcher false})))"
      portal-url-file))
    (sleep-for 1) ;; uh... this is a hack, we have to wait for the file to be there 
    (let ((url
           (with-temp-buffer
             (insert-file-contents portal-url-file)
             (buffer-string))))
      (xwidget-webkit-browse-url url))))

but I'd like to avoid writing the URL to a file and needing to invoke sleep - since I can use eval to get the result. I tried to do it this way using Monroe's internal functions:


(defun lk/monroe-eval-code-and-callback-with-value (code-str on-value)
  (message ">>EV %s" (type-of on-value))
  (monroe-send-eval-string
   code-str
   (lambda (response)
     (monroe-dbind-response response
                            (id info value status)
                            (when (member "done" status)
                              (remhash id monroe-requests))
                            (when value
                              (message ">>EV %s -> %s -> %s"
                                       value
                                       (type-of on-value)
                                       (type-of value))
                              (funcall #'on-value value))))))

(defun lk/monroe-portal-2 ()
  (interactive)
  (lk/monroe-eval-code-and-callback-with-value
   "(do 
      (require 'portal.api)
      (portal.api/url
        (portal.api/open {:window-title \"monroe portal\" :launcher false})))"
   #'(lambda (url) (xwidget-webkit-browse-url url))))

My debugging shows that everything works until the funcall part - when I print out the type of on-value callback, rather than function, Emacs reports it as cons. I assume it's something to do with monroe-dbind-response being a macro, but this is where I'm hitting a wall and limit of my Elisp understanding :-) I tried few things (quoting and not quoting) but I couldn't figure out how to make this work. Any pointers would be appreciated and I could add a function as part of a public API to make this sort of integration easier - I think it would be useful to bridge Emacs and the nREPL process together in many ways.

Many thanks!

sanel commented 1 year ago

Yes, monroe-input-sender is intended to be a one-way function to nrepl session, but monroe-send-eval-string is a low-level call that will receive nrepl session reply through callback.

Can you try to print response from this expression:

 ...
 (lambda (response)
     ;;; print here
    ; (monroe-dbind-response response

monroe-dbind-reponse will simply destructure decoded message map, like clojure let does, so I'm not sure that could be the cause

Also, make sure to call comint-output-filter if you want to display (parsed) response in monroe buffer.

lukaszkorecki commented 1 year ago

Response looks correct:

response (dict (id . 10) (ns . collie.scratch) (session . ef80b251-4aaf-4756-8ff7-f8ee438ab1da) (value . "http://localhost:61089?4ccd48ba-82c0-46ba-9a2d-1fac9b9bc866")) 
response (dict (id . 10) (session . ef80b251-4aaf-4756-8ff7-f8ee438ab1da) (status done)) 

I started digging more and learned how to catch exceptions 😉 and this is somewhat... unexpected, given this code:

(defun lk/monroe-eval-code-and-callback-with-value (code-str on-value)
  (monroe-send-eval-string
   code-str
   (lambda (response)
     (condition-case err
         (monroe-dbind-response response
                                (id info value status)
                                (when (member "done" status)
                                  (remhash id monroe-requests))
                                (when value
                                  (message "value %s" value)
                                  (funcall on-value value)))
       (error (message "error %s" err))))))

(defun lk/monroe-portal-2 ()
  (interactive)
  (lk/monroe-eval-code-and-callback-with-value
   "(r/portal-start!)"
   (lambda (url)
     (message "opening portal %s" url)
     (xwidget-webkit-browse-url url))))

The messages and error I'm getting are:

value "http://localhost:61089?ef9677c6-5386-46d0-b57a-1dcb9b408fe2"
error (void-variable on-value)

So the value is correctly extracted from the nREPL eval - but for whatever reason on-value is somehow void? I tried making the lambda into a function via defun and passing it as 'lk/open-url and that didn't work either. Mysterious. So I tried something like this, as a sanity check:

(defun lk/monroe-eval-code-and-callback-with-value-2 (code-str on-value)
  (funcall on-value code-str))

(defun lk/monroe-portal-2 ()
  (interactive)
  (lk/monroe-eval-code-and-callback-with-value-2
   "(r/portal-start!)" (lambda (in ) (message "in %s" in))))

and I'm getting expected result in *Messages* buffer.

Sorry if this is not quite Monroe related, but it's very weird :-)

This almost works,


(defun lk/open-url (url)
  (message "opening portal %s" url)
  (xwidget-webkit-browse-url url))

(defun lk/monroe-eval-code-and-callback-with-value (code-str on-value)
  (monroe-send-eval-string
   code-str
   (lambda (response)
     (condition-case err
         (monroe-dbind-response response
                                (id info value status)
                                (when (member "done" status)
                                  (remhash id monroe-requests))
                                (when value
                                  (message "value %s" value)
                                  ;; ignore callback and just open the URL using
                                  ;; the wrapper function
                                  (lk/open-url value)))
       (error (message "error %s" err))))))

but there are some issues with xwidgets opening Monroe's server url:

CleanShot 2023-10-11 at 14 49 17

event though I see this in Messages:

opening portal "http://localhost:61089?2d9bf3e1-9079-494a-abf6-e63b728483ba"

So not quite sure what's going on 🙃

sanel commented 1 year ago

Do you use lexical binding by any chance? If you don't use it, that might be the reason why on-value is void inside lambda.

To enable it, put this as the first line in your elisp file:

-*- lexical-binding: t; -*-
lukaszkorecki commented 1 year ago

Thanks! I'll try that and see what's going on 👍

lukaszkorecki commented 1 year ago

That was it! Emacs is weird sometimes, (or I'm just confused 😉) seems like when making changes I have to run M-x eval-buffer rather than just eval-region when editing my code, otherwise the lexical binding setting gets lost, at least that's how it looks like to me.

So the final code looks like this:


; - *- lexical-binding: t; -*- 

;; ^^^^ at the top of the file
;; ....

(defun lk/monroe-eval-code-and-callback-with-value (code-str on-value)
    (monroe-send-eval-string
     code-str
     (lambda (response)
       (condition-case err
           (monroe-dbind-response response
                                  (value status id)
                                  (when value
                                    (funcall on-value value))
                                  (when (member "done" status)
                                    (remhash id monroe-requests)))
         (error (message "Eval callback error %s" err))))))

No I just need to figure out why running (xwidget-webkit-browse-url value) in the callback doesn't actually navigate the browser to the URL, but I think that's a webkit issue, nothing to do with Monroe. Solved, I had to strip the double quote from the value returned in the callback, duh.

Would you be interested in adding this function as part of Monroe's public API? I can think of other ways of using it to bridge the nREPL process with Emacs. For example, run tests, get location of broken ones and open a buffer formatted for grep-mode to enable a quick jump to the failing test location. I know it's little code, so maybe it's not worth it.

Thank you for your help!

sanel commented 12 months ago

I'm happy you managed to get it working :)

Would you be interested in adding this function as part of Monroe's public API?

Monroe doesn't use lexical binding globally to support older emacses. If you can come up with a clever patch that will not use lexical binding, I'll be happy to include it :)

I'll close this ticket and if you think it should be re-opened, please do so. Cheers!

lukaszkorecki commented 12 months ago

Let me think about it, and also see what other functionality emerges while I'm iterating on my config and setup 👍