aboutsummaryrefslogtreecommitdiff
path: root/lua/hobs
diff options
context:
space:
mode:
authorJosh Rahm <rahm@josher.dev>2026-08-11 11:19:05 -0600
committerJosh Rahm <rahm@josher.dev>2026-08-11 11:19:05 -0600
commitd7829c069b09d2115daa1aa33a729dd9ffee301e (patch)
tree4b4845c79c0df4b3879249e83ebbea44239f5cef /lua/hobs
parentd2eadb42ffde7f07b4c2d46efc3724d565ec8255 (diff)
downloadhobs.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.lua170
-rw-r--r--lua/hobs/prompt.lua39
-rw-r--r--lua/hobs/util.lua101
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