nvim-oxi

repository·main·Indexed 22 days ago

https://github.com/noib3/nvim-oxi

Safe and idiomatic Rust bindings to the Neovim API using FFI. nvim-oxi allows developers to write high-performance, type-safe Neovim plugins by hooking directly into Neovim's C code, providing feature parity with in-process Lua plugins. It includes tools for exposing Rust functions to Lua, deserializing Lua tables via serde, managing Neovim threads with libuv, and a dedicated testing harness using the #[nvim_oxi::test] macro.

Tokens
25K
Snippets
96
Records
128
Agent score
77%

What's inside nvim-oxi

  1. Use nvim-oxi for Neovim plugin development

    main

    nvim-oxi provides safe and idiomatic Rust bindings to the Neovim API. Unlike traditional plugins that use RPC channels (which require MessagePack serialization and limit features like callbacks), nvim-oxi uses Rust's Foreign Function Interface (FFI) to hook directly into Neovim's C code. This provides feature parity with 'in-process' Lua plugins and avoids the overhead of an extra IO layer.

    Key advantages include:

    • Access to the Rust ecosystem: Use crates for networking, IO, serialization, and concurrency.
    • Fully typed API: Function fields and callback arguments are checked at compile-time, allowing for faster iteration via cargo check.
  2. Set up a new `nvim-oxi` plugin crate

    main

    To start a new plugin, create a library crate and configure Cargo.toml to produce a dynamic library (cdylib) and include nvim-oxi as a dependency.

    1. Create the crate:

      cargo new --lib {your_plugin}
    2. Update Cargo.toml:

    [lib]
    crate-type = ["cdylib"]
    
    [dependencies]
    nvim-oxi = "0.3"
    1. Annotate your entry point in lib.rs using the #[nvim_oxi::plugin] macro:
    // lib.rs
    
    #[nvim_oxi::plugin]
    fn foo() -> i32 {
        42
    }
    #[nvim_oxi::plugin]
    fn foo() -> i32 {
        42
    }
  3. Configure build.rs for nvim-oxi integration tests

    main

    To run integration tests, you must place them in a separate cdylib crate and use the following build.rs configuration to ensure the environment is set up correctly for the Neovim testing harness.

    // build.rs
    fn main() -> Result<(), nvim_oxi::tests::BuildError> {
        nvim_oxi::tests::build()
    }
  4. Configure linker for macOS (Apple Darwin)

    main

    macOS users must configure the Rust linker to treat FFI functions provided by nvim-oxi as dynamic lookups. Create a .cargo/config file in your project root with the following settings:

    [target.x86_64-apple-darwin]
    rustflags = [
      "-C", "link-arg=-undefined",
      "-C", "link-arg=dynamic_lookup",
    ]
    
    [target.aarch64-apple-darwin]
    rustflags = [
      "-C", "link-arg=-undefined",
      "-C", "link-arg=dynamic_lookup",
    ]
  5. Test Neovim code with the nvim-oxi test feature

    main

    By enabling the test feature, you can use the #[nvim_oxi::test] macro. This replaces the standard #[test] macro and allows you to run Rust tests from within a spawned Neovim instance. When cargo test is executed, it spawns a new Neovim process using the nvim binary found in your $PATH, runs your code, and then exits.

    Limitations:

    • You cannot have two tests with the same name in the same crate, even if they are in different modules.
    • Integration tests must reside in a separate cdylib crate.
    use nvim_oxi::api;
    
    #[nvim_oxi::test]
    fn set_get_del_var() {
        api::set_var("foo", 42).unwrap();
        assert_eq!(Ok(42), api::get_var("foo"));
        assert_eq!(Ok(()), api::del_var("foo"));
    }
  6. Install and load your compiled plugin in Neovim

    main

    After building your plugin with cargo build {--release}, follow these steps to load it into Neovim:

    1. Locate artifacts: Find the compiled library in target/debug or target/release.

      • Linux: libfoo.so
      • macOS: libfoo.dylib
      • Windows: foo.dll
    2. Prepare Lua directory: Create a directory named lua and place the library inside it. Rename the file to match Lua naming conventions:

      • Linux: foo.so
      • macOS: foo.so
      • Windows: foo.dll (no renaming required)
    3. Update runtimepath: Add the parent directory of the lua folder to Neovim's runtimepath: :set rtp+=~/foobar (assuming the path is ~/foobar/lua)

    4. Load in Lua: Use require to load the plugin. It will return the output of your annotated entry point function.

    print(require("foo")) -- prints 42
    print(require("foo")) -- prints `42`
  7. Use nvim-oxi core modules

    main

    The nvim-oxi crate is organized into several functional modules that provide different levels of access to Neovim:

    • api: Bindings to the official Neovim C API.
    • libuv: (Requires libuv feature) Bindings to the Neovim event loop powered by libuv, accessible via vim.loop in Lua.
    • lua: Low-level Rust bindings to LuaJIT, the Lua engine used by Neovim.
    • mlua: (Requires mlua feature) Safe Lua bindings via the mlua crate, including the ability to retrieve the active Lua instance.
    • types: Common Neovim-related types and string utilities.
  8. Use nvim-oxi API types and modules

    main

    The api crate exports several modules that provide access to Neovim's core functionality. Key exported modules include:

    • autocmd: Autocommand management.
    • buffer: Buffer-related operations and the Buffer object.
    • command: Command execution.
    • extmark: Extended marks (extmarks) management.
    • options: Neovim option manipulation.
    • tabpage: Tabpage-related operations and the TabPage object.
    • types: Core Neovim types.
    • window: Window-related operations and the Window object.
    • vim: Core vim table bindings.
    • vimscript: Vimscript execution capabilities.
  9. Understand the nvim-oxi API naming convention

    main

    The nvim-oxi API provides bindings to the Neovim API. To make the API more idiomatic, it follows two main transformation rules:

    1. Prefix Removal: All functions have the leading nvim_ prefix removed. For example, nvim_get_current_buf is exposed as get_current_buf.
    2. Object-Oriented Methods: Functions that originally started with nvim_buf_*, nvim_win_*, or nvim_tabpage_* are implemented as methods on the Buffer, Window, and TabPage objects respectively, rather than being standalone functions.
  10. Understand the Neovim `Object` type

    main

    The Object struct is the core representation of any valid Neovim type in the nvim-oxi ecosystem. It is a C-compatible (#[repr(C)]) container that holds either a primitive value or a reference to a Lua object. It is used to bridge data between Rust and Neovim/Lua.

    Key characteristics:

    • Type Safety: It uses an ObjectKind enum to track the underlying data type.
    • Memory Management: It handles ownership for complex types like String, Array, and Dictionary using ManuallyDrop to ensure correct cleanup when the Object is dropped.
    • Conversion: It implements From for most common Rust primitives (bool, integer, float, string, etc.), making it easy to wrap values.
  11. Understand the Neovim `String` type

    main

    The String type in nvim-oxi is a binding to Neovim's internal string representation. It differs from Rust's standard String in several critical ways:

    • Not guaranteed to be valid UTF-8: It can contain arbitrary byte sequences.
    • Null bytes: It can contain null bytes within the sequence.
    • Null-terminated: The underlying buffer is null-terminated.

    When working with this type, use to_str() if you need a validated UTF-8 slice, or to_string_lossy() if you want to convert it to a standard Rust String while replacing invalid UTF-8 sequences with the replacement character .

  12. Use the `derive(OptsBuilder)` macro to create option builders

    main

    The derive(OptsBuilder) macro (implemented via expand_derive_opts_builder) automatically generates a builder pattern for a struct.

    When applied to a struct, it:

    1. Creates a new builder struct named {StructName}Builder.
    2. Implements Default for the builder, initializing it with the struct's Default values.
    3. Implements Clone for the builder.
    4. Adds a builder() method to the original struct to instantiate the builder.
    5. Generates setter methods for each named field in the struct.
    6. Provides a .build() method on the builder to consume it and return the original struct.

    To enable bitmask tracking, the first field of the struct must be marked with #[builder(mask)]. This field acts as the mask for all subsequent fields.

    #[derive(Default)]
    #[derive(OptsBuilder)]
    struct MyOptions {
        #[builder(mask)]
        _mask: u32,
        field_a: String,
        field_b: i32,
    }
    
    // Usage:
    let opts = MyOptions::builder()
        .field_a("hello".to_string())
        .field_b(42)
        .build();