diff options
| author | Josh Rahm <rahm@josher.dev> | 2026-08-11 11:19:05 -0600 |
|---|---|---|
| committer | Josh Rahm <rahm@josher.dev> | 2026-08-11 11:19:05 -0600 |
| commit | d7829c069b09d2115daa1aa33a729dd9ffee301e (patch) | |
| tree | 4b4845c79c0df4b3879249e83ebbea44239f5cef /lua/hobs | |
| parent | d2eadb42ffde7f07b4c2d46efc3724d565ec8255 (diff) | |
| download | hobs.nvim-d7829c069b09d2115daa1aa33a729dd9ffee301e.tar.gz hobs.nvim-d7829c069b09d2115daa1aa33a729dd9ffee301e.tar.bz2 hobs.nvim-d7829c069b09d2115daa1aa33a729dd9ffee301e.zip | |
[refactor] break up some of the mega lua file.
Diffstat (limited to 'lua/hobs')
| -rw-r--r-- | lua/hobs/log.lua | 170 | ||||
| -rw-r--r-- | lua/hobs/prompt.lua | 39 | ||||
| -rw-r--r-- | lua/hobs/util.lua | 101 |
3 files changed, 310 insertions, 0 deletions
diff --git a/lua/hobs/log.lua b/lua/hobs/log.lua new file mode 100644 index 0000000..7371dfb --- /dev/null +++ b/lua/hobs/log.lua @@ -0,0 +1,170 @@ +local M = {} + +local ns = nil +local log_buf = nil +local log_ns = nil + +function M.init(hobs_ns) + ns = hobs_ns + log_ns = vim.api.nvim_create_namespace("hobs.nvim.log") + + vim.api.nvim_set_hl(log_ns, "HobLogTimestamp", { fg = "grey", bold = true }) + vim.api.nvim_set_hl(log_ns, "HobLogTagStarted", { fg = "#4ade80", bold = true }) + vim.api.nvim_set_hl(log_ns, "HobLogTagFinished", { fg = "#60a5fa", bold = true }) + vim.api.nvim_set_hl(log_ns, "HobLogTagFailed", { fg = "#f87171", bold = true }) + vim.api.nvim_set_hl(log_ns, "HobLogTagError", { fg = "#fb923c", bold = true }) + vim.api.nvim_set_hl(log_ns, "HobLogSed", { fg = "#a78bfa" }) + vim.api.nvim_set_hl(log_ns, "HobLogSeparator", { fg = "#71717a" }) +end + +local function get_log_buf() + if log_buf and vim.api.nvim_buf_is_valid(log_buf) then + return log_buf + end + + log_buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_name(log_buf, "hobs://log") + vim.bo[log_buf].buftype = "nofile" + vim.bo[log_buf].bufhidden = "hide" + vim.bo[log_buf].swapfile = false + vim.bo[log_buf].filetype = "log" + vim.bo[log_buf].modifiable = false + + return log_buf +end + +local function append_to_log(lines, hl_group) + local buf = get_log_buf() + local base_row = vim.api.nvim_buf_line_count(buf) + + vim.bo[buf].modifiable = true + vim.api.nvim_buf_set_lines(buf, -1, -1, false, lines) + vim.bo[buf].modifiable = false + + if hl_group then + local end_row = vim.api.nvim_buf_line_count(buf) - 1 + for row = base_row, end_row do + vim.api.nvim_buf_add_highlight(buf, log_ns, hl_group, row, 0, -1) + end + end +end + +local function timestamp() + return os.date("%Y-%m-%d %H:%M:%S") +end + +local function log_entry(tag, msg, hl_group) + local lines = vim.split(msg, "\n", { plain = true }) + + local prefix = string.format("[%s] %s:", timestamp(), tag) + lines[1] = prefix .. " " .. lines[1] + for i = 2, #lines do + lines[i] = "" .. lines[i] + end + + if hl_group and #lines == 1 then + local buf = get_log_buf() + local base_row = vim.api.nvim_buf_line_count(buf) + + vim.bo[buf].modifiable = true + vim.api.nvim_buf_set_lines(buf, -1, -1, false, lines) + + local tag_end_col = #prefix + vim.api.nvim_buf_add_highlight(buf, log_ns, "HobLogTimestamp", base_row, 0, tag_end_col) + vim.api.nvim_buf_add_highlight(buf, log_ns, hl_group, base_row, tag_end_col, -1) + + vim.bo[buf].modifiable = false + else + append_to_log(lines) + end +end + +function M.log_info(msg, hl_group) + append_to_log({ string.format("[%s] INFO: %s", timestamp(), msg) }, hl_group) +end + +function M.log_error(msg) + log_entry("ERROR", msg, "HobLogTagError") +end + +function M.log_hob_started(id, filename, prompt) + local filename_short = vim.fn.fnamemodify(filename, ":t") + local msg = string.format("Hob %d started | %s | %s", id, filename_short, prompt) + log_entry("HOB", msg, "HobLogTagStarted") +end + +function M.log_hob_working(id, status) + log_entry("HOB", string.format("Hob %d: %s", id, status)) +end + +function M.log_hob_sed(id, script) + local buf = get_log_buf() + + vim.bo[buf].modifiable = true + local base_row = vim.api.nvim_buf_line_count(buf) + + local separator_line = string.format("===== Hob %d sed script =====", id) + vim.api.nvim_buf_set_lines(buf, -1, -1, false, { separator_line }) + vim.api.nvim_buf_add_highlight(buf, log_ns, "HobLogSeparator", base_row, 0, -1) + + local sed_lines = vim.split(script, "\n", { plain = true }) + if #sed_lines > 0 then + vim.api.nvim_buf_set_lines(buf, -1, -1, false, sed_lines) + + for i = 1, #sed_lines do + vim.api.nvim_buf_add_highlight(buf, log_ns, "HobLogSed", base_row + i - 1, 0, -1) + end + end + + local end_sep_row = vim.api.nvim_buf_line_count(buf) - 1 + vim.api.nvim_buf_set_lines(buf, -1, -1, false, { "===== end sed script =====" }) + vim.api.nvim_buf_add_highlight(buf, log_ns, "HobLogSeparator", end_sep_row, 0, -1) + + vim.bo[buf].modifiable = false +end + +function M.log_hob_finished(id) + log_entry("HOB", string.format("Hob %d finished and applied edit", id), "HobLogTagFinished") +end + +function M.log_hob_failed(id, reason) + log_entry("HOB", string.format("Hob %d failed: %s", id, reason or "unknown error"), "HobLogTagFailed") +end + +function M.log_hob_edit_failed(id, reason) + log_entry("HOB", string.format("Hob %d edit failed: %s", id, reason or "unknown error"), "HobLogTagFailed") +end + +function M.open() + local buf = get_log_buf() + if not buf or not vim.api.nvim_buf_is_valid(buf) then + return + end + + local win = vim.api.nvim_get_current_win() + local win_height = vim.api.nvim_win_get_height(win) + + vim.cmd("botright split") + vim.api.nvim_win_set_buf(0, buf) + + local log_height = math.min(15, math.ceil(win_height * 0.4)) + vim.api.nvim_win_set_height(0, log_height) + vim.api.nvim_win_set_hl_ns(0, log_ns) + + local lines_count = vim.api.nvim_buf_line_count(buf) + if lines_count > 0 then + local col = vim.fn.min({ vim.api.nvim_win_get_width(0) - 1, vim.fn.strwidth(vim.api.nvim_get_current_line()) }) + vim.api.nvim_win_set_cursor(0, { lines_count, col }) + end + + vim.cmd("startinsert") +end + +function M.reset() + if log_buf and vim.api.nvim_buf_is_valid(log_buf) then + vim.api.nvim_buf_delete(log_buf, { force = true }) + log_buf = nil + end +end + +return M diff --git a/lua/hobs/prompt.lua b/lua/hobs/prompt.lua new file mode 100644 index 0000000..2a1d6a9 --- /dev/null +++ b/lua/hobs/prompt.lua @@ -0,0 +1,39 @@ +local M = {} + +function M.build(filename, selection, user_prompt) + return table.concat({ + "You are a coding agent working concurrently with a human in Neovim.", + "", + "The human selected this code and gave you a task.", + "The buffer may continue changing while you work.", + "", + "File: " .. filename, + string.format( + "Initial selection: %d:%d-%d:%d", + selection.display_start_line, + selection.display_start_col, + selection.display_end_line, + selection.display_end_col + ), + "", + "Selected text:", + "<selection>", + selection.text, + "</selection>", + "", + "Task:", + user_prompt, + "", + "IMPORTANT OUTPUT CONTRACT:", + "- Do not edit any files yourself.", + "- You may inspect the project as needed, but your final response must be ONLY a GNU sed script.", + "- The sed script will be applied to the CURRENT live contents of this Neovim buffer after you finish.", + "- Do not use absolute line-number addresses; the human may have inserted or removed lines while you were working.", + "- Address edits by distinctive source text/patterns and keep substitutions narrowly scoped.", + "- Prefer scripts that fail to change anything rather than matching unrelated code.", + "- Do not include Markdown fences, prose, explanations, or shell commands.", + "- Output only the contents of a sed -f program.", + }, "\n") +end + +return M
\ No newline at end of file diff --git a/lua/hobs/util.lua b/lua/hobs/util.lua new file mode 100644 index 0000000..5cda17c --- /dev/null +++ b/lua/hobs/util.lua @@ -0,0 +1,101 @@ +local M = {} + +--- +-- Validate that `search_lines` matches exactly once in `buffer_lines`. +-- Returns the start index (1-based) and end index (inclusive) of the match, +-- or errors if there is not exactly one match. +local function find_exact_match(buffer_lines, search_lines) + local search_text = table.concat(search_lines, "\n") + + local matches = {} + local search_len = #search_lines + + for i = 1, #buffer_lines - search_len + 1 do + local candidate = table.concat(vim.list_slice(buffer_lines, i, i + search_len - 1), "\n") + if candidate == search_text then + table.insert(matches, { start = i, end_ = i + search_len - 1 }) + end + end + + if #matches == 0 then + error("search text not found in buffer") + elseif #matches > 1 then + error("search text matches multiple times") + end + + return matches[1].start, matches[1].end_ +end + +--- +-- Insert `lines` before the lines matched by `search_lines`. +-- Returns a new table of lines with the insertion applied. +-- Errors if `search_lines` does not match exactly once. +function M.insert_before(buffer_lines, search_lines, insert_lines) + local start, end_ = find_exact_match(buffer_lines, search_lines) + + local result = {} + + for i = 1, start - 1 do + table.insert(result, buffer_lines[i]) + end + + for _, line in ipairs(insert_lines) do + table.insert(result, line) + end + + for i = start, #buffer_lines do + table.insert(result, buffer_lines[i]) + end + + return result +end + +--- +-- Append `lines` after the lines matched by `search_lines`. +-- Returns a new table of lines with the append applied. +-- Errors if `search_lines` does not match exactly once. +function M.append_after(buffer_lines, search_lines, append_lines) + local start, end_ = find_exact_match(buffer_lines, search_lines) + + local result = {} + + for i = 1, end_ do + table.insert(result, buffer_lines[i]) + end + + for _, line in ipairs(append_lines) do + table.insert(result, line) + end + + for i = end_ + 1, #buffer_lines do + table.insert(result, buffer_lines[i]) + end + + return result +end + +--- +-- Replace the lines matched by `search_lines` with `replace_lines`. +-- Returns a new table of lines with the replacement applied. +-- Errors if `search_lines` does not match exactly once. +function M.replace(buffer_lines, search_lines, replace_lines) + local start, end_ = find_exact_match(buffer_lines, search_lines) + + local result = {} + + for i = 1, start - 1 do + table.insert(result, buffer_lines[i]) + end + + for _, line in ipairs(replace_lines) do + table.insert(result, line) + end + + for i = end_ + 1, #buffer_lines do + table.insert(result, buffer_lines[i]) + end + + return result +end + +return M |