Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The main difference between jumpy and other tools, like 99, are:
#

- **inline prompt**: describe a change without leaving the buffer
- **selection-scoped edits**: visually select a function (or any region) and prompt — the whole buffer is still sent as context so the model understands the surroundings, while it focuses on your selection
- **multi-line prompt + history**: `<C-CR>` submits, `<CR>` adds a newline, `<Up>`/`<Down>` recall earlier prompts
- **async & parallel prompts**: requests run in the background; each response lands in its own buffer (`:JumpyCancel` aborts everything in flight)
- **multi-file prompts**: `@mention` several files in one prompt and review the cross-file hunks
Expand Down
9 changes: 5 additions & 4 deletions lua/jumpy/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ function M._setup_highlights()
hl(0, "JumpyRemoved", { bg = "#3a1a1a", strikethrough = true, default = true })
hl(0, "JumpyAddedSign", { fg = "#4ec94e", default = true })
hl(0, "JumpyRemovedSign", { fg = "#e05252", default = true })
-- Inspect overlay: a read-only, yellow before/after view of a recorded hunk.
hl(0, "JumpyInspect", { bg = "#3a3320", default = true })
hl(0, "JumpyInspectSign", { fg = "#d7b95a", default = true })
hl(0, "JumpyInspectOther", { fg = "#8a7a3a", italic = true, default = true })
Expand All @@ -149,8 +148,10 @@ function M._setup_keymaps()
local opts = { silent = true }
local c = M.config.keymaps

-- The prompt is also bound in visual mode ("x") so you can select a region
-- and scope the edit to it; everything else is normal-mode only.
local keymaps = {
{ c.prompt, "jumpy.prompt", "open" },
{ c.prompt, "jumpy.prompt", "open", { "n", "x" } },
{ c.next_hunk, "jumpy.navigate", "next_hunk" },
{ c.prev_hunk, "jumpy.navigate", "prev_hunk" },
{ c.accept, "jumpy.navigate", "accept" },
Expand All @@ -163,8 +164,8 @@ function M._setup_keymaps()
}

for _, km in ipairs(keymaps) do
local key, mod, fn = km[1], km[2], km[3]
map("n", key, function()
local key, mod, fn, modes = km[1], km[2], km[3], km[4] or "n"
map(modes, key, function()
require(mod)[fn]()
end, opts)
end
Expand Down
20 changes: 19 additions & 1 deletion lua/jumpy/llm.lua
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,29 @@ local function build_messages(context)
}
end

-- A visual selection marks the region to focus on. The full file is still
-- sent for context; the model is asked to prefer changing only the selection.
local selection_note = ""
local sel = context.selection
if sel and sel.lines and #sel.lines > 0 then
selection_note = string.format(
"\n\nFocus on the SELECTED region below (lines %d-%d). Use the rest of the "
.. "file as context and prefer to change only the selection:\n"
.. "--- SELECTION (lines %d-%d) ---\n%s\n--- END SELECTION ---",
sel.start_line or 0,
sel.end_line or 0,
sel.start_line or 0,
sel.end_line or 0,
table.concat(sel.lines, "\n")
)
end

local user_content = string.format(
"File type: %s\n\n--- FILE CONTENTS ---\n%s\n--- END FILE ---%s\n\nInstruction: %s",
"File type: %s\n\n--- FILE CONTENTS ---\n%s\n--- END FILE ---%s%s\n\nInstruction: %s",
context.filetype or "text",
context.file_contents,
context.symbols,
selection_note,
context.prompt
)

Expand Down
30 changes: 17 additions & 13 deletions lua/jumpy/prompt.lua
Original file line number Diff line number Diff line change
Expand Up @@ -399,8 +399,18 @@ function M._submit()
local source_buf = state.source_buf
local visual_selection = state.visual_selection

local source_lines = visual_selection and vim.split(visual_selection.text, "\n", { plain = true })
or vim.api.nvim_buf_get_lines(source_buf, 0, -1, false)
-- Always send the whole buffer so the model has surrounding context; a visual
-- selection just marks the region to focus on (see `selection` in the context
-- and jumpy.llm). Edits are patched against the full buffer.
local source_lines = vim.api.nvim_buf_get_lines(source_buf, 0, -1, false)

local selection = visual_selection
and {
start_line = visual_selection.start_line,
end_line = visual_selection.end_line,
lines = vim.split(visual_selection.text, "\n", { plain = true }),
}
or nil

local source_name = vim.api.nvim_buf_get_name(source_buf)
local source_rel = source_name ~= "" and path.rel_path(source_name, path.project_root()) or "current"
Expand Down Expand Up @@ -582,6 +592,7 @@ function M._submit()
prompt = cleaned_prompt,
symbols = symbols,
filetype = filetype,
selection = selection,
}

llm.request(context, function(response_text)
Expand All @@ -595,10 +606,10 @@ function M._submit()
local render = require("jumpy.render")
local patch = require("jumpy.patch")

-- Full-buffer prompts re-read the buffer so parallel work done in
-- the meantime is respected; visual-selection prompts keep the
-- snapshot since they target a fixed region.
local original = visual_selection and source_lines or vim.api.nvim_buf_get_lines(source_buf, 0, -1, false)
-- Re-read the buffer so parallel work done while the request was in
-- flight is respected. A selection only guided the model; the patch
-- and resulting hunks are computed against the whole buffer.
local original = vim.api.nvim_buf_get_lines(source_buf, 0, -1, false)

local proposed_lines, unmatched = patch.apply(original, response_text)

Expand All @@ -607,13 +618,6 @@ function M._submit()
end

local hunks = diff.compute(original, proposed_lines)
if visual_selection then
local offset = visual_selection.start_line - 1

for _, hunk in ipairs(hunks) do
hunk.old_start = hunk.old_start + offset
end
end

if #hunks == 0 then
vim.notify("jumpy: no changes proposed", vim.log.levels.INFO)
Expand Down
36 changes: 36 additions & 0 deletions tests/llm_spec.lua
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,42 @@ describe("llm Claude Code command", function()
end)
end)

describe("llm single-file selection", function()
it("sends the whole file plus a marked selection region", function()
local msgs = llm._build_messages({
filetype = "lua",
file_contents = "local a = 1\nlocal b = 2\nlocal c = 3",
symbols = "",
prompt = "rename b",
selection = {
start_line = 2,
end_line = 2,
lines = { "local b = 2" },
},
})

local user = msgs[2].content
-- full file is present as context
assert.is_truthy(user:find("local a = 1", 1, true))
assert.is_truthy(user:find("local c = 3", 1, true))
-- the selection is called out with its line range
assert.is_truthy(user:find("--- SELECTION (lines 2-2) ---", 1, true))
assert.is_truthy(user:find("Focus on the SELECTED region", 1, true))
assert.is_truthy(user:find("Instruction: rename b", 1, true))
end)

it("omits the selection note when there is no selection", function()
local msgs = llm._build_messages({
filetype = "lua",
file_contents = "local a = 1",
symbols = "",
prompt = "do it",
})

assert.is_nil(msgs[2].content:find("SELECTION", 1, true))
end)
end)

describe("llm multi-file messages", function()
it("names the current file so relative references resolve", function()
local msgs = llm._build_messages({
Expand Down
Loading