diff options
Diffstat (limited to 'lua')
| -rw-r--r-- | lua/99/init.lua | 58 | ||||
| -rw-r--r-- | lua/99/ops/qfix-helpers.lua | 22 | ||||
| -rw-r--r-- | lua/99/ops/vibe.lua | 2 | ||||
| -rw-r--r-- | lua/99/prompt.lua | 7 | ||||
| -rw-r--r-- | lua/99/state.lua | 11 | ||||
| -rw-r--r-- | lua/99/test/qfix_helpers_spec.lua | 39 | ||||
| -rw-r--r-- | lua/99/test/vibe_search_spec.lua | 56 | ||||
| -rw-r--r-- | lua/99/window/init.lua | 40 |
8 files changed, 193 insertions, 42 deletions
diff --git a/lua/99/init.lua b/lua/99/init.lua index bdf35e0..17eeb06 100644 --- a/lua/99/init.lua +++ b/lua/99/init.lua @@ -186,6 +186,8 @@ local _99_state --- @field search fun(opts: _99.ops.SearchOpts): _99.TraceID --- Performs a search across your project with the prompt you provide and return out a list of --- locations with notes that will be put into your quick fix list. +--- @field vibe_search fun(opts?: _99.ops.Opts): _99.TraceID | nil +--- Select a previous search, edit it, then pass it to vibe. --- @field visual fun(opts: _99.ops.Opts): _99.TraceID --- takes your current selection and sends that along with the prompt provided and replaces --- your visual selection with the results @@ -235,6 +237,7 @@ local function capture_prompt(cb, name, context, opts, capture_content) table.insert(opts.additional_rules, r) end opts.additional_prompt = response + context.user_prompt = response cb(context, opts) end, on_load = function() @@ -277,7 +280,7 @@ function _99.open_tutorial(xid, opts) opts = opts or { split_direction = "vertical" } if xid == nil then --- @type _99.Prompt.Data.Tutorial[] - local tutorials = _99_state:get_request_data_by_type("tutorial") + local tutorials = _99_state:request_by_type("tutorial") if #tutorials == 0 then print("no tutorials available") return @@ -319,6 +322,7 @@ function _99.vibe(opts) local o = process_opts(opts) local context = Prompt.vibe(_99_state) if o.additional_prompt then + context.user_prompt = o.additional_prompt ops.vibe(context, o) else capture_prompt(ops.vibe, "Vibe", context, o) @@ -332,6 +336,7 @@ function _99.search(opts) local o = process_opts(opts) --[[ @as _99.ops.SearchOpts ]] local context = Prompt.search(_99_state) if o.additional_prompt then + context.user_prompt = o.additional_prompt ops.search(context, o) else capture_prompt(ops.search, "Search", context, o) @@ -339,6 +344,53 @@ function _99.search(opts) return context.xid end +--- @param opts? _99.ops.Opts +--- @return _99.TraceID | nil +function _99.vibe_search(opts) + local searches = _99_state:request_by_type("search") + if #searches == 0 then + error("no previous search results") + return nil + end + + local o = process_opts(opts) + local lines = {} + for i, context in ipairs(searches) do + table.insert( + lines, + string.format("%d: %s", i, context:summary()) + ) + end + + Window.capture_select_input("Select Search", { + content = lines, + cb = function(ok, result) + if not ok then + return + end + + local idx = tonumber(string.match(result, "^(%d+)%:")) + local selected = idx and searches[idx] or nil + if not selected then + print("failed to select search result") + return + end + + local selected_data = selected:search_data() + local context = Prompt.vibe(_99_state) + capture_prompt( + ops.vibe, + "Vibe", + context, + o, + vim.split(selected_data.response, "\n") + ) + end, + }) + + return nil +end + --- @param opts _99.ops.Opts function _99.tutorial(opts) opts = process_opts(opts) @@ -356,6 +408,7 @@ function _99.visual(opts) opts = process_opts(opts) local context = Prompt.visual(_99_state) if opts.additional_prompt then + context.user_prompt = opts.additional_prompt ops.over_range(context, opts) else capture_prompt(ops.over_range, "Visual", context, opts) @@ -416,13 +469,14 @@ function _99.clear_all_marks() _99_state.__active_marks = {} end ---- @param xid number | nil +--- @param xid number function _99.qfix(xid) --- @type _99.Prompt local entry = _99_state.__request_by_id[xid] assert(entry, "qfix_search_results could not find id: " .. xid) local items = entry:qfix_data() + Logger:set_id(xid):debug("qfix items retrieved", "items", items) vim.fn.setqflist({}, "r", { title = "99 Results", items = items }) vim.cmd("copen") end diff --git a/lua/99/ops/qfix-helpers.lua b/lua/99/ops/qfix-helpers.lua index 8d7ee63..9e55314 100644 --- a/lua/99/ops/qfix-helpers.lua +++ b/lua/99/ops/qfix-helpers.lua @@ -2,25 +2,23 @@ local M = {} --- @return _99.Search.Result | nil function M.parse_line(line) - local parts = vim.split(line, ":", { plain = true }) - if #parts ~= 3 then + local filepath, lnum_raw, rest = line:match("^(.-):([^:]+):(.+)$") + if not filepath or not lnum_raw or not rest then return nil end - local filepath = parts[1] - local lnum = parts[2] - local comma_parts = vim.split(parts[3], ",", { plain = true }) - local col = comma_parts[1] - local notes = nil - - if #comma_parts >= 2 then - notes = table.concat(comma_parts, ",", 2) + local col_raw, _, notes = rest:match("^([^,]+),([^,]+),?(.*)$") + if not col_raw then + return nil end + local lnum = tonumber(lnum_raw) or 1 + local col = tonumber(col_raw) or 1 + return { filename = filepath, - lnum = tonumber(lnum) or 1, - col = tonumber(col) or 1, + lnum = lnum, + col = col, text = notes or "", } end diff --git a/lua/99/ops/vibe.lua b/lua/99/ops/vibe.lua index 62b40b9..9a64b78 100644 --- a/lua/99/ops/vibe.lua +++ b/lua/99/ops/vibe.lua @@ -30,7 +30,7 @@ end --- @field text string --- @param context _99.Prompt ----@param opts _99.ops.SearchOpts +---@param opts _99.ops.Opts local function vibe(context, opts) opts = opts or {} diff --git a/lua/99/prompt.lua b/lua/99/prompt.lua index e832a34..384dbde 100644 --- a/lua/99/prompt.lua +++ b/lua/99/prompt.lua @@ -53,6 +53,7 @@ local filetype_map = { --- @class _99.Prompt --- @field md_file_names string[] --- @field model string +--- @field user_prompt string --- @field operation _99.Prompt.Operation --- @field state _99.Prompt.State --- @field full_path string @@ -84,6 +85,7 @@ local function set_defaults(context, _99) context.state = "ready" context._99 = _99 + context.user_prompt = "" context.clean_ups = {} context.md_file_names = copy(_99.md_files) context.model = _99.model @@ -153,6 +155,11 @@ function Prompt.visual(_99) return context end +--- @return string +function Prompt:summary() + return string.format("%s: %s", self.operation, self.user_prompt) +end + --- @param _99 _99.State --- @return _99.Prompt function Prompt.tutorial(_99) diff --git a/lua/99/state.lua b/lua/99/state.lua index d472117..ae7fc8c 100644 --- a/lua/99/state.lua +++ b/lua/99/state.lua @@ -151,13 +151,12 @@ function State:active_request_count() end --- @param type "search" | "visual" | "tutorial" ---- @return _99.Prompt.Data -function State:get_request_data_by_type(type) - local out = {} +--- @return _99.Prompt[] +function State:request_by_type(type) + local out = {} --[[ @as _99.Prompt[] ]] for _, r in ipairs(self.__request_history) do - local data = r.data - if data and data.type == type then - table.insert(out, data) + if r.operation == type then + table.insert(out, r) end end return out diff --git a/lua/99/test/qfix_helpers_spec.lua b/lua/99/test/qfix_helpers_spec.lua index ad9e5ab..f8c9ac7 100644 --- a/lua/99/test/qfix_helpers_spec.lua +++ b/lua/99/test/qfix_helpers_spec.lua @@ -4,9 +4,10 @@ local eq = assert.are.same local create_entries = QFixHelpers.create_qfix_entries describe("qfix helpers", function() - it("parse_line parses filename, line, column, and notes", function() - local parsed = - QFixHelpers.parse_line("lua/99/ops/search.lua:42:7,found semantic search") + it("parse_line parses filename, line, column, range, and notes", function() + local parsed = QFixHelpers.parse_line( + "lua/99/ops/search.lua:42:7,3,found semantic search" + ) eq({ filename = "lua/99/ops/search.lua", @@ -17,8 +18,9 @@ describe("qfix helpers", function() end) it("parse_line keeps commas in notes text", function() - local parsed = QFixHelpers.parse_line("file.lua:10:3,note,with,commas") + local parsed = QFixHelpers.parse_line("file.lua:10:3,1,note,with,commas") + assert(parsed) eq("file.lua", parsed.filename) eq(10, parsed.lnum) eq(3, parsed.col) @@ -28,30 +30,25 @@ describe("qfix helpers", function() it("parse_line returns nil for malformed lines", function() eq(nil, QFixHelpers.parse_line("file.lua:10")) eq(nil, QFixHelpers.parse_line("file.lua:10:3:extra")) + eq(nil, QFixHelpers.parse_line("file.lua:10:3")) end) - it( - "parse_line uses defaults for invalid numbers and missing notes", - function() - local parsed = QFixHelpers.parse_line("file.lua:notnum:notcol") + it("parse_line keeps colons in notes text", function() + local parsed = + QFixHelpers.parse_line("file.lua:10:3,2,check this: important section") - eq({ - filename = "file.lua", - lnum = 1, - col = 1, - text = "", - }, parsed) - end - ) + assert(parsed) + eq("check this: important section", parsed.text) + end) it( "create_qfix_entries parses valid lines and skips malformed ones", function() local response = table.concat({ - "a.lua:1:2,first hit", + "a.lua:1:2,4,first hit", "not a valid line", - "b.lua:3:4", - "c.lua:notnum:notcol,fallback values", + "b.lua:3:4,1,", + "c.lua:5:7,2,fallback values", "", }, "\n") @@ -72,8 +69,8 @@ describe("qfix helpers", function() }, { filename = "c.lua", - lnum = 1, - col = 1, + lnum = 5, + col = 7, text = "fallback values", }, }, locations) diff --git a/lua/99/test/vibe_search_spec.lua b/lua/99/test/vibe_search_spec.lua new file mode 100644 index 0000000..f3f13ae --- /dev/null +++ b/lua/99/test/vibe_search_spec.lua @@ -0,0 +1,56 @@ +-- luacheck: globals describe it assert before_each after_each +local _99 = require("99") +local Window = require("99.window") +local ops = require("99.ops") +local test_utils = require("99.test.test_utils") +local eq = assert.are.same + +describe("vibe_search", function() + local provider + local previous_capture_input + local previous_capture_select_input + local previous_vibe + + before_each(function() + provider = test_utils.TestProvider.new() + _99.setup(test_utils.get_test_setup_options({}, provider)) + + previous_capture_input = Window.capture_input + previous_capture_select_input = Window.capture_select_input + previous_vibe = ops.vibe + end) + + after_each(function() + Window.capture_input = previous_capture_input + Window.capture_select_input = previous_capture_select_input + ops.vibe = previous_vibe + end) + + it("selects a previous search and passes edited output to vibe", function() + _99.search({ + additional_prompt = "find error handling", + }) + provider:resolve("success", "/tmp/foo.lua:1:1,search note") + test_utils.next_frame() + + local selected_content + Window.capture_select_input = function(_, opts) + opts.cb(true, opts.content[1]) + end + + Window.capture_input = function(_, opts) + selected_content = opts.content + opts.cb(true, "extra vibe context") + end + + local vibe_prompt + ops.vibe = function(_, opts) + vibe_prompt = opts.additional_prompt + end + + _99.vibe_search() + + eq({ "/tmp/foo.lua:1:1,search note" }, selected_content) + eq("extra vibe context", vibe_prompt) + end) +end) diff --git a/lua/99/window/init.lua b/lua/99/window/init.lua index f546fac..47f6f1c 100644 --- a/lua/99/window/init.lua +++ b/lua/99/window/init.lua @@ -445,6 +445,46 @@ function M.capture_input(name, opts) ensure_no_new_lines(opts.content) ) end + + return win +end + +--- @param name string +--- @param opts _99.window.CaptureInputOpts +function M.capture_select_input(name, opts) + local win + win = M.capture_input(name, { + content = opts.content, + rules = opts.rules, + cb = function(success, result) + if not success then + opts.cb(false, result) + end + end, + on_load = function() + vim.bo[win.buf_id].modifiable = false + vim.bo[win.buf_id].readonly = true + if opts.on_load then + opts.on_load() + end + end, + }) + + vim.keymap.set("n", "<CR>", function() + if not nvim_win_is_valid(win.win_id) then + return + end + + local cursor = vim.api.nvim_win_get_cursor(win.win_id) + local line = vim.api.nvim_buf_get_lines( + win.buf_id, + cursor[1] - 1, + cursor[1], + false + )[1] or "" + M.clear_active_popups() + opts.cb(true, line) + end, { buffer = win.buf_id, nowait = true }) end function M.clear_active_popups() |
