rep icon indicating copy to clipboard operation
rep copied to clipboard

Add REPL buffer to kakoune

Open adrusi opened this issue 3 years ago • 5 comments

I've extended rep to have it write repl I/O to a dedicated *rep* buffer in kakoune. This is necessary for how our clojure build system works at my job for... reasons. But it also has some advantages, like being able to interact with repl output with the full facilities of kakoune.

I'd be happy to clean this up and submit a PR if you think it's in scope for this project.

adrusi avatar Apr 30 '21 07:04 adrusi

To be honest, I have mixed feelings which I've never quite sorted out about adding a REPL buffer: Basically, as common as it is for nice lisp editors to have REPL buffers, the REPL buffer is basically a terminal, and we should already have a terminal.

Granted, this doesn't work as smoothly as I'd like.

I'm curious to see what you currently have though.

eraserhd avatar May 03 '21 13:05 eraserhd

Unfortunately after some days using it, I'm noticing some serious deficiencies in my implementation.

I'm currently creating a scratch buffer and writing the output of rep to it using execute-keys and some escaping/scrolling hacks. Works fine, but you only get output after rep actually exits, so anything that takes a long time to evaluation gives no feedback to the user.

My initial approach was using a fifo buffer, but I gave up on that after I realized that apparently I don't completely know what I'm doing with fifos.

This is what I've got though:

declare-option -hidden str rep_buffer false
declare-option -hidden str rep_selection

hook global WinSetOption ^filetype=rep$ %{
    add-highlighter window/clojure ref clojure
    set-option buffer readonly true
}

define-command -override -hidden rep-find-namespace %{
    evaluate-commands -draft %{
        set-option buffer rep_namespace ''
        # Probably will get messed up if the file starts with comments
        # containing parens.
        execute-keys 'gkm'
        evaluate-commands %sh{
            ns=$(rep --port="@.nrepl-port@${kak_buffile-.}" -- "(second '$kak_selection)" 2>/dev/null)
            if [ $? -ne 0 ]; then
                printf 'fail "could not parse namespace"\n'
            else
                printf 'set-option buffer rep_namespace %s\n' "$ns"
                rep --port="@.nrepl-port@${kak_buffile-.}" -- "(require '$kak_selection)" 1>/dev/null 2>/dev/null
            fi
        }
    }
}

define-command -hidden create-rep-buffer %{
    evaluate-commands %sh{
        if [ "$kak_opt_rep_buffer" = true ]; then
            printf '%s\n' "
                evaluate-commands -try-client '$kak_opt_toolsclient' %{
                    buffer *rep*
                }
            "
        else
            printf '%s\n' "
                set-option global rep_buffer true
                evaluate-commands -try-client '$kak_opt_toolsclient' %{
                    edit -scratch *rep*
                    set-option buffer filetype rep
                }
            "
        fi
    }
}

define-command \
    -params 0.. \
    -docstring %{rep-evaluate-selection: Evaluate selected code in REPL and write the result to *rep*.
Switches:
  -namespace <ns>   Evaluate in <ns>. Default is the current file's ns or user if not found.} \
    rep-evaluate-selection-fifo %{
    evaluate-commands %{
        set-option global rep_evaluate_output ''
        set-option global rep_selection %val{selection}
        try %{ rep-find-namespace }
        evaluate-commands -itersel -draft %{
            evaluate-commands %sh{
                add_port() {
                    if [ -n "$kak_buffile" ]; then
                        rep_command="$rep_command --port=\"@.nrepl-port@$kak_buffile\""
                    fi
                }
                add_file_line_and_column() {
                    anchor="${kak_selection_desc%,*}"
                    anchor_line="${anchor%.*}"
                    anchor_column="${anchor#*.}"
                    cursor="${kak_selection_desc#*,}"
                    cursor_line="${cursor%.*}"
                    cursor_column="${cursor#*.}"
                    if [ $anchor_line -lt $cursor_line ]; then
                        start="$anchor_line:$anchor_column"
                    elif [ $anchor_line -eq $cursor_line ] && [ $anchor_column -lt $cursor_column ]; then
                        start="$anchor_line:$anchor_column"
                    else
                        start="$cursor_line:$cursor_column"
                    fi
                    rep_command="$rep_command --line=\"$kak_buffile:$start\""
                }
                add_namespace() {
                    ns="$kak_opt_rep_namespace"
                    while [ $# -gt 0 ]; do
                        case "$1" in
                            -namespace) shift; ns="$1";;
                        esac
                        shift
                    done
                    if [ -n "$ns" ]; then
                        rep_command="$rep_command --namespace=$ns"
                    fi
                }
                error_file=$(mktemp)
                rep_command='value=$(rep'
                add_port
                add_file_line_and_column
                add_namespace "$@"
                pprint="(set! nrepl.middleware.print/*print-fn* clojure.pprint/pprint)"
                rep_command="$rep_command"' -- "$pprint $kak_selection" 2>"$error_file" |sed -e "s/'"'"'/'"''"'/g")'
                printf '%s\n' "echo -debug %{[rep] $rep_command}"
                eval "$rep_command"
                error=$(sed "s/'/''/g" <"$error_file")
                rm -f "$error_file"
                printf "set-option -add global rep_evaluate_output '%s'\n" "$value"
                [ -n "$error" ] && printf "set-option -add global rep_evaluate_output '\n%s'\n" "$error"
            }
        }
        create-rep-buffer
        evaluate-commands -draft -no-hooks -buffer *rep* %sh{
            in="$(printf '%s\n' "$kak_opt_rep_selection" |
                sed 's/</<lt>/g' |
                sed 's/{/⸨/g' |
                sed 's/}/⸩/g'
            )"
            out="$(printf '%s\n' "$kak_opt_rep_evaluate_output" |
                tail -n+2
                sed 's/</<lt>/g' |
                sed 's/{/⸨/g' |
                sed 's/}/⸩/g'
            )"
            printf 'echo -debug %%{[rep] in=%s}\n' "$in" | kak -p "$kak_session"
            printf 'echo -debug %%{[rep] out=%s}\n' "$out" | kak -p "$kak_session"
            printf '%s\n' "
                set-option buffer readonly false
                execute-keys -draft %{gj"\\"o$in<ret>$out<ret><esc>}
                try %{ execute-keys -draft '%s⸨<ret>c{<esc>%s⸩<ret>c}<esc>' }
                set-option buffer readonly true
            "
        }
        try %{ execute-keys -client %opt{toolsclient} gj }
    }   
}

map -docstring 'evaluate the selection in the FIFO REPL' global rep e ': rep-evaluate-selection-fifo<ret>'
map -docstring 'evaluate the selection in the echo REPL' global rep E ': rep-evaluate-selection<ret>'

adrusi avatar May 04 '21 20:05 adrusi

You remind me that when I was first thinking about connecting Kakoune to a REPL, I was thinking of some daemonized, persistent process (to support a buffer). The rep command being a one-shot was partly a change of thinking, and that makes the REPL buffer thing harder.

However, to get incremental output, you could do something like (following is completely untested):

( rep --print="out,1,printf 'rep-append-text %%∑%n%{out}%n∑' |kak -p $kak_session%n" \
  --print="val,1,printf 'rep-append-text %%∑%n%{val}%n∑' |kak -p $kak_session%n" \
  --print="err,1,printf ' ... ' | $SHELL ) </dev/null >/dev/null 2>&1 &

The idea is that each incremental response packet is translated into shell for asynchronously updating the buffer. Shell instead of just making Kakoune commands is because I'm pretty sure that kak -p collects all its input before processing any command, and it would still wait.

EDIT: That clearly has a number of shell quoting issues, if double quotes or single quotes appear in the output. I would be up for adding modifiers to the print formats to shell-quote the result.

eraserhd avatar May 05 '21 16:05 eraserhd

Actually, maybe a --shell-interpret option that, for each reply message, converts the whole packet into correctly quoted shell variables and runs a user-supplied shell function, e.g. "out='foo' err='bar' rep_reply_received". This seems like an obvious way I should have implemented formats in the first place.

eraserhd avatar May 05 '21 16:05 eraserhd

My initial approach to getting incremental output working was to create a temp directory with a normal file (call it rep/in) and a fifo (rep/out) and spawn a background process tail -f rep/in > rep/out that lives for as long as the repl buffer (which is a fifo buffer reading from rep/out) exists.

Then each invocation of rep would append it's output to rep/in.

However this approach did not work, and I'm not sure why. If it's possible to get working then it is rather convenient, since it doesn't involve any complicated escaping hacks to communicate with kakoune over execute-keys.

On Wed, May 5, 2021, 12:17 Jason Felice @.***> wrote:

Actually, maybe a --shell-interpret option that, for each reply message, converts the whole packet into correctly quoted shell variables and runs a user-supplied shell function, e.g. "out='foo' err='bar' rep_reply_received". This seems like an obvious way I should have implemented formats in the first place.

— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/eraserhd/rep/issues/8#issuecomment-832825186, or unsubscribe https://github.com/notifications/unsubscribe-auth/AABMYDJL57CCBODIGN7FSULTMFVSDANCNFSM433TM4YQ .

adrusi avatar May 05 '21 20:05 adrusi