nvim-rs

repository·master·Indexed 42 days ago

https://github.com/killthemule/nvim-rs

A Rust library for writing Neovim msgpack-rpc clients, version 0.9.2. It provides asynchronous capabilities to handle nested requests and includes extended types for interacting with Neovim buffers, tabpages, and windows. The library supports both tokio and async-std runtimes and includes tools for implementing custom RPC handlers and managing plugin lifecycles via Vimscript glue code.

Tokens
5.3K
Snippets
24
Records
28
Agent score
53%

What's inside nvim-rs

  1. Overview of nvim-rs

    master

    nvim-rs is a Rust library designed for Neovim msgpack-rpc clients. It utilizes asynchronous programming to support arbitrary nesting of requests.

    Note: The API is currently unstable. Developers should refer to the project's roadmap for planned changes.

  2. Notify the RPC plugin of editor events

    master

    Once the RPC process is running, you can send real-time editor state changes to your Rust plugin using the rpcnotify function. This is typically done by mapping Neovim autocmd events to rpcnotify calls.

    Common events to notify include:

    • cursor-moved-i: Triggered on CursorMovedI (Cursor moved in Insert mode).
    • insert-change: Triggered on InsertChange (Text changed in Insert mode).
    • insert-enter: Triggered on InsertEnter.
    • insert-leave: Triggered on InsertLeave.
    • quit: Sent to the plugin to signal it should shut down.

    Example mapping for cursor movement:

    function! s:NotifyCursorMovedI()
      let [ bufnum, lnum, column, off ] = getpos('.')
      call rpcnotify(s:jobid, 'cursor-moved-i', lnum, column)
    endfunction
    " Example of mapping an autocmd to an RPC notification
    autocmd CursorMovedI * :call s:NotifyCursorMovedI()
    
    function! s:NotifyCursorMovedI()
      let [ bufnum, lnum, column, off ] = getpos('.')
      call rpcnotify(s:jobid, 'cursor-moved-i', lnum, column)
    endfunction
  3. Integrate an RPC plugin with Neovim

    master

    To use a plugin built with nvim-rs as an RPC plugin (a child process of Neovim), you must write Vimscript glue code to manage the lifecycle of the plugin process. This involves starting the process with RPC enabled, notifying the process of editor events via rpcnotify, and ensuring the process is terminated when Neovim exits.

    Integration Steps

    1. Create an Autoload script: Place a script in $rtp/autoload/<plugin_name>.vim to handle job management, event notifications (like cursor movement or insert mode changes), and process cleanup.
    2. Create a Plugin script: Place a script in $rtp/plugin/<plugin_name>.vim to expose user-facing commands (e.g., :PluginConnect) to start the process.
    3. Configure the binary path: Set the global variable <plugin_name>_program to point to your compiled Rust binary.

    Lifecycle Management Pattern

    • Starting: Use jobstart() with the 'rpc': v:true option to ensure the child process can communicate via the Neovim RPC protocol.
    • Notifying: Use rpcnotify(jobid, 'method_name', ...args) to send events from Neovim to your Rust plugin.
    • Stopping: Use rpcnotify(jobid, 'quit') followed by jobwait() and jobstop() to gracefully shut down the plugin during VimLeavePre.
    " 1. Autoload script: $rtp/autoload/scorched_earth.vim
    function! s:StartJob()
      if 0 == s:jobid
        " Crucial: 'rpc': v:true enables the RPC interface
        let id = jobstart([g:scorched_earth_program], { 'rpc': v:true, 'on_stderr': function('s:OnStderr') })
        return id
      else
        return 0
      endif
    endfunction
    
    function! s:StopJob()
      if 0 < s:jobid
        call rpcnotify(s:jobid, 'quit')
        let result = jobwait(s:jobid, 500)
        if -1 == result
          call jobstop(s:jobid)
        endif
        let s:jobid = 0
      endif
    endfunction
    
    " 2. Plugin script: $rtp/plugin/scorched_earth.vim
    command! -nargs=0 ScorchedEarthConnect call scorchedEarth#connect()
  4. Run tests for nvim-rs

    master

    To run tests, you may need a Neovim installation. Set the NVIMRS_TEST_BIN environment variable to the path of your Neovim binary.

    You can choose between the tokio or async-std runtimes by specifying the corresponding feature flag during execution.

    # Using tokio (default recommendation)
    export NVIMRS_TEST_BIN=/path/to/nvim
    cargo test --features="use_tokio"
    
    # Using async-std
    export NVIMRS_TEST_BIN=/path/to/nvim
    cargo test --features="use_async-std"
  5. Initialize a Neovim session with `Neovim::new`

    master

    To start a new Neovim session, use Neovim::new. This function requires a reader, a writer, and a handler that implements the Handler and Spawner traits. It returns the Neovim instance and a Future that manages the background I/O and handler loops. You must poll/await this future for the client to function.

    Note: new assumes the connection is already established and ready for RPC calls.

    // Example conceptual usage
    let (neovim, loop_fut) = Neovim::new(reader, writer, handler);
    // You must manage `loop_fut` (e.g., via tokio::spawn) to keep the session alive.
  6. Initialize a Neovim session with `Neovim::handshake`

    master

    If you need to establish a connection that is tolerant to extra data in the reader before the handshake response is received, use Neovim::handshake.

    This method sends a unique handshake message via nvim_exec_lua and waits for the response.

    Important: The message string must follow Neovim's string packing constraints: its length must be either less than 20 characters or more than 31 characters long. Using a length between 20 and 31 will cause an assertion failure.

    // Example conceptual usage
    let (neovim, loop_fut) = Neovim::handshake(reader, writer, handler, "my_unique_handshake_msg").await?;
  7. Reference: UiAttachOptions configuration keys

    master

    The following table maps the UiAttachOptions setter methods to the underlying configuration keys used during Neovim UI attachment.

    Setter MethodConfiguration Key
    set_rgbrgb
    set_overrideoverride
    set_cmdline_externalext_cmdline
    set_hlstate_externalext_hlstate
    set_linegrid_externalext_linegrid
    set_messages_externalext_messages
    set_multigrid_externalext_multigrid
    set_popupmenu_externalext_popupmenu
    set_tabline_externalext_tabline
    set_termcolors_externalext_termcolors
    set_term_nameterm_name
    set_term_colorsterm_colors
    set_term_backgroundterm_background
    set_stdin_fdstdin_fd
    set_stdin_ttystdin_tty
    set_stdout_ttystdout_tty
    set_wildmenu_externalext_wildmenu
    // Example of how keys are mapped internally
    // set_rgb(true) -> ("rgb", Value::Bool(true))
    // set_term_name("xterm") -> ("term_name", Value::String("xterm"))
  8. Configure Neovim UI attachment with UiAttachOptions

    master

    When manually implementing ui_attach via crate::neovim::Neovim::ui_attach, use UiAttachOptions to specify the configuration for the UI implementation.

    UiAttachOptions uses a builder pattern to set various parameters. If you set the same option multiple times, the last value provided will overwrite previous ones.

    Available configuration methods include:

    • set_rgb(bool)
    • set_override(bool)
    • set_cmdline_external(bool)
    • set_hlstate_external(bool)
    • set_linegrid_external(bool)
    • set_messages_external(bool)
    • set_multigrid_external(bool)
    • set_popupmenu_external(bool)
    • set_tabline_external(bool)
    • set_termcolors_external(bool)
    • set_term_name(&str)
    • set_term_colors(u64)
    • set_term_background(&str)
    • set_stdin_fd(u64)
    • set_stdin_tty(bool)
    • set_stdout_tty(bool)
    • set_wildmenu_external(bool)
    let options = UiAttachOptions::new()
        .set_rgb(true)
        .set_popupmenu_external(true)
        .set_term_name("xterm-256color");
  9. Manage Neovim tabpages with the Tabpage struct

    master

    The Tabpage<W> struct represents a Neovim tabpage object. It is tied to a specific Neovim<W> instance. You can use it to interact with the tabpage via RPC calls.

    Key methods:

    • list_wins(): Returns a Vec<Window<W>> containing all windows associated with this tabpage.
    • get_win(): Returns the current window (Window<W>) of the tabpage.
    // Example usage (conceptual):
    // Assuming you have a Tabpage instance named `tabpage` 
    
    // Get all windows in the tabpage
    let windows = tabpage.list_wins().await?;
    
    // Get the current window in the tabpage
    let current_win = tabpage.get_win().await?;
  10. Handle RPC communication errors with CallError

    master

    When calling Neovim APIs, errors are returned as a CallError. These errors can be categorized into several types:

    • SendError(EncodeError, String): Failed to send the request to Neovim.
    • InternalReceiveError(oneshot::Canceled, String): The internal channel for the response was closed.
    • DecodeError(Arc<DecodeError>, String): Neovim's response could not be decoded.
    • NeovimError(Option<i64>, String): Neovim encountered an error during execution (matches :h api).
    • WrongValueType(Value): The response contained a value of an unexpected type.

    Detecting Neovim Shutdown To determine if an error is actually a signal that Neovim has closed (and thus the plugin should quit), use the is_channel_closed() method. This is often triggered by an UnexpectedEof error during communication.

    // Example of checking if a CallError indicates Neovim has closed
    if call_result.is_err() && call_result.as_ref().unwrap_err().is_channel_closed() {
        // Signal to quit the plugin
    }
  11. Decode RpcMessages from an async reader

    master

    The decode function is used to continuously read from an asynchronous reader and extract RpcMessage objects.

    Because messages might arrive in fragments, you must provide a rest: &mut Vec<u8> buffer. The function will:

    1. Attempt to decode a message from the existing rest buffer.
    2. If a full message is found, it returns the message and updates rest to contain only the remaining unparsed bytes.
    3. If the buffer does not contain a full message, it reads more data from the reader into the buffer and tries again.
    4. Returns an error if the reader reaches EOF without a complete message or if a decoding error occurs.
    let mut rest = Vec::new();
    // 'reader' is your AsyncRead source (e.g., a TCP stream)
    let msg = decode(&mut reader, &mut rest).await?;
  12. Interact with Neovim windows using the Window struct

    master

    The Window<W> struct represents a Neovim window object. It is tied to a specific Neovim<W> instance. You can use it to retrieve associated Neovim objects like the current buffer or the current tabpage via RPC calls.

    Key methods:

    • get_buf(): Returns a Buffer<W> representing the buffer associated with this window.
    • get_tabpage(): Returns a Tabpage<W> representing the tabpage containing this window.
    // Example usage of Window methods
    let buffer = window.get_buf().await?;
    let tabpage = window.get_tabpage().await?;