My nvim configuration
feat(blink): toggle blink on and off
6 files changed, +198 -2
+16-1claude-nvim/lua/claude/ui.lua
| @@ -541,19 +541,34 @@ ensure_panel_for_current_tab = function() | ||
| 541 | 541 | return p |
| 542 | 542 | end |
| 543 | 543 | |
| 544 | +local function buf_display_path(buf) | |
| 545 | + local fname = vim.api.nvim_buf_get_name(buf) | |
| 546 | + if fname == "" then return "[No Name]" end | |
| 547 | + return vim.fn.fnamemodify(fname, ":.") | |
| 548 | +end | |
| 549 | + | |
| 544 | 550 | function M.prompt_visual_chat() |
| 545 | 551 | local origin_buf = vim.api.nvim_get_current_buf() |
| 546 | 552 | local origin_win = vim.api.nvim_get_current_win() |
| 547 | 553 | if vim.fn.mode():match("^[vV\22]") then |
| 548 | 554 | vim.cmd("noautocmd normal! \27") |
| 549 | 555 | end |
| 556 | + local s = vim.fn.getpos("'<") | |
| 557 | + local e = vim.fn.getpos("'>") | |
| 550 | 558 | local sel = capture_visual_selection(origin_buf) |
| 551 | 559 | if sel == "" then |
| 552 | 560 | vim.notify("claude: empty selection", vim.log.levels.WARN) |
| 553 | 561 | return |
| 554 | 562 | end |
| 555 | 563 | local ft = vim.bo[origin_buf].filetype or "" |
| 556 | - local context = string.format("```%s\n%s\n```", ft, sel) | |
| 564 | + local path = buf_display_path(origin_buf) | |
| 565 | + local sl, sc = s[2], s[3] | |
| 566 | + local el, ec = e[2], e[3] | |
| 567 | + -- For linewise selections '> column is huge (MAXCOL); clamp to line len. | |
| 568 | + local last_line = vim.api.nvim_buf_get_lines(origin_buf, el - 1, el, false)[1] or "" | |
| 569 | + if ec > #last_line then ec = math.max(1, #last_line) end | |
| 570 | + local range = string.format("%s:%d:%d-%d:%d", path, sl, sc, el, ec) | |
| 571 | + local context = string.format("Selection from `%s`:\n```%s\n%s\n```", range, ft, sel) | |
| 557 | 572 | |
| 558 | 573 | ensure_panel_for_current_tab() |
| 559 | 574 | M.open_input({ return_to = origin_win, context = context }) |
+157-0claude-nvim/tests/visual_context_spec.lua
| @@ -0,0 +1,157 @@ | ||
| 1 | +-- <leader>p (visual mode -> ui.prompt_visual_chat) must include | |
| 2 | +-- file path + line:col-line:col range alongside the selected text in the | |
| 3 | +-- context sent to claude. | |
| 4 | + | |
| 5 | +local function reload() | |
| 6 | + package.loaded["claude.ui"] = nil | |
| 7 | + package.loaded["claude.runner"] = nil | |
| 8 | + package.loaded["claude.store"] = nil | |
| 9 | + return require("claude.ui"), require("claude.runner"), require("claude.store") | |
| 10 | +end | |
| 11 | + | |
| 12 | +describe("visual selection context", function() | |
| 13 | + local ui, runner | |
| 14 | + local tmp_cwd, orig_cwd, tmp_data, orig_stdpath | |
| 15 | + local orig_open_input | |
| 16 | + | |
| 17 | + before_each(function() | |
| 18 | + orig_cwd = vim.fn.getcwd() | |
| 19 | + tmp_cwd = vim.fn.tempname() | |
| 20 | + vim.fn.mkdir(tmp_cwd, "p") | |
| 21 | + vim.cmd("cd " .. vim.fn.fnameescape(tmp_cwd)) | |
| 22 | + | |
| 23 | + tmp_data = vim.fn.tempname() | |
| 24 | + vim.fn.mkdir(tmp_data, "p") | |
| 25 | + orig_stdpath = vim.fn.stdpath | |
| 26 | + vim.fn.stdpath = function(k) ---@diagnostic disable-line: duplicate-set-field | |
| 27 | + if k == "data" then return tmp_data end | |
| 28 | + return orig_stdpath(k) | |
| 29 | + end | |
| 30 | + | |
| 31 | + while vim.fn.tabpagenr("$") > 1 do vim.cmd("tabclose") end | |
| 32 | + | |
| 33 | + ui, runner = reload() | |
| 34 | + ui.setup({ width = 80, skip_permissions = false }) | |
| 35 | + runner.setup({ skip_permissions = false, debug = false }) | |
| 36 | + | |
| 37 | + orig_open_input = ui.open_input | |
| 38 | + end) | |
| 39 | + | |
| 40 | + after_each(function() | |
| 41 | + ui.open_input = orig_open_input | |
| 42 | + while vim.fn.tabpagenr("$") > 1 do vim.cmd("tabclose") end | |
| 43 | + vim.fn.stdpath = orig_stdpath | |
| 44 | + vim.cmd("cd " .. vim.fn.fnameescape(orig_cwd)) | |
| 45 | + vim.fn.delete(tmp_cwd, "rf") | |
| 46 | + vim.fn.delete(tmp_data, "rf") | |
| 47 | + end) | |
| 48 | + | |
| 49 | + local function setup_buffer(rel_name, lines, ft) | |
| 50 | + local fpath = tmp_cwd .. "/" .. rel_name | |
| 51 | + vim.cmd("edit " .. vim.fn.fnameescape(fpath)) | |
| 52 | + local buf = vim.api.nvim_get_current_buf() | |
| 53 | + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) | |
| 54 | + vim.bo[buf].filetype = ft | |
| 55 | + return buf | |
| 56 | + end | |
| 57 | + | |
| 58 | + it("linewise selection: context has filename, line range, ft fence, and selected lines", function() | |
| 59 | + setup_buffer("foo.lua", { | |
| 60 | + "local M = {}", | |
| 61 | + "function M.hello()", | |
| 62 | + " return 'hi'", | |
| 63 | + "end", | |
| 64 | + "return M", | |
| 65 | + }, "lua") | |
| 66 | + | |
| 67 | + -- Visual line select lines 2..4 | |
| 68 | + vim.cmd("normal! 2GVjj\27") | |
| 69 | + | |
| 70 | + local captured | |
| 71 | + ui.open_input = function(opts) captured = opts end ---@diagnostic disable-line: duplicate-set-field | |
| 72 | + | |
| 73 | + ui.prompt_visual_chat() | |
| 74 | + | |
| 75 | + assert.is_truthy(captured, "open_input should have been called") | |
| 76 | + local ctx = captured.context or "" | |
| 77 | + assert.matches("foo%.lua:2:1%-4:", ctx, "context should contain filename and line range") | |
| 78 | + assert.matches("```lua", ctx, "should fence with filetype") | |
| 79 | + assert.matches("function M%.hello%(%)", ctx, "selected text should appear") | |
| 80 | + assert.matches("return 'hi'", ctx) | |
| 81 | + assert.matches("end", ctx) | |
| 82 | + end) | |
| 83 | + | |
| 84 | + it("charwise selection: column boundaries reflect actual selection", function() | |
| 85 | + setup_buffer("bar.lua", { | |
| 86 | + "abcdef", | |
| 87 | + "ghijkl", | |
| 88 | + "mnopqr", | |
| 89 | + }, "lua") | |
| 90 | + | |
| 91 | + -- Char-visual from line 1 col 3 ('c') to line 2 col 4 ('j') | |
| 92 | + vim.cmd("normal! gg") | |
| 93 | + vim.cmd("normal! 0lvj0lll\27") | |
| 94 | + -- Set marks explicitly to be deterministic | |
| 95 | + vim.fn.setpos("'<", { 0, 1, 3, 0 }) | |
| 96 | + vim.fn.setpos("'>", { 0, 2, 4, 0 }) | |
| 97 | + | |
| 98 | + -- Force visualmode to charwise so capture_visual_selection picks the | |
| 99 | + -- column-aware path, not the linewise fallback. | |
| 100 | + local orig_vm = vim.fn.visualmode | |
| 101 | + vim.fn.visualmode = function() return "v" end ---@diagnostic disable-line: duplicate-set-field | |
| 102 | + | |
| 103 | + local captured | |
| 104 | + ui.open_input = function(opts) captured = opts end ---@diagnostic disable-line: duplicate-set-field | |
| 105 | + ui.prompt_visual_chat() | |
| 106 | + vim.fn.visualmode = orig_vm | |
| 107 | + | |
| 108 | + assert.is_truthy(captured) | |
| 109 | + local ctx = captured.context or "" | |
| 110 | + assert.matches("bar%.lua:1:3%-2:4", ctx, "context should reflect 1:3-2:4 range") | |
| 111 | + -- selection text spans "cdef" + newline + "ghij" | |
| 112 | + assert.matches("cdef\nghij", ctx, "selected chars should appear in fence") | |
| 113 | + end) | |
| 114 | + | |
| 115 | + it("buffer with no name falls back to [No Name]", function() | |
| 116 | + -- Don't create a backing file: just a scratch buf. | |
| 117 | + vim.cmd("enew") | |
| 118 | + local buf = vim.api.nvim_get_current_buf() | |
| 119 | + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "alpha", "beta", "gamma" }) | |
| 120 | + vim.bo[buf].filetype = "text" | |
| 121 | + | |
| 122 | + vim.cmd("normal! ggVj\27") | |
| 123 | + | |
| 124 | + local captured | |
| 125 | + ui.open_input = function(opts) captured = opts end ---@diagnostic disable-line: duplicate-set-field | |
| 126 | + ui.prompt_visual_chat() | |
| 127 | + | |
| 128 | + assert.is_truthy(captured) | |
| 129 | + assert.matches("%[No Name%]:1:1%-2:", captured.context or "") | |
| 130 | + end) | |
| 131 | + | |
| 132 | + it("send() concatenates context + user prompt and forwards to runner.send", function() | |
| 133 | + setup_buffer("baz.lua", { "x = 1", "y = 2", "z = 3" }, "lua") | |
| 134 | + vim.cmd("normal! ggVj\27") | |
| 135 | + | |
| 136 | + -- Use real open_input but stub runner.send so we can inspect final_text. | |
| 137 | + local sent | |
| 138 | + runner.send = function(_sid, text, _opts) ---@diagnostic disable-line: duplicate-set-field | |
| 139 | + sent = text | |
| 140 | + return true | |
| 141 | + end | |
| 142 | + | |
| 143 | + ui.prompt_visual_chat() | |
| 144 | + -- The floating prompt window is now open and is current. Type into it. | |
| 145 | + vim.api.nvim_put({ "What does this do?" }, "c", true, true) | |
| 146 | + -- Trigger <C-s> mapping to send. | |
| 147 | + vim.cmd("stopinsert") | |
| 148 | + vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes("<C-s>", true, false, true), "x", false) | |
| 149 | + | |
| 150 | + assert.is_truthy(sent, "runner.send should have been invoked") | |
| 151 | + assert.matches("baz%.lua:1:1%-2:", sent, "final text should contain file:line range") | |
| 152 | + assert.matches("```lua", sent) | |
| 153 | + assert.matches("x = 1", sent) | |
| 154 | + assert.matches("y = 2", sent) | |
| 155 | + assert.matches("What does this do%?", sent, "user-typed text should follow context") | |
| 156 | + end) | |
| 157 | +end) |
+1-0lazy-lock.json
| @@ -9,6 +9,7 @@ | ||
| 9 | 9 | "gitsigns.nvim": { "branch": "main", "commit": "20ad4419564d6e22b189f6738116b38871082332" }, |
| 10 | 10 | "grug-far.nvim": { "branch": "main", "commit": "b58b2d65863f4ebad88b10a1ddd519e5380466e0" }, |
| 11 | 11 | "hydra.nvim": { "branch": "main", "commit": "8c4a9f621ec7cdc30411a1f3b6d5eebb12b469dc" }, |
| 12 | + "image.nvim": { "branch": "master", "commit": "da2be65c153ba15a14a342b05591652a6df70d58" }, | |
| 12 | 13 | "kulala.nvim": { "branch": "main", "commit": "c328aeb219c4b77106917dd2698c90ea9657281b" }, |
| 13 | 14 | "lazy.nvim": { "branch": "main", "commit": "85c7ff3711b730b4030d03144f6db6375044ae82" }, |
| 14 | 15 | "leetcode.nvim": { "branch": "master", "commit": "fdd3f91800b3983e27bc9fcfb99cfa7293d7f11a" }, |
+7-1lua/keymaps.lua
| @@ -22,9 +22,14 @@ easymap("n", "<leader>,", ":Telescope current_folder<cr>", "Search Current Folde | ||
| 22 | 22 | easymap("n", "<leader>m", ':exe "resize ".float2nr(&lines*0.8)<cr>', "80% Window") |
| 23 | 23 | -- easymap("n", "<leader>s", ":Scratch<cr>", "Scratch") |
| 24 | 24 | easymap("n", "<leader>.", ":Telescope all_files<cr>", "All Files") |
| 25 | -easymap("n", "<leader>i", function() | |
| 25 | +easymap("n", "gri", function() | |
| 26 | 26 | vim.lsp.inlay_hint.enable(not vim.lsp.inlay_hint.is_enabled({ 0 }), { 0 }) |
| 27 | 27 | end, "Toggle Inlay Hints") |
| 28 | +easymap("n", "<leader>lt", function() | |
| 29 | + local on = vim.g.blink_cmp_enabled ~= false | |
| 30 | + vim.g.blink_cmp_enabled = not on | |
| 31 | + vim.notify("blink.cmp " .. (vim.g.blink_cmp_enabled and "on" or "off")) | |
| 32 | +end, "Toggle Completion") | |
| 28 | 33 | for i = 1, 9 do |
| 29 | 34 | easymap("n", "<leader>" .. i, i .. "gt", "Tab " .. i) |
| 30 | 35 | end |
| @@ -110,6 +115,7 @@ vim.keymap.set("n", "<C-S-Up>", ":resize +2<CR>") | ||
| 110 | 115 | -- -Gotos |
| 111 | 116 | -- ########################################################################## -- |
| 112 | 117 | easymap("n", "gd", vim.lsp.buf.definition, "Goto Definition") |
| 118 | +easymap("n", "gD", vim.lsp.buf.type_definition, "Goto Type Definition") | |
| 113 | 119 | easymap({ "n", "v" }, "gy", '"+y', "System Clipboard Copy") |
| 114 | 120 | easymap({ "n", "v" }, "gp", '"+p', "System Clipboard Paste") |
| 115 | 121 | easymap("n", "gs", "<cmd>EditCurrentFiletype<cr>", "Filetype Settings") |
+14-0lua/plugins.lua
| @@ -5,6 +5,20 @@ local plugins = { | ||
| 5 | 5 | { "ellisonleao/dotenv.nvim", opts = {} }, |
| 6 | 6 | { "folke/tokyonight.nvim" }, |
| 7 | 7 | { "mg979/vim-visual-multi" }, |
| 8 | + { | |
| 9 | + "3rd/image.nvim", | |
| 10 | + opts = { | |
| 11 | + backend = "sixel", | |
| 12 | + processor = "magick_cli", | |
| 13 | + integrations = { | |
| 14 | + markdown = { | |
| 15 | + enabled = true, | |
| 16 | + only_render_image_at_cursor = true, | |
| 17 | + only_render_image_at_cursor_mode = "popup", | |
| 18 | + }, | |
| 19 | + }, | |
| 20 | + }, | |
| 21 | + }, | |
| 8 | 22 | { |
| 9 | 23 | "mason-org/mason.nvim", |
| 10 | 24 | opts = {}, |
+3-0lua/plugins/blink.lua
| @@ -48,6 +48,9 @@ return { | ||
| 48 | 48 | }, |
| 49 | 49 | build = "cargo build --release", |
| 50 | 50 | opts = { |
| 51 | + enabled = function() | |
| 52 | + return vim.g.blink_cmp_enabled ~= false | |
| 53 | + end, | |
| 51 | 54 | cmdline = { enabled = false }, |
| 52 | 55 | sources = { |
| 53 | 56 | default = { "lsp", "buffer", "snippets", "path" }, |