Skip to content

Commit

Permalink
Merge pull request #1 from jam1015/pid_input
Browse files Browse the repository at this point in the history
enhanced terminal tracking/Pid input
  • Loading branch information
Klafyvel committed Jun 17, 2023
2 parents 22e5fa6 + bc6b2ef commit 8b4c9b8
Show file tree
Hide file tree
Showing 3 changed files with 251 additions and 20 deletions.
106 changes: 97 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,103 @@
# vim-slime-ext-neovim
An experiment for an external neovim plugin for vim-slime

## Example of configuration using packer
A plugin to send code from a neovim Neovim buffer to a running Neovim terminal, enhancing your development workflow. This plugin uses Neovim's built-in terminal and extends [vim-slime-ext-plugins](https://github.com/jpalardy/vim-slime-ext-plugins/).


## Example of installation and configuration using lazy.nvim

### Lua installation Configuration

```lua
use {
'Klafyvel/vim-slime-ext-neovim',
config=function ()
vim.g.slime_target_send = "slime_neovim#send"
vim.g.slime_target_config = "slime_neovim#config"
end
}
{
'Klafyvel/vim-slime-ext-neovim',
dependencies = { "jpalardy/vim-slime-ext-plugins" },
config = function()
vim.g.slime_target_send = "slime_neovim#send"
vim.g.slime_target_config = "slime_neovim#config"

-- allows use of PID rather than internal job_id for config see note below this codeblock
vim.g.slime_input_pid = 1

-- optional but useful keymaps:
---- send text using gz as operator before motion or text object
vim.keymap.set("n", "gz", "<Plug>SlimeMotionSend", { remap = true })
---- send line of text
vim.keymap.set("n", "gzz", "<Plug>SlimeLineSend", { remap = true })
---- send visual selection
vim.keymap.set("x", "gz", "<Plug>SlimeRegionSend", { remap = true })
end
}

```

#### Note on `g:slime_input_pid`

Used to send text using the external PID rather than Neovim's internal job id. Setting this to a nonzero value (evaluated as `true` in vimscript), as is done here, is recommended because the PID is the number displayed on the status line of a terminal buffer, making it easier to select the desired terminal. This recommended setting is not the default because neovim uses it's internal job id to send text to a terminal; the plugin has a function that translates the PID to the inernal job id.

##### Side Note

Recall that when configuring neovim in lua, variables in the global `g:` namespace are set with `vim.g.foo = bar`.

### vimscript configuration

```vim
let g:slime_target_send = "slime_neovim#send"
let g:slime_target_config = "slime_neovim#config"
" Use external PID instead of Neovim's internal job id
let g:slime_input_pid = 1
" Key mappings:
" Send text using gz as operator before motion or text object
nmap gz <Plug>SlimeMotionSend
" Send line of text
nmap gzz <Plug>SlimeLineSend
" Send visual selection
xmap gz <Plug>SlimeRegionSend
```



## What This Is
Say you are writing code in, for example, python. One way of quickly testing code is to have a terminal where you repeatedly source commands from the terminal. For example if your file is `hello.py` you might have an editor open in one window, and a shell open in another where you input `python hello.py` after you save changes. Another way might be to copy and paste your code to an open python session in the terminal.

The [vim-slime](https://github.com/jpalardy/vim-slime) plugin allows the user to set keybindings to send text directly from a Vim or Neovim buffer to a running shell or window. Configuration code for each target is included in that repository.

[vim-slime-ext-plugins](https://github.com/jpalardy/vim-slime-ext-plugins/) in contrast provides infrastructure for sending text to a target, and leaves the community to develop plugins for each target. This plugin extends `vim-slime-ext-plugins` and targets the Neovim terminal.

## How to Use

See `:h vim-slime.txt` for default keybindings to send text to a target. I repeat the suggested keymappings from the config section above:

- `gz[operator/motion]`: send text using an operator or motion.
- In visual mode `gz` can send visually selected text to the target.
- `gzz` sends the current line to the target.

Of course these are optional and you can do what you want.

When you use one of these motions, the plugin will try to find the most recently opened terminal and select it as the target. You are prompted with the identification number (`terminal_job_id`) or (`terminal_job_pid` if `g:sline_input_pid` is set to a nonzero value). `terminal_job_pid` is easier to use because that number is displayed on the status line of each terminal buffer. `terminal_job_id` is used by default because that is that Neovim internally uses to send text to the terminals.

Call the `:SlimeConfig` function from an open buffer to reconfigure the terminal connection of that buffer.

## Capabilities Summary

At the risk of repetition, this plugin:

### Keeps Track of Multiple Terminals

It does this using the `g:slime_last_channel` variable which is an array of vimscript dictionaries containing the PIDs (external identifier) and job ids (Neovim internal identifier) of open Neovim terminals. If a connected terminal is closed, upon trying to send text again the user is prompted to pick another terminal, with the next-most recently opened terminal selected by default. If no terminals are available, or if there is misconfiguration, a helpful message telling you to open a new terminal is displayed. If you find the messages aren't helpful enogh please leave feedback witha repo maintainer.


### Can Use PID or internal job id for configuration

Under the hood Neovim sends text to a running a terminal using the `terminal_job_id`, which are typically low numbers. Neovim also keeps track of the `terminal_job_pid` which is the system's identifier, and importantly *is displayed on the status line of the terinal buffer*. The default settings are that the user if prompted with a `terminal_job_id` value, because this is what is used by neovim internally to send text to a terminal. However, because it is readily displayed for each running terminal, `terminal_job_pid` is much easier to manually configure, and that is why `vim.g.slime_input_pid=1` is included in the example configuration (the vimscript equivalent of this is `let g:slime_input_pid=1`.


## Glossary

- `PID`: In Linux, PID stands for "Process IDentifier". A PID is a unique number that is automatically assigned to each process when it is created on a Unix-like operating system. A process is an executing (i.e., running) instance of a program. Applies to Macos as well. Represented as `terminal_job_pid` in under the `variables` field of the `getbufinfo()` Neovim vimscript command.


- `job id`: the internal identifier that Neovim attaches to a running terminal process. `termnal_job_id` is the corresponding field in the `variables` section of `getbufinfo()`.


159 changes: 148 additions & 11 deletions autoload/slime_neovim.vim
Original file line number Diff line number Diff line change
@@ -1,18 +1,155 @@
" Public API for vim-slime to use.

function! slime_neovim#config(config)
if !exists("a:config['neovim']")
let a:config["neovim"] = {"jobid": get(g:, "slime_last_channel", "")}
end
if exists("g:slime_get_jobid")
let a:config["neovim"]["jobid"] = g:slime_get_jobid()
else
let a:config["neovim"]["jobid"] = input("jobid: ", a:config["neovim"]["jobid"])
end
return a:config
function! slime_neovim#config(config, ...)
let internal = a:0 > 0 && a:1 == "internal" "testing if we called the function internally, or if slime_neovim_ext_plugins called it
let config_in = a:config
if s:NotExistsLastChannel()
if internal
throw "Terminal not detected."
else
return {}
endif
endif
if s:NotValidConfig(config_in)
let config_in = {}
let config_in["neovim"]= {"jobid": str2nr(get(g:slime_last_channel, -1, "")['jobid'])}
endif
let id_in = 0
if get(g:, "slime_input_pid", 0)
let pid_in = input("pid: ", str2nr(slime_neovim#translate_id_to_pid(config_in["neovim"]["jobid"])))
let id_in = slime_neovim#translate_pid_to_id(pid_in)
else
if exists("g:slime_get_jobid")
let id_in = g:slime_get_jobid()
else
let id_in = input("jobid: ", str2nr(config_in["neovim"]["jobid"]))
let id_in = str2nr(id_in)
endif
endif
if id_in == -1
if internal
throw "No matching job id for the provided pid."
else
return {}
endif
endif
let config_in["neovim"]["jobid"] = id_in
if s:NotValidConfig(config_in)
if internal
throw "Channel id not valid."
else
return {}
endif
endif
return config_in
endfunction


function! s:NotExistsConfig() abort
return exists("b:slime_config")
endfunction

function! s:NotValidConfig(config) abort
"checks if the current configuration refers to an actual running terminal
let not_valid = 1

if type(a:config) != v:t_dict || !exists("g:slime_last_channel")
return not_valid
endif

if has_key(a:config, 'neovim') && has_key(a:config['neovim'], 'jobid') && index( slime_neovim#channel_to_array(g:slime_last_channel), a:config['neovim']['jobid']) >= 0
let not_valid = 0
return not_valid
endif



return not_valid

endfunction


function! slime_neovim#SlimeAddChannel()
if !exists("g:slime_last_channel")
let g:slime_last_channel = [{'jobid': &channel, 'pid': b:terminal_job_pid}]
else
call add(g:slime_last_channel, {'jobid': &channel, 'pid': b:terminal_job_pid})
endif
endfunction

function slime_neovim#SlimeClearChannel()
if !exists("g:slime_last_channel")
return
elseif len(g:slime_last_channel) == 1
unlet g:slime_last_channel
else
let bufinfo = getbufinfo()
call filter(bufinfo, {_, val -> has_key(val['variables'], "terminal_job_id") && has_key(val['variables'], "terminal_job_pid")})
call map(bufinfo, {_, val -> val["variables"]["terminal_job_id"] })
call filter(g:slime_last_channel, {_, val -> index(bufinfo, val["jobid"]) >= 0})
endif
endfunction

function! slime_neovim#send(config, text)
call chansend(str2nr(a:config["neovim"]["jobid"]), split(a:text, "\n", 1))
let config_in = a:config
let not_valid = s:NotValidConfig(config_in)

if not_valid

try
let b:slime_config = slime_neovim#config(config_in, "internal")
let config_in = b:slime_config

catch /No matching job id for the provided pid/
echo "No matching job id for the provided pid. Try again. "
return
catch /Terminal not detected./
echo "Terminal not detected: Open a neovim terminal and try again. "
return
catch /Channel id not valid./
echo "Channel id not valid: Open a neovim terminal and try again. "
return
finally
endtry
endif

call chansend(str2nr(config_in["neovim"]["jobid"]), split(a:text, "\n", 1))
endfunction


function! slime_neovim#translate_pid_to_id(pid)
for ch in g:slime_last_channel
if ch['pid'] == a:pid
return ch['jobid']
endif
endfor
return -1
endfunction

function! slime_neovim#translate_id_to_pid(jobid)
for ch in g:slime_last_channel
if ch['jobid'] == a:jobid
return ch['pid']
endif
endfor
return -1
endfunction

function! s:NotExistsLastChannel() abort "
" check if slime_last_channel variable exists
let not_exists = 1

if !exists("g:slime_last_channel") || (len(g:slime_last_channel)) < 1
return not_exists
endif


let not_exists = 0
return not_exists
endfunction



function! slime_neovim#channel_to_array(channel_dict)
return map(copy(a:channel_dict), {_, val -> val["jobid"]})
endfunction
6 changes: 6 additions & 0 deletions plugin/slime-neovim.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
" autocmds to keep of terminal identification numbers whenever a terminal is opened or closed
augroup nvim_slime
autocmd!
autocmd TermOpen * call slime_neovim#SlimeAddChannel()
autocmd TermClose * call slime_neovim#SlimeClearChannel()
augroup END

0 comments on commit 8b4c9b8

Please sign in to comment.