Skip to content
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

Add REPL buffer to kakoune #8

Open
adrusi opened this issue Apr 30, 2021 · 5 comments
Open

Add REPL buffer to kakoune #8

adrusi opened this issue Apr 30, 2021 · 5 comments

Comments

@adrusi
Copy link

adrusi commented Apr 30, 2021

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.

@eraserhd
Copy link
Owner

eraserhd commented May 3, 2021

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.

@adrusi
Copy link
Author

adrusi commented May 4, 2021

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>'

@eraserhd
Copy link
Owner

eraserhd commented May 5, 2021

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
Copy link
Owner

eraserhd commented May 5, 2021

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.

@adrusi
Copy link
Author

adrusi commented May 5, 2021 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants