Skip to content
Open
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
45 changes: 45 additions & 0 deletions lua/jumpy/context-tools.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
local M = {}

function M.get_workspace_symbols(bufnr, callback)
local MAX_SYMBOLS = 200

local clients = vim.lsp.get_clients({ bufnr = bufnr })
local supports_workspace_symbols = vim.tbl_any(function(client)
return client:supports_method("workspace/symbol")
end, clients)

if not supports_workspace_symbols then
vim.notify("No attached LSP supports workspace/symbol", vim.log.levels.WARN)

callback("")
return
end

vim.lsp.buf_request(bufnr, "workspace/symbol", { query = "" }, function(err, result)
if err then
vim.notify("workspace/symbol request failed: " .. err.message, vim.log.levels.WARN)

callback("")
return
end

local out = {}
local kinds = vim.lsp.protocol.SymbolKind

for i, s in ipairs(result or {}) do
if i > MAX_SYMBOLS then
break
end

local path = vim.uri_to_fname(s.location.uri)
path = vim.fn.fnamemodify(path, ":.")

out[#out + 1] =
string.format("- %s [%s] > %s (%s)", s.name, kinds[s.kind] or "?", s.containerName or "global", path)
end

callback(table.concat(out, "\n"))
end)
end

return M
4 changes: 3 additions & 1 deletion lua/jumpy/llm.lua
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ end

local function build_messages(context)
local config = get_config()

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

Expand Down
160 changes: 108 additions & 52 deletions lua/jumpy/prompt.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
local M = {}

local context_tools = require("jumpy.context-tools")

local state = {
win = nil,
buf = nil,
Expand Down Expand Up @@ -50,6 +52,34 @@ function M.open()
state.buf, state.win = create_float(" jumpy ")

M._set_submit_keymap()
M._setup_completions(state.buf)
end

function M._setup_completions(buf)
vim.bo[buf].completeopt = "menu,menuone,noselect" -- QoL option so vim opens a completion dropdown instead of directly appending the first option

local completionItems = {
{ word = "@lsp", menu = "[jumpy]" },
}

vim.api.nvim_create_autocmd("TextChangedI", {
buffer = buf,
callback = function()
local line = vim.api.nvim_get_current_line()
local col = vim.fn.col(".")
local before = line:sub(1, col - 1)

local match = before:match("@%w*$")

if vim.fn.mode() == "i" then
if match then
vim.schedule(function()
vim.fn.complete(col - #match, completionItems)
end)
end
end
end,
})
end

function M.reprompt()
Expand All @@ -65,6 +95,7 @@ function M.reprompt()
state.buf, state.win = create_float(" jumpy: reprompt this hunk ")

M._set_submit_keymap()
M._setup_completions(state.buf)
end

function M._set_submit_keymap()
Expand All @@ -90,74 +121,99 @@ function M._submit()
return
end

M._close()

local source_buf = state.source_buf
local source_lines = vim.api.nvim_buf_get_lines(source_buf, 0, -1, false)
local filetype = vim.bo[source_buf].filetype
local reprompt_idx = state.reprompt_hunk_idx

local llm = require("jumpy.llm")

if reprompt_idx then
local render = require("jumpy.render")
local hunk_state = render.get_state(source_buf)
if not hunk_state or not hunk_state.hunks[reprompt_idx] then
vim.notify("jumpy: hunk no longer exists", vim.log.levels.WARN)
return
local function send_request(symbols)
symbols = symbols or ""

if symbols ~= "" then
symbols = "\n\n--- WORKSPACE SYMBOLS ---\n" .. symbols .. "\n--- END SYMBOLS ---"
end

local hunk = hunk_state.hunks[reprompt_idx]
local context = {
original_lines = hunk.removed_lines,
proposed_lines = hunk.added_lines,
prompt = prompt_text,
filetype = filetype,
}

llm.reprompt(context, function(new_lines)
vim.schedule(function()
local nav = require("jumpy.navigate")
nav.replace_hunk(source_buf, reprompt_idx, new_lines)
vim.notify("jumpy: hunk updated", vim.log.levels.INFO)
M._close()

if reprompt_idx then
local render = require("jumpy.render")
local hunk_state = render.get_state(source_buf)

if not hunk_state or not hunk_state.hunks[reprompt_idx] then
vim.notify("jumpy: hunk no longer exists", vim.log.levels.WARN)
return
end

local hunk = hunk_state.hunks[reprompt_idx]

local context = {
original_lines = hunk.removed_lines,
proposed_lines = hunk.added_lines,
prompt = prompt_text,
symbols = symbols,
filetype = filetype,
}

llm.reprompt(context, function(new_lines)
vim.schedule(function()
local nav = require("jumpy.navigate")

nav.replace_hunk(source_buf, reprompt_idx, new_lines)

vim.notify("jumpy: hunk updated", vim.log.levels.INFO)
end)
end)
end)
else
local context = {
file_contents = table.concat(source_lines, "\n"),
prompt = prompt_text,
filetype = filetype,
}

llm.request(context, function(response_text)
vim.schedule(function()
if not vim.api.nvim_buf_is_valid(source_buf) then
vim.notify("jumpy: buffer closed, discarding response", vim.log.levels.WARN)
return
end
else
local context = {
file_contents = table.concat(source_lines, "\n"),
prompt = prompt_text,
symbols = symbols,
filetype = filetype,
}

local diff = require("jumpy.diff")
local render = require("jumpy.render")
local patch = require("jumpy.patch")
llm.request(context, function(response_text)
vim.schedule(function()
if not vim.api.nvim_buf_is_valid(source_buf) then
vim.notify("jumpy: buffer closed, discarding response", vim.log.levels.WARN)
return
end

local proposed_lines, unmatched = patch.apply(source_lines, response_text)
if unmatched > 0 then
vim.notify(string.format("jumpy: %d block(s) could not be matched", unmatched), vim.log.levels.WARN)
end
local hunks = diff.compute(source_lines, proposed_lines)
local diff = require("jumpy.diff")
local render = require("jumpy.render")
local patch = require("jumpy.patch")

if #hunks == 0 then
vim.notify("jumpy: no changes proposed", vim.log.levels.INFO)
return
end
local proposed_lines, unmatched = patch.apply(source_lines, response_text)

if unmatched > 0 then
vim.notify(string.format("jumpy: %d block(s) could not be matched", unmatched), vim.log.levels.WARN)
end

render.show(source_buf, hunks, source_lines, proposed_lines)
vim.notify(string.format("jumpy: %d hunk(s) proposed", #hunks), vim.log.levels.INFO)
local hunks = diff.compute(source_lines, proposed_lines)

local nav = require("jumpy.navigate")
nav.next_hunk()
if #hunks == 0 then
vim.notify("jumpy: no changes proposed", vim.log.levels.INFO)
return
end

render.show(source_buf, hunks, source_lines, proposed_lines)

vim.notify(string.format("jumpy: %d hunk(s) proposed", #hunks), vim.log.levels.INFO)

local nav = require("jumpy.navigate")
nav.next_hunk()
end)
end)
end)
end
end

if prompt_text:find("@lsp") then
prompt_text = vim.trim(prompt_text:gsub("%f[%w@]@lsp%f[%W]", ""))

context_tools.get_workspace_symbols(tonumber(source_buf) or 0, send_request)
else
send_request("")
end
end

Expand Down