BlinkResearchLabs/blink-edit.nvim

Pure-Lua Neovim plugin for Cursor-style next-edit predictions using local LLMs.

Lua

146

11 commits

updated Feb 1, 2026

See the code

README

██████╗ ██╗     ██╗███╗   ██╗██╗  ██╗    ███████╗██████╗ ██╗████████╗
██╔══██╗██║     ██║████╗  ██║██║ ██╔╝    ██╔════╝██╔══██╗██║╚══██╔══╝
██████╔╝██║     ██║██╔██╗ ██║█████╔╝     █████╗  ██║  ██║██║   ██║
██╔══██╗██║     ██║██║╚██╗██║██╔═██╗     ██╔══╝  ██║  ██║██║   ██║
██████╔╝███████╗██║██║ ╚████║██║  ██╗    ███████╗██████╔╝██║   ██║
╚═════╝ ╚══════╝╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝    ╚══════╝╚═════╝ ╚═╝   ╚═╝

blink-edit.nvim

Pure-Lua Neovim plugin for Cursor-style next-edit predictions using local LLMs.

Neovim Lua License

⚠️ Alpha — Expect breaking changes and rapid iteration ⚠️

blink-edit demo

⚡ What This Is

blink-edit.nvim predicts your next edit and renders it as ghost text. It sends context-aware prompts to a local LLM and shows predictions inline.

  • Fast: Written entirely in Lua.
  • Private: Bring your own local model (OpenAI-compatible or Ollama).
  • Simple: Accept with <Tab>, reject with <Esc> in insert mode.

✨ Features

  • 👻 Ghost Text — Next-edit predictions rendered inline
  • 🎮 Intuitive Controls — Accept/reject with standard insert-mode keymaps
  • 🏥 Health Check — Status popup with live server health monitoring
  • 🔌 Providers — Built-in support for Sweep and Zeta (more coming soon)
  • 🤖 Backends — Connects to any OpenAI-compatible API or Ollama

📦 Installation

Requirements

lazy.nvim

{
  "BlinkResearchLabs/blink-edit.nvim",
  config = function()
    require("blink-edit").setup({
      llm = {
        provider = "sweep",
        backend = "openai",
        url = "http://localhost:8000",
        model = "sweep",
      },
    })
  end,
}

packer.nvim

use({
  "BlinkResearchLabs/blink-edit.nvim",
  config = function()
    require("blink-edit").setup({
      llm = {
        provider = "sweep",
        backend = "openai",
        url = "http://localhost:8000",
        model = "sweep",
      },
    })
  end,
})

🚀 Quick Start

  1. Setup the plugin:
require("blink-edit").setup({
  llm = {
    provider = "sweep",
    backend = "openai",
    url = "http://localhost:8000",
    model = "sweep",
  },
})

Model name: llm.model must match an ID returned by your backend (/v1/models). Examples:

  • sweepai/sweep-next-edit-1.5B (if /v1/models returns the HF name)
  • /home/.../sweep-next-edit-1.5b.q8_0.v2.gguf (llama.cpp can expose the full path)
  • sweep (if you served an alias like --served-model-name sweep)
  • sweep when /v1/models only exposes the full path
  1. Start typing in Insert mode. Predictions will appear as ghost text.

  2. Control:

    • <Tab> to Accept the next hunk
    • <C-j> to Accept Line (line-by-line through visible changes)
    • <C-]> to Clear (dismiss without leaving insert mode)
    • <Esc> to Reject and exit insert mode
    • :BlinkEditStatus to check server health

🔌 Providers

Sweep

Default provider. Uses <|file_sep|> delimiters for context sections. Works best with the Sweep Next-Edit model.

llm = { provider = "sweep", backend = "openai", url = "http://localhost:8000", model = "sweep" }

Zeta

Instruction-style prompt with editable region markers. Supports next-edit mode only.

llm = { provider = "zeta", backend = "openai", url = "http://localhost:8000", model = "zeta" }

Generic (Not Validated)

Template-based provider. Exists but is not validated. Avoid in production until tested.


🤖 Backends

OpenAI-compatible

Uses /v1/completions. Works with llama.cpp, vLLM, text-generation-webui, and other OpenAI-compatible servers.

Ollama

Uses /api/generate. Works with any model available through Ollama. See Running a Local Model for setup.


⚙️ Configuration Reference

Click to view full default configuration
require("blink-edit").setup({
  llm = {
    backend = "openai",           -- "openai" | "ollama"
    provider = "sweep",           -- "sweep" | "zeta" | "generic"
    url = "http://localhost:8000",-- Model server URL
    model = "sweep",              -- Model name sent to backend
    temperature = 0.0,            -- Sampling temperature (0 = deterministic)
    max_tokens = 512,             -- Max tokens to generate
    timeout_ms = 5000,            -- Request timeout in ms
  },

  context = {
    enabled = true,               -- Master switch for context collection
    lines_before = nil,           -- Lines before cursor (nil = provider default)
    lines_after = nil,            -- Lines after cursor (nil = provider default)
    max_tokens = 512,             -- Token budget for context

    selection = {
      enabled = true,             -- Include visual selection in context
      max_lines = 10,             -- Max lines from selection
    },

    lsp = {
      enabled = true,             -- Fetch LSP references for cursor symbol
      max_definitions = 2,        -- Max definition locations
      max_references = 2,         -- Max reference locations
      timeout_ms = 100,           -- LSP request timeout
    },

    same_file = {
      enabled = true,             -- Include surrounding lines from same file
      max_lines_before = 20,      -- Lines above the window
      max_lines_after = 20,       -- Lines below the window
    },

    history = {
      enabled = false,            -- Include recent edit history
      max_items = 5,              -- Number of history entries
      max_tokens = 512,           -- Token budget for history
      max_files = 2,              -- Max files in history
      global = true,              -- Share history across buffers
    },
  },

  ui = {
    progress = true,              -- Show "thinking..." indicator
    suppress_lsp_floats = true,   -- Hide LSP floats while prediction visible
  },

  prefetch = {
    enabled = false,              -- Speculative prefetch (uses extra tokens)
    strategy = "n-1",             -- Prefetch when one hunk remains
  },

  normal_mode = {
    enabled = false,              -- Trigger predictions on idle in normal mode
    debounce_ms = 200,            -- Normal-mode debounce (ms)
  },

  debounce_ms = 100,              -- Delay (ms) before sending prediction request

  keymaps = {
    insert = {
      accept = "<Tab>",           -- Accept next hunk
      accept_line = "<C-j>",      -- Accept next visible line (line-by-line)
      clear = "<C-]>",            -- Dismiss ghost text (stay in insert mode)
      reject = "<Esc>",           -- Reject and exit insert mode
    },
    normal = {
      accept = "<Tab>",           -- Accept next hunk (when normal_mode.enabled)
      accept_line = "<C-j>",      -- Accept next visible line (when normal_mode.enabled)
    },
  },
})

⌨️ Keymaps & Commands

Insert Mode (always active)

KeyActionDescription
<Tab>AcceptAccept the next predicted hunk
<C-j>Accept LineAccept next visible line (line-by-line)
<C-]>ClearDismiss prediction (stay in insert mode)
<Esc>RejectReject prediction and exit insert mode

Normal Mode (when normal_mode.enabled = true)

KeyActionDescription
<Tab>AcceptAccept the next predicted hunk
<C-j>Accept LineAccept next visible line (line-by-line)

Note: Normal mode only maps accept/accept_line; when a prediction is visible, pressing <Esc> clears it without adding a persistent mapping.

Note: After you start line-by-line accepts with <C-j>, <Tab> accepts the remaining visible lines.

Note: If you use blink.cmp or nvim-cmp, the <Tab> keymap automatically checks if the completion menu is visible before accepting predictions.

Customization:

Keymaps are organized by mode and can be customized or disabled (set to nil):

require("blink-edit").setup({
  keymaps = {
    insert = {
      accept = "<C-y>",        -- Use Ctrl+y to accept
      accept_line = "<C-j>",   -- Keep default
      clear = nil,             -- Disable clear keymap
      reject = "<C-n>",        -- Use Ctrl+n to reject
    },
    normal = {
      accept = "<C-y>",        -- Match insert mode binding
      accept_line = nil,       -- Disable in normal mode
    },
  },
})
CommandDescription
:BlinkEditStatusShow status popup with health, config, and state
:BlinkEditEnableEnable predictions
:BlinkEditDisableDisable predictions
:BlinkEditToggleToggle predictions on/off

🛠️ Public API

require("blink-edit").setup(opts)     -- Initialize with config

require("blink-edit").enable()        -- Enable predictions
require("blink-edit").disable()       -- Disable predictions
require("blink-edit").toggle()        -- Toggle predictions

require("blink-edit").trigger()       -- Manually trigger a prediction
require("blink-edit").accept()        -- Accept next hunk
require("blink-edit").accept_line()   -- Accept next visible line (line-by-line)
require("blink-edit").clear()         -- Clear prediction (stay in insert mode)
require("blink-edit").reject()        -- Reject prediction

require("blink-edit").status()        -- Get status table
require("blink-edit").health_check()  -- Check backend health

📊 Status UI

:BlinkEditStatus opens a popup showing:

  • Provider / Backend / Model / URL
  • Server health (with live refresh)
  • In-flight request state
  • Current prediction visibility
  • History entry count

Press r to refresh health, q or <Esc> to close.


📝 Notes

LSP References

  • References are fetched only for the symbol under the cursor at InsertEnter.
  • If your LSP server returns an error or is still indexing, references won't appear in the prompt.
  • Some LSP servers (e.g. certain Pyright versions) have known bugs with reference requests.

Selection Context

  • Selection is captured from visual selection and yank events.
  • When enabled, selected text is included in the prompt even if you're editing elsewhere.

🔧 Troubleshooting

Predictions aren't showing up
  1. Run :BlinkEditStatus to check if the server is healthy.
  2. Ensure your model server (Ollama/llama.cpp) is running and accessible.
LSP references are missing
  1. Check :LspInfo to verify an LSP is attached to the buffer.
  2. Your LSP server may have errors — test with :lua vim.print(vim.lsp.buf_request_sync(...)).
Predictions are slow
  1. Try using a quantized model (e.g., q4_0 or q8_0).
  2. Reduce context.max_tokens or context.lsp.timeout_ms in your config.
Tab conflicts with completion menu

We check for blink.cmp/nvim-cmp visibility, but you can change keymaps.insert.accept (and keymaps.normal.accept for normal mode) if needed.


🐛 Debugging

vim.g.blink_edit_debug = true   -- Summary logs
vim.g.blink_edit_debug = 2      -- Verbose (prompts + responses)

🧠 Running a Local Model

A 1.5B parameter model optimized for next-edit prediction.

Using llama-server:

# Download and run directly
llama-server -hf sweepai/sweep-next-edit-1.5b --port 8000

# Or download manually first
huggingface-cli download sweepai/sweep-next-edit-1.5b \
  sweep-next-edit-1.5b.q8_0.v2.gguf --local-dir ./models

llama-server -m ./models/sweep-next-edit-1.5b.q8_0.v2.gguf --port 8000

Config:

llm = { provider = "sweep", backend = "openai", url = "http://localhost:8000", model = "sweep" }

Option B: Zeta

A 7B parameter model from Zed Industries, fine-tuned from Qwen2.5-Coder-7B.

Using vLLM (Recommended):

vllm serve zed-industries/zeta --served-model-name zeta --port 8000

# With optimizations (requires compatible GPU)
vllm serve zed-industries/zeta --served-model-name zeta --port 8000 \
  --enable-prefix-caching --quantization="fp8"

Config:

llm = { provider = "zeta", backend = "openai", url = "http://localhost:8000", model = "zeta" }

Using Ollama:

ollama pull lennyerik/zeta
# Runs on port 11434 by default

Config for Ollama:

llm = { provider = "zeta", backend = "ollama", url = "http://localhost:11434", model = "lennyerik/zeta" }

🗺️ Roadmap

  • Validate and expand generic provider support
  • Support non-local / remote model backends
  • Explore other editor integrations
  • ...

🤝 Contributing

Issues and PRs are welcome! This is an alpha project — your feedback helps shape the future of local AI coding in Neovim.


📄 License

MIT — see LICENSE.

Contributors

shravandoda

10 commits

wjyoung65

1 commits

BlinkResearchLabs/blink-edit.nvim

Pure-Lua Neovim plugin for Cursor-style next-edit predictions using local LLMs.

Lua

146

11 commits

updated Feb 1, 2026

See the code

README

██████╗ ██╗     ██╗███╗   ██╗██╗  ██╗    ███████╗██████╗ ██╗████████╗
██╔══██╗██║     ██║████╗  ██║██║ ██╔╝    ██╔════╝██╔══██╗██║╚══██╔══╝
██████╔╝██║     ██║██╔██╗ ██║█████╔╝     █████╗  ██║  ██║██║   ██║
██╔══██╗██║     ██║██║╚██╗██║██╔═██╗     ██╔══╝  ██║  ██║██║   ██║
██████╔╝███████╗██║██║ ╚████║██║  ██╗    ███████╗██████╔╝██║   ██║
╚═════╝ ╚══════╝╚═╝╚═╝  ╚═══╝╚═╝  ╚═╝    ╚══════╝╚═════╝ ╚═╝   ╚═╝

blink-edit.nvim

Pure-Lua Neovim plugin for Cursor-style next-edit predictions using local LLMs.

Neovim Lua License

⚠️ Alpha — Expect breaking changes and rapid iteration ⚠️

blink-edit demo

⚡ What This Is

blink-edit.nvim predicts your next edit and renders it as ghost text. It sends context-aware prompts to a local LLM and shows predictions inline.

  • Fast: Written entirely in Lua.
  • Private: Bring your own local model (OpenAI-compatible or Ollama).
  • Simple: Accept with <Tab>, reject with <Esc> in insert mode.

✨ Features

  • 👻 Ghost Text — Next-edit predictions rendered inline
  • 🎮 Intuitive Controls — Accept/reject with standard insert-mode keymaps
  • 🏥 Health Check — Status popup with live server health monitoring
  • 🔌 Providers — Built-in support for Sweep and Zeta (more coming soon)
  • 🤖 Backends — Connects to any OpenAI-compatible API or Ollama

📦 Installation

Requirements

lazy.nvim

{
  "BlinkResearchLabs/blink-edit.nvim",
  config = function()
    require("blink-edit").setup({
      llm = {
        provider = "sweep",
        backend = "openai",
        url = "http://localhost:8000",
        model = "sweep",
      },
    })
  end,
}

packer.nvim

use({
  "BlinkResearchLabs/blink-edit.nvim",
  config = function()
    require("blink-edit").setup({
      llm = {
        provider = "sweep",
        backend = "openai",
        url = "http://localhost:8000",
        model = "sweep",
      },
    })
  end,
})

🚀 Quick Start

  1. Setup the plugin:
require("blink-edit").setup({
  llm = {
    provider = "sweep",
    backend = "openai",
    url = "http://localhost:8000",
    model = "sweep",
  },
})

Model name: llm.model must match an ID returned by your backend (/v1/models). Examples:

  • sweepai/sweep-next-edit-1.5B (if /v1/models returns the HF name)
  • /home/.../sweep-next-edit-1.5b.q8_0.v2.gguf (llama.cpp can expose the full path)
  • sweep (if you served an alias like --served-model-name sweep)
  • sweep when /v1/models only exposes the full path
  1. Start typing in Insert mode. Predictions will appear as ghost text.

  2. Control:

    • <Tab> to Accept the next hunk
    • <C-j> to Accept Line (line-by-line through visible changes)
    • <C-]> to Clear (dismiss without leaving insert mode)
    • <Esc> to Reject and exit insert mode
    • :BlinkEditStatus to check server health

🔌 Providers

Sweep

Default provider. Uses <|file_sep|> delimiters for context sections. Works best with the Sweep Next-Edit model.

llm = { provider = "sweep", backend = "openai", url = "http://localhost:8000", model = "sweep" }

Zeta

Instruction-style prompt with editable region markers. Supports next-edit mode only.

llm = { provider = "zeta", backend = "openai", url = "http://localhost:8000", model = "zeta" }

Generic (Not Validated)

Template-based provider. Exists but is not validated. Avoid in production until tested.


🤖 Backends

OpenAI-compatible

Uses /v1/completions. Works with llama.cpp, vLLM, text-generation-webui, and other OpenAI-compatible servers.

Ollama

Uses /api/generate. Works with any model available through Ollama. See Running a Local Model for setup.


⚙️ Configuration Reference

Click to view full default configuration
require("blink-edit").setup({
  llm = {
    backend = "openai",           -- "openai" | "ollama"
    provider = "sweep",           -- "sweep" | "zeta" | "generic"
    url = "http://localhost:8000",-- Model server URL
    model = "sweep",              -- Model name sent to backend
    temperature = 0.0,            -- Sampling temperature (0 = deterministic)
    max_tokens = 512,             -- Max tokens to generate
    timeout_ms = 5000,            -- Request timeout in ms
  },

  context = {
    enabled = true,               -- Master switch for context collection
    lines_before = nil,           -- Lines before cursor (nil = provider default)
    lines_after = nil,            -- Lines after cursor (nil = provider default)
    max_tokens = 512,             -- Token budget for context

    selection = {
      enabled = true,             -- Include visual selection in context
      max_lines = 10,             -- Max lines from selection
    },

    lsp = {
      enabled = true,             -- Fetch LSP references for cursor symbol
      max_definitions = 2,        -- Max definition locations
      max_references = 2,         -- Max reference locations
      timeout_ms = 100,           -- LSP request timeout
    },

    same_file = {
      enabled = true,             -- Include surrounding lines from same file
      max_lines_before = 20,      -- Lines above the window
      max_lines_after = 20,       -- Lines below the window
    },

    history = {
      enabled = false,            -- Include recent edit history
      max_items = 5,              -- Number of history entries
      max_tokens = 512,           -- Token budget for history
      max_files = 2,              -- Max files in history
      global = true,              -- Share history across buffers
    },
  },

  ui = {
    progress = true,              -- Show "thinking..." indicator
    suppress_lsp_floats = true,   -- Hide LSP floats while prediction visible
  },

  prefetch = {
    enabled = false,              -- Speculative prefetch (uses extra tokens)
    strategy = "n-1",             -- Prefetch when one hunk remains
  },

  normal_mode = {
    enabled = false,              -- Trigger predictions on idle in normal mode
    debounce_ms = 200,            -- Normal-mode debounce (ms)
  },

  debounce_ms = 100,              -- Delay (ms) before sending prediction request

  keymaps = {
    insert = {
      accept = "<Tab>",           -- Accept next hunk
      accept_line = "<C-j>",      -- Accept next visible line (line-by-line)
      clear = "<C-]>",            -- Dismiss ghost text (stay in insert mode)
      reject = "<Esc>",           -- Reject and exit insert mode
    },
    normal = {
      accept = "<Tab>",           -- Accept next hunk (when normal_mode.enabled)
      accept_line = "<C-j>",      -- Accept next visible line (when normal_mode.enabled)
    },
  },
})

⌨️ Keymaps & Commands

Insert Mode (always active)

KeyActionDescription
<Tab>AcceptAccept the next predicted hunk
<C-j>Accept LineAccept next visible line (line-by-line)
<C-]>ClearDismiss prediction (stay in insert mode)
<Esc>RejectReject prediction and exit insert mode

Normal Mode (when normal_mode.enabled = true)

KeyActionDescription
<Tab>AcceptAccept the next predicted hunk
<C-j>Accept LineAccept next visible line (line-by-line)

Note: Normal mode only maps accept/accept_line; when a prediction is visible, pressing <Esc> clears it without adding a persistent mapping.

Note: After you start line-by-line accepts with <C-j>, <Tab> accepts the remaining visible lines.

Note: If you use blink.cmp or nvim-cmp, the <Tab> keymap automatically checks if the completion menu is visible before accepting predictions.

Customization:

Keymaps are organized by mode and can be customized or disabled (set to nil):

require("blink-edit").setup({
  keymaps = {
    insert = {
      accept = "<C-y>",        -- Use Ctrl+y to accept
      accept_line = "<C-j>",   -- Keep default
      clear = nil,             -- Disable clear keymap
      reject = "<C-n>",        -- Use Ctrl+n to reject
    },
    normal = {
      accept = "<C-y>",        -- Match insert mode binding
      accept_line = nil,       -- Disable in normal mode
    },
  },
})
CommandDescription
:BlinkEditStatusShow status popup with health, config, and state
:BlinkEditEnableEnable predictions
:BlinkEditDisableDisable predictions
:BlinkEditToggleToggle predictions on/off

🛠️ Public API

require("blink-edit").setup(opts)     -- Initialize with config

require("blink-edit").enable()        -- Enable predictions
require("blink-edit").disable()       -- Disable predictions
require("blink-edit").toggle()        -- Toggle predictions

require("blink-edit").trigger()       -- Manually trigger a prediction
require("blink-edit").accept()        -- Accept next hunk
require("blink-edit").accept_line()   -- Accept next visible line (line-by-line)
require("blink-edit").clear()         -- Clear prediction (stay in insert mode)
require("blink-edit").reject()        -- Reject prediction

require("blink-edit").status()        -- Get status table
require("blink-edit").health_check()  -- Check backend health

📊 Status UI

:BlinkEditStatus opens a popup showing:

  • Provider / Backend / Model / URL
  • Server health (with live refresh)
  • In-flight request state
  • Current prediction visibility
  • History entry count

Press r to refresh health, q or <Esc> to close.


📝 Notes

LSP References

  • References are fetched only for the symbol under the cursor at InsertEnter.
  • If your LSP server returns an error or is still indexing, references won't appear in the prompt.
  • Some LSP servers (e.g. certain Pyright versions) have known bugs with reference requests.

Selection Context

  • Selection is captured from visual selection and yank events.
  • When enabled, selected text is included in the prompt even if you're editing elsewhere.

🔧 Troubleshooting

Predictions aren't showing up
  1. Run :BlinkEditStatus to check if the server is healthy.
  2. Ensure your model server (Ollama/llama.cpp) is running and accessible.
LSP references are missing
  1. Check :LspInfo to verify an LSP is attached to the buffer.
  2. Your LSP server may have errors — test with :lua vim.print(vim.lsp.buf_request_sync(...)).
Predictions are slow
  1. Try using a quantized model (e.g., q4_0 or q8_0).
  2. Reduce context.max_tokens or context.lsp.timeout_ms in your config.
Tab conflicts with completion menu

We check for blink.cmp/nvim-cmp visibility, but you can change keymaps.insert.accept (and keymaps.normal.accept for normal mode) if needed.


🐛 Debugging

vim.g.blink_edit_debug = true   -- Summary logs
vim.g.blink_edit_debug = 2      -- Verbose (prompts + responses)

🧠 Running a Local Model

A 1.5B parameter model optimized for next-edit prediction.

Using llama-server:

# Download and run directly
llama-server -hf sweepai/sweep-next-edit-1.5b --port 8000

# Or download manually first
huggingface-cli download sweepai/sweep-next-edit-1.5b \
  sweep-next-edit-1.5b.q8_0.v2.gguf --local-dir ./models

llama-server -m ./models/sweep-next-edit-1.5b.q8_0.v2.gguf --port 8000

Config:

llm = { provider = "sweep", backend = "openai", url = "http://localhost:8000", model = "sweep" }

Option B: Zeta

A 7B parameter model from Zed Industries, fine-tuned from Qwen2.5-Coder-7B.

Using vLLM (Recommended):

vllm serve zed-industries/zeta --served-model-name zeta --port 8000

# With optimizations (requires compatible GPU)
vllm serve zed-industries/zeta --served-model-name zeta --port 8000 \
  --enable-prefix-caching --quantization="fp8"

Config:

llm = { provider = "zeta", backend = "openai", url = "http://localhost:8000", model = "zeta" }

Using Ollama:

ollama pull lennyerik/zeta
# Runs on port 11434 by default

Config for Ollama:

llm = { provider = "zeta", backend = "ollama", url = "http://localhost:11434", model = "lennyerik/zeta" }

🗺️ Roadmap

  • Validate and expand generic provider support
  • Support non-local / remote model backends
  • Explore other editor integrations
  • ...

🤝 Contributing

Issues and PRs are welcome! This is an alpha project — your feedback helps shape the future of local AI coding in Neovim.


📄 License

MIT — see LICENSE.

Contributors

shravandoda

10 commits

wjyoung65

1 commits

Languages

Lua

100.0%