From af0ddc4005b25a0d752bf0eadf90c88b7b4b27d8 Mon Sep 17 00:00:00 2001 From: Will Handley Date: Sat, 27 Jul 2024 09:43:38 +0100 Subject: [PATCH] LLM REPL with shell_gpt (#37) * Added first draft working sgpt * Updated documentation * Update README.rst * Added cycling commands * Big improvements to logging * Removed uid comment --- README.rst | 142 ++++++++++++++++++++------------------ autoload/vimteractive.vim | 84 ++++++++++++++++++++-- doc/vimteractive.txt | 96 ++++++++++++++++---------- plugin/vimteractive.vim | 25 +++++-- 4 files changed, 232 insertions(+), 115 deletions(-) diff --git a/README.rst b/README.rst index 3f4c9bb..23423a3 100644 --- a/README.rst +++ b/README.rst @@ -3,7 +3,7 @@ Vimteractive ============ :vimteractive: send commands from text files to interactive programs via vim :Author: Will Handley -:Version: 2.5.0 +:Version: 2.6.0 :Homepage: https://github.com/williamjameshandley/vimteractive :Documentation: ``:help vimteractive`` @@ -11,31 +11,36 @@ Vimteractive was inspired by the workflow of the `vim-ipython `__ plugin. This plugin is designed to extend a subset of the functionality of vim-ipython -to other interpreters (including ipython). It is based around the unix +to other `REPLs `__ (including ipython). It is based around the unix philosophy of `"do one thing and do it well" `__. Vimteractive aims to provide a robust and simple link between text files and -interactive interpreters. Vimteractive will never aim to do things like +language shells. Vimteractive will never aim to do things like autocompletion, leaving that to other, more developed tools such as `YouCompleteMe `__ or -`TabNine `__. - -The activating commands are - -- ipython ``:Iipython`` -- julia ``:Ijulia`` -- maple ``:Imaple`` -- mathematica ``:Imathematica`` -- bash ``:Ibash`` -- zsh ``:Izsh`` -- python ``:Ipython`` -- clojure ``:Iclojure`` -- apl ``:Iapl`` +`Copilot `__. + +The activating commands are: + +- `ipython `__ ``:Iipython`` +- `julia `__ ``:Ijulia`` +- `maple `__ ``:Imaple`` +- `mathematica `__ ``:Imathematica`` +- `bash `__ ``:Ibash`` +- `zsh `__ ``:Izsh`` +- `python `__ ``:Ipython`` +- `clojure `__ ``:Iclojure`` +- `apl `__ ``:Iapl`` +- `R `__ ``:IR`` +- `sgpt `__ ``:Isgpt`` - autodetect based on filetype ``:Iterm`` -Commands may be sent from a text file to the chosen terminal using ``CTRL-S``. -If there is no terminal, ``CTRL-S`` will automatically open one for you using +Commands may be sent from a text file to the chosen REPL using ``CTRL-S``. +If there is no REPL, ``CTRL-S`` will automatically open one for you using ``:Iterm``. +For some terminals, the output of the last command may be retrieved with +``CTRL-Y``. + Note: it's highly recommended to use IPython as your default Python interpreter. You can set it like this: @@ -49,16 +54,6 @@ Installation Since this package leverages the native vim interactive terminal, vimteractive is only compatible with vim 8 or greater. -To use the key-bindings, you should first disable the ``CTRL-S`` -default, which is a terminal command to freeze the output. You can -disable this by putting - -.. code:: bash - - stty -ixon - -into your ``.bashrc`` (or equivalent shell profile file). - Installation should be relatively painless via `the usual routes `_ such as `Vundle `__, @@ -95,6 +90,17 @@ interface to the command line `maple `__. Usage ----- +To use the key-bindings, you should first disable the ``CTRL-S`` +default, which is a terminal command to freeze the output. You can +disable this by putting + +.. code:: bash + + stty -ixon + +into your ``.bashrc`` (or equivalent shell profile file). + + Example usage: ~~~~~~~~~~~~~~ @@ -126,51 +132,68 @@ pressing ``CTRL-S``. If you switch windows with ``CTRL-W+k``, you will see the terminal buffer switch to a more usual looking normal-mode buffer, from which you can perform traditional normal mode commands. However, if you try to insert, you will enter -the terminal, and be able to enter commands interactively into the prompt as if +the REPL, and be able to enter commands interactively into the prompt as if you had run it in the command line. You can save this buffer if you wish to a new file if it contains valuable output -You may want to send lines to one terminal from two buffers. To achieve that, +You may want to send lines to one REPL from two buffers. To achieve that, run ``:Iconn `` where ```` is a name of buffer -containing terminal. If there is only one terminal, you can use just +containing REPL. If there is only one REPL, you can use just ``:Iconn``. -Supported terminals -~~~~~~~~~~~~~~~~~~~ +Supported REPLs +~~~~~~~~~~~~~~~ -- ``:Iipython`` Activate an ipython terminal -- ``:Ijulia`` Activate a julia terminal -- ``:Imaple`` Activate a maple terminal -- ``:Imathematica`` Activate a mathematica terminal -- ``:Ibash`` Activate a bash terminal -- ``:Izsh`` Activate a zsh terminal -- ``:Ipython`` Activate a python terminal -- ``:Iclojure`` Activate a clojure terminal -- ``:Iapl`` Activate an apl terminal -- ``:Iterm`` Activate default terminal for this filetype +- ``:Iipython`` Activate an ipython REPL +- ``:Ijulia`` Activate a julia REPL +- ``:Imaple`` Activate a maple REPL +- ``:Imathematica`` Activate a mathematica REPL +- ``:Ibash`` Activate a bash REPL +- ``:Izsh`` Activate a zsh REPL +- ``:Ipython`` Activate a python REPL +- ``:Iclojure`` Activate a clojure REPL +- ``:Iapl`` Activate an apl REPL +- ``:IR`` Activate an R REPL +- ``:Isgpt`` Activate an sgpt REPL +- ``:Iterm`` Activate default REPL for this filetype Sending commands ~~~~~~~~~~~~~~~~ ``CTRL-S`` sends lines of text to the interpreter in a mode-dependent manner: -In Normal mode, ``CTRL-S`` sends the line currently occupied by the cursor the -terminal. +In Normal mode, ``CTRL-S`` sends the line currently occupied by the cursor to the +REPL. In Insert mode, ``CTRL-S`` sends the line currently being edited, and then returns to insert mode at the same location. -In Visual mode, ``CTRL-S`` sends the current selection to the terminal. +In Visual mode, ``CTRL-S`` sends the current selection to the REPL. ``ALT-S`` sends all lines from the start to the current line. -Connecting to an existing terminal -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Retrieving command outputs +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +CTRL-Y retrieves the output of the last command sent to the REPL. This only +implemented in a subset of terminas (``:Iipython`` and ``:Isgpt``) + +In ``Normal-mode``, CTRL-Y retrieves the output of the last command sent to the +REPL and places it in the current buffer. + +In ``Insert-mode``, CTRL-Y retrieves the output of the last command sent to the +REPL and places it in the current buffer, and then returns to insert mode +after the output. + +Connecting to an existing REPL +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``:Iconn [{buffer]`` connects current buffer to REPL in ``{buffer}``. You can connect any number of buffers to one REPL. ``{buffer}`` can be omitted if there -is only one terminal. +is only one REPL. +``]v`` and ``[v`` can be used to cycle between connected buffers in the style of +`unimpaired `__. Common issues ------------- @@ -194,8 +217,8 @@ These options can be put in your ``.vimrc``, or run manually as desired: .. code:: vim - let g:vimteractive_vertical = 1 " Vertically split terminals - let g:vimteractive_autostart = 0 " Don't start terminals by default + let g:vimteractive_vertical = 1 " Vertically split REPLs + let g:vimteractive_autostart = 0 " Don't start REPLs by default Extending functionality ----------------------- @@ -252,20 +275,3 @@ Similar projects - `vipy `__ .. |example_usage| image:: https://raw.githubusercontent.com/williamjameshandley/vimteractive/master/images/example_usage.gif - -Changelist ----------- -:v2.2: `Vertical splitting option `__ -:v2.1: `Visual selection improvement `__ -:v2.0: `Multiple terminal functionality `__ -:v1.7: `Autodetection of terminals `__ -:v1.6: CtrlP `bugfix `__ -:v1.5: Added julia support -:v1.4: `Buffer rename `_ -:v1.3: Added zsh support -:v1.2: - - no line numbers in terminal window -:v1.1: - - `Bracketed paste `__ seems - to fix most of ipython issues. - - ``ALT-S`` sends all lines from start to current line. diff --git a/autoload/vimteractive.vim b/autoload/vimteractive.vim index 9b58214..53c752c 100644 --- a/autoload/vimteractive.vim +++ b/autoload/vimteractive.vim @@ -4,6 +4,9 @@ " s:vimteractive_buffers " script-local variable that keeps track of vimteractive terminal buffers " +" s:vimteractive_logfiles +" script-local variable that keeps track of logfiles for each terminal +" " b:vimteractive_connected_term " buffer-local variable held by buffer that indicates the name of the " connected terminal buffer @@ -11,12 +14,16 @@ " b:vimteractive_term_type " buffer-local variable held by terminal buffer that indicates the terminal type - " Initialise the list of terminal buffer numbers on startup if !exists('s:vimteractive_buffers') let s:vimteractive_buffers = [] end +" Initialise the list of logfiles on startup +if !exists('s:vimteractive_logfiles') + let s:vimteractive_logfiles = {} +end + " Remove a terminal from the list on deletion. function! s:del_term() let l:term_bufname = expand('') @@ -103,9 +110,8 @@ function! vimteractive#sendlines(lines) endif endfunction - " Start a vimteractive terminal -function! vimteractive#term_start(term_type) +function! vimteractive#term_start(term_type, ...) if has('terminal') == 0 echoerr "Your version of vim is not compiled with +terminal. Cannot use vimteractive" return @@ -118,6 +124,9 @@ function! vimteractive#term_start(term_type) let l:term_type = a:term_type endif + " Name the buffer + let l:term_bufname = s:new_name(l:term_type) + " Retrieve starting command if has_key(g:vimteractive_commands, l:term_type) let l:term_command = get(g:vimteractive_commands, l:term_type) @@ -126,9 +135,18 @@ function! vimteractive#term_start(term_type) return endif + " Assign a logfile name + let l:logfile = tempname() . '-' . l:term_type . '.log' + let l:term_command = substitute(l:term_command, '', l:logfile, '') + + " Pass any environment variables necessary for logging + let $CHAT_CACHE_PATH="/" " sgpt logfiles + + " Add all other arguments to the command + let l:term_command = l:term_command . ' ' . join(a:000, ' ') + " Create a new term echom "Starting " . l:term_command - let l:term_bufname = s:new_name(l:term_type) if v:version < 801 call term_start(l:term_command, { \ "term_name": l:term_bufname, @@ -147,6 +165,7 @@ function! vimteractive#term_start(term_type) " Add this terminal to the buffer list, and store type call add(s:vimteractive_buffers, bufnr(l:term_bufname)) let b:vimteractive_term_type = l:term_type + let s:vimteractive_logfiles[bufnr(l:term_bufname)] = l:logfile " Turn line numbering off set nonumber norelativenumber @@ -213,3 +232,60 @@ function! vimteractive#connect(...) echom "Connected " . bufname("%") . " to " . l:bufname endfunction + +function! vimteractive#get_response() + let l:term_type = getbufvar(b:vimteractive_connected_term, "vimteractive_term_type") + return g:vimteractive_get_response[l:term_type]() +endfunction + +" Get the last response from the terminal for sgpt +function! vimteractive#get_response_sgpt() + let l:logfile = s:vimteractive_logfiles[b:vimteractive_connected_term] + let l:json_content = join(readfile(l:logfile), "\n") + let l:json_data = json_decode(l:json_content) + if len(l:json_data) > 0 + let l:last_response = l:json_data[-1]['content'] + return l:last_response + endif +endfunction + +" Get the last response from the terminal for ipython +function! vimteractive#get_response_ipython() + let l:logfile = s:vimteractive_logfiles[b:vimteractive_connected_term] + let lines = readfile(l:logfile) + let block = [] + for i in range(len(lines) - 1, 0, -1) + if match(lines[i], '^#\[Out\]#') == 0 + let line = substitute(lines[i], '^#\[Out\]# ', '', '') + call add(block, line) + else + break + endif + endfor + let block = reverse(block) + return join(block, "\n") +endfunction + +" Cycle connection forward through terminal buffers +function! vimteractive#next_term() + let l:current_buffer = b:vimteractive_connected_term + let l:current_index = index(s:vimteractive_buffers, l:current_buffer) + if l:current_index == -1 + echom "Not in a terminal buffer" + return + endif + let l:next_index = (l:current_index + 1) % len(s:vimteractive_buffers) + call vimteractive#connect(vimteractive#buffer_list()[l:next_index]) +endfunction + +" Cycle connection backward through terminal buffers +function! vimteractive#prev_term() + let l:current_buffer = b:vimteractive_connected_term + let l:current_index = index(s:vimteractive_buffers, l:current_buffer) + if l:current_index == -1 + echom "Not in a terminal buffer" + return + endif + let l:prev_index = (l:current_index - 1 + len(s:vimteractive_buffers)) % len(s:vimteractive_buffers) + call vimteractive#connect(vimteractive#buffer_list()[l:prev_index]) +endfunction diff --git a/doc/vimteractive.txt b/doc/vimteractive.txt index 55f1174..424fc9d 100644 --- a/doc/vimteractive.txt +++ b/doc/vimteractive.txt @@ -21,11 +21,11 @@ Vimteractive was inspired by the workflow of the vim-ipython plugin: https://github.com/ivanov/vim-ipython This plugin is designed to extend a subset of the functionality of vim-ipython -to other interpreters (including ipython). It is based around the unix -philosophy of "do one thing and do it well". It aims to provide a robust and -simple link between text files and interactive interpreters. Vimteractive will -never aim to do things like autocompletion, leaving that to other, more -developed tools such as YouCompleteMe or TabNine. +to other REPLs (including ipython). It is based around the unix philosophy of +"do one thing and do it well". It aims to provide a robust and simple link +between text files and language shells. Vimteractive will never aim to do +things like autocompletion, leaving that to other, more developed tools such as +YouCompleteMe or GitHub copilot. The activating commands are - ipython |:Iipython| @@ -37,12 +37,18 @@ The activating commands are - python |:Ipython| - clojure |:Iclojure| - apl |:Iclojure| +- R |:IR| +- mathematica |:Imathematica| +- sgpt |:Isgpt| - autodetect based on filetype |:Iterm| -Commands may be sent from a text file to the chosen terminal using CTRL-S. If -there is no terminal, CTRL-S will automatically open one for you using +Commands may be sent from a text file to the chosen REPL using CTRL-S. If +there is no REPL, CTRL-S will automatically open one for you using |:Iterm|. See |v_CTRL_S| for more details. +For some terminals, the output of the last command may be retrieved with +``CTRL-Y``. See |v_CTRL_Y| for more details. + Note: it's highly recommended to use IPython as your default Python interpreter. You can set it like this: @@ -76,63 +82,81 @@ Create a python file "test.py" with the following content: ax.set_xlabel('$x$') ax.set_ylabel('$y$') -Now start an ipython interpreter in vim with :Iipython. You should see a -preview window open above with your ipython prompt. Position your cursor over -the first line of test.py, and press CTRL-S. You should see this -line now appear in the first prompt of the preview window. Do the same with -the second and fourth lines. At the fourth line, you should see a figure -appear once it's constructed with plt.subplots(). Continue by sending lines to -the interpreter. You can send multiple lines by doing a visual selection and -pressing CTRL-S. +Now start an ipython REPL in vim with |:Iipython|. You should see a preview +window open above with your ipython prompt. Position your cursor over the first +line of test.py, and press |v_CTRL-S|. You should see this line now appear in the +first prompt of the preview window. Do the same with the second and fourth +lines. At the fourth line, you should see a figure appear once it's constructed +with plt.subplots(). Continue by sending lines to the REPL. You can send +multiple lines by doing a visual selection and pressing |v_CTRL-S|. If you switch windows with CTRL-W k, you will see the terminal buffer switch to a more usual looking normal-mode buffer, from which you can perform traditional normal mode commands. However, if you try to insert, you will -enter the terminal, and be able to enter commands interactively into the +enter the REPL, and be able to enter commands interactively into the prompt as if you had run it in the command line. You can save this buffer if you wish to a new file if it contains valuable output -By default every buffer is connected to separate terminal. If you want to -connect two buffers to one terminal, use |:Iconn| command. +You may want to send lines to one REPL from two buffers. To achieve that, run +:Iconn where is a name of buffer containing REPL. +If there is only one REPL, you can use just |:Iconn|. ------------------------------------------------------------------------------ Supported terminals *vimteractive-terminals* -*:Iipython* Activate an ipython terminal -*:Ijulia* Activate a julia terminal -*:Imaple* Activate a maple terminal -*:Imathematica* Activate a mathematica terminal -*:Ibash* Activate a bash terminal -*:Izsh* Activate a zsh terminal -*:Ipython* Activate a python terminal -*:Iclojure* Activate a clojure terminal -*:Iapl* Activate an apl terminal -*:Iterm* Activate a terminal based on current filetype +*:Iipython* Activate an ipython REPL +*:Ijulia* Activate a julia REPL +*:Imaple* Activate a maple REPL +*:Imathematica* Activate a mathematica REPL +*:Ibash* Activate a bash REPL +*:Izsh* Activate a zsh REPL +*:Ipython* Activate a python REPL +*:Iclojure* Activate a clojure REPL +*:Iapl* Activate an apl REPL +*:IR* Activate an R REPL +*:Isgpt* Activate an sgpt REPL +*:Iterm* Activate a REPL based on current filetype ------------------------------------------------------------------------------ Sending commands *v_CTRL_S* -CTRL-S sends lines of text to the interpreter in a mode-dependent manner: +CTRL-S sends lines of text to the REPL in a mode-dependent manner: In |Normal-mode|, CTRL-S sends the line currently occupied by the cursor -the terminal. +the REPL. In |Insert-mode|, CTRL-S sends the line currently being edited, and then returns to insert mode at the same location. -In |Visual-mode|, CTRL-S sends the current selection to the terminal. +In |Visual-mode|, CTRL-S sends the current selection to the REPL. ALT-S sends all lines from the start to the current line. -If there is no active terminal for current buffer, CTRL-S will automatically +If there is no active REPL for current buffer, CTRL-S will automatically create one for you using |:Iterm|. +------------------------------------------------------------------------------ +Retrieving command outputs *v_CTRL_Y* + +CTRL-Y retrieves the output of the last command sent to the REPL. This only +implemented in a subset of terminas (|:Iipython| and |:Isgpt|) + +In |Normal-mode|, CTRL-Y retrieves the output of the last command sent to the +REPL and places it in the current buffer. + +In |Insert-mode|, CTRL-Y retrieves the output of the last command sent to the +REPL and places it in the current buffer, and then returns to insert mode +after the output. + ------------------------------------------------------------------------------ Connecting to existing REPLs *:Iconn* *vimteractive-connecting* :Iconn [{buffer}] connects current buffer to REPL in {buffer}. You can connect -any number of buffers to one REPL. {buffer} can be omitted if there is only one -terminal. +any number of buffers to one REPL. {buffer} can be omitted if there is only +one REPL. + +]v and [v can be used to cycle between connected buffers in the style of +unimpaired. ============================================================================== 3. Common issues *vimteractive-issues* @@ -153,8 +177,8 @@ setting with These options can be put in your |.vimrc|, or run manually as desired: - let g:vimteractive_vertical = 1 " Vertically split terminals - let g:vimteractive_autostart = 0 " Don't start terminals by default + let g:vimteractive_vertical = 1 " Vertically split REPLs + let g:vimteractive_autostart = 0 " Don't start REPLs by default let g:vimteractive_switch_mode = 0 " Don't switch to normal mode diff --git a/plugin/vimteractive.vim b/plugin/vimteractive.vim index 56bd15b..30c7a84 100644 --- a/plugin/vimteractive.vim +++ b/plugin/vimteractive.vim @@ -29,13 +29,8 @@ if !has_key(g:, 'vimteractive_commands') let g:vimteractive_commands = { } endif - -let g:vimteractive_commands.ipython = 'ipython --matplotlib --no-autoindent' -let g:vimteractive_commands.ipython2 = 'ipython2 --matplotlib --no-autoindent' -let g:vimteractive_commands.ipython3 = 'ipython3 --matplotlib --no-autoindent' +let g:vimteractive_commands.ipython = 'ipython --matplotlib --no-autoindent --logfile="-o "' let g:vimteractive_commands.python = 'python' -let g:vimteractive_commands.python2 = 'python2' -let g:vimteractive_commands.python3 = 'python3' let g:vimteractive_commands.bash = 'bash' let g:vimteractive_commands.zsh = 'zsh' let g:vimteractive_commands.julia = 'julia' @@ -44,6 +39,7 @@ let g:vimteractive_commands.clojure = 'clojure' let g:vimteractive_commands.apl = 'apl' let g:vimteractive_commands.R = 'R' let g:vimteractive_commands.mathematica = 'math' +let g:vimteractive_commands.sgpt = 'sgpt --repl ' " Override default shells for different filetypes if !has_key(g:, 'vimteractive_default_shells') @@ -70,6 +66,11 @@ if !has_key(g:, 'vimteractive_slow_prompt') endif let g:vimteractive_slow_prompt.clojure = 200 +let g:vimteractive_get_response = { + \ 'ipython': function('vimteractive#get_response_ipython'), + \ 'sgpt': function('vimteractive#get_response_sgpt') + \} + " Plugin commands " =============== @@ -78,7 +79,7 @@ if !has_key(g:, 'vimteractive_loaded') " Building :I* commands (like :Ipython, :Iipython and so) for term_type in keys(g:vimteractive_commands) - execute 'command! I' . term_type . " :call vimteractive#term_start('" . term_type . "')" + execute 'command! -nargs=? I' . term_type . " :call vimteractive#term_start('" . term_type . "', )" endfor command! Iterm :call vimteractive#term_start('-auto-') @@ -101,3 +102,13 @@ vnoremap m`""y:call vimteractive#sendlines(substitute(getreg('"') " Alt-S in normal mode to send all lines up to this point noremap :call vimteractive#sendlines(join(getline(1,'.'), "\n")) + +" Control-Y in normal mode to get last response +noremap :put =vimteractive#get_response() + +" Control-Y in insert mode to get last response +inoremap :put =vimteractive#get_response()a + +" cycle through terminal buffers in the style of unimpaired +nnoremap ]v :call vimteractive#next_term() +nnoremap [v :call vimteractive#prev_term()