summaryrefslogtreecommitdiff
path: root/lua
diff options
context:
space:
mode:
Diffstat (limited to 'lua')
-rw-r--r--lua/99/init.lua58
-rw-r--r--lua/99/ops/qfix-helpers.lua22
-rw-r--r--lua/99/ops/vibe.lua2
-rw-r--r--lua/99/prompt.lua7
-rw-r--r--lua/99/state.lua11
-rw-r--r--lua/99/test/qfix_helpers_spec.lua39
-rw-r--r--lua/99/test/vibe_search_spec.lua56
-rw-r--r--lua/99/window/init.lua40
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()