diff options
| author | ThePrimeAgain <theprimeagain@theprimeagain.com> | 2026-01-15 11:22:01 -0700 |
|---|---|---|
| committer | ThePrimeAgain <theprimeagain@theprimeagain.com> | 2026-01-15 11:22:01 -0700 |
| commit | fc2ce13fcc6454fa12ee1015f921611cd2edc4fa (patch) | |
| tree | dc2cc7817d012eff49d8a902f8d083e73e98bf59 | |
| parent | 4e24d093d50177e299823703759a5e276d198170 (diff) | |
| download | a4-fc2ce13fcc6454fa12ee1015f921611cd2edc4fa.tar.xz a4-fc2ce13fcc6454fa12ee1015f921611cd2edc4fa.zip | |
autocomplete appearing to work
| -rw-r--r-- | lua/99/extensions/agents/helpers.lua (renamed from lua/99/agents/helpers.lua) | 4 | ||||
| -rw-r--r-- | lua/99/extensions/agents/init.lua (renamed from lua/99/agents/init.lua) | 19 | ||||
| -rw-r--r-- | lua/99/extensions/cmp.lua | 76 | ||||
| -rw-r--r-- | lua/99/extensions/init.lua | 38 | ||||
| -rw-r--r-- | lua/99/init.lua | 75 | ||||
| -rw-r--r-- | lua/99/window/init.lua | 19 | ||||
| -rw-r--r-- | scratch/custom_rules/back-end.md | 0 | ||||
| -rw-r--r-- | scratch/custom_rules/foo.md | 2 | ||||
| -rw-r--r-- | scratch/custom_rules/front-end.md | 0 | ||||
| -rw-r--r-- | scratch/custom_rules/vim.lsp.md | 0 | ||||
| -rw-r--r-- | scratch/custom_rules/vim.md | 0 | ||||
| -rw-r--r-- | scratch/custom_rules/vim.treesitter.md | 0 | ||||
| -rw-r--r-- | scratch/refresh.lua | 146 |
13 files changed, 173 insertions, 206 deletions
diff --git a/lua/99/agents/helpers.lua b/lua/99/extensions/agents/helpers.lua index a5000cb..1f4ed01 100644 --- a/lua/99/agents/helpers.lua +++ b/lua/99/extensions/agents/helpers.lua @@ -3,8 +3,8 @@ local M = {} --- @param dir string --- @return _99.Agents.Rule[] function M.ls(dir) - local cursor_rules_dir = vim.uv.cwd() .. dir - local files = vim.fn.glob(cursor_rules_dir .. "/*.{mdc,md}", false, true) + local cwd = vim.fs.joinpath(vim.uv.cwd(), dir) + local files = vim.fn.glob(cwd .. "/*.{mdc,md}", false, true) local rules = {} for _, file in ipairs(files) do diff --git a/lua/99/agents/init.lua b/lua/99/extensions/agents/init.lua index be43727..f594802 100644 --- a/lua/99/agents/init.lua +++ b/lua/99/extensions/agents/init.lua @@ -1,4 +1,4 @@ -local helpers = require("99.agents.helpers") +local helpers = require("99.extensions.agents.helpers") local M = {} --- @class _99.Agents.Rule @@ -17,7 +17,7 @@ local M = {} function M.rules(_99) local cursor = helpers.ls(".cursor/rules") local custom = {} - for _, path in ipairs(_99.custom_rules) do + for _, path in ipairs(_99.completion.custom_rules or {}) do local c = helpers.ls(path) table.insert(custom, c) end @@ -27,4 +27,19 @@ function M.rules(_99) } end +--- @param rules _99.Agents.Rules +--- @return _99.Agents.Rule[] +function M.rules_to_items(rules) + local items = {} + for _, rule in ipairs(rules.cursor or {}) do + table.insert(items, rule) + end + for _, custom_rules in ipairs(rules.custom or {}) do + for _, rule in ipairs(custom_rules) do + table.insert(items, rule) + end + end + return items +end + return M diff --git a/lua/99/extensions/cmp.lua b/lua/99/extensions/cmp.lua index e55ae36..30e5d2c 100644 --- a/lua/99/extensions/cmp.lua +++ b/lua/99/extensions/cmp.lua @@ -1,14 +1,24 @@ +local Agents = require("99.extensions.agents") +local SOURCE = "99" + +--- @param _99 _99.State +--- @return _99.Agents.Rule[] +local function rules(_99) + return Agents.rules_to_items(Agents.rules(_99)) +end + --- @class CmpSource --- @field _99 _99.State ---- @field items string[] +--- @field items _99.Agents.Rule[] local CmpSource = {} CmpSource.__index = CmpSource -local SOURCE = "99" - --- @param _99 _99.State function CmpSource.new(_99) - return setmetatable({}, CmpSource) + return setmetatable({ + _99 = _99, + items = rules(_99), + }, CmpSource) end function CmpSource:is_available() @@ -39,41 +49,39 @@ end -- true: I might return more if user types more -- false: this result set is complete function CmpSource:complete(params, callback) - local cmp = require("cmp") local before = params.context.cursor_before_line or "" - local prefix = before:match("(%w+)$") or "" - local items = {} --[[ @as CompletionItem[] ]] - print("complete: context", vim.inspect(params.context)) + if #before > 1 and before:sub(#before - 1) ~= " @" then + callback({ + items = {}, + isIncomplete = false, + }) + return + end + + for _, item in ipairs(self.items) do + table.insert(items, { + label = item.name, + insertText = item.path, + filterText = item.name, + kind = 17, -- file + -- documentation = "here is the documentation and everything associated with it", + -- detail = "detail: right side hint", + }) + end callback({ items = items, - isIncomplete = true, + isIncomplete = false, }) end --- resolve(completion_item, callback) (optional) --- Some sources return lightweight items first, then fill in heavy fields --- only when the user selects an item. --- --- For example: --- - fetch docs lazily --- - compute expensive detail text --- --- If you don’t need it, omit it. +--- TODO: Look into what this could be function CmpSource:resolve(completion_item, callback) - -- You can modify completion_item here. callback(completion_item) end --- execute(completion_item, callback) (optional) --- Called when the item is confirmed, if the item contains an "command" field --- or if your source wants to perform side-effects. --- --- Examples: --- - insert import statements (usually via additionalTextEdits instead) --- - open a snippet, run something, etc. function CmpSource:execute(completion_item, callback) callback(completion_item) end @@ -86,24 +94,32 @@ local function init_for_buffer(_99) local cmp = require("cmp") cmp.setup.buffer({ sources = { - { name = "my_source" }, + { name = SOURCE }, }, }) end --- @param _99 _99.State local function init(_99) - assert(source == nil, "the source must be nil when calling init on an completer") + assert( + source == nil, + "the source must be nil when calling init on an completer" + ) local cmp = require("cmp") source = CmpSource.new(_99) cmp.register_source(SOURCE, source) end -local function refresh_state(_99) end +--- @param _99 _99.State +local function refresh_state(_99) + source.items = rules(_99) +end -return { +--- @type _99.Extensions.Source +local source_wrapper = { init_for_buffer = init_for_buffer, init = init, refresh_state = refresh_state, } +return source_wrapper diff --git a/lua/99/extensions/init.lua b/lua/99/extensions/init.lua index 94190d6..ceb63a5 100644 --- a/lua/99/extensions/init.lua +++ b/lua/99/extensions/init.lua @@ -1,5 +1,37 @@ -local M = {} +local cmp = require("99.extensions.cmp") ---- @param _99 _99.State -function M.init_buffer(_99) +--- @class _99.Extensions.Source +--- @field init_for_buffer fun(_99: _99.State): nil +--- @field init fun(_99: _99.State): nil +--- @field refresh_state fun(_99: _99.State): nil + +--- @param completion _99.Completion | nil +--- @return _99.Extensions.Source | nil +local function get_source(completion) + if not completion or not completion.source then + return + end + local source = completion.source + if source == "cmp" then + return cmp + end end + +return { + --- @param _99 _99.State + init = function(_99) + local source = get_source(_99.completion) + if not source then + return + end + source.init(_99) + end, + + setup_buffer = function(_99) + local source = get_source(_99.completion) + if not source then + return + end + source.init_for_buffer(_99) + end, +} diff --git a/lua/99/init.lua b/lua/99/init.lua index 3faa6e2..a677d6b 100644 --- a/lua/99/init.lua +++ b/lua/99/init.lua @@ -6,6 +6,7 @@ local Window = require("99.window") local get_id = require("99.id") local RequestContext = require("99.request-context") local Range = require("99.geo").Range +local Extensions = require("99.extensions") --- @alias _99.Cleanup fun(): nil @@ -34,6 +35,10 @@ local function create_99_state() } end +--- @class _99.Completion +--- @field source "cmp" | nil +--- @field custom_rules string[] | nil + --- @class _99.Options --- @field logger _99.Logger.Options? --- @field model string? @@ -41,14 +46,14 @@ end --- @field provider _99.Provider? --- @field debug_log_prefix string? --- @field display_errors? boolean ---- @field custom_rules? string[] +--- @field completion _99.Completion? --- unanswered question -- will i need to queue messages one at a time or --- just send them all... So to prepare ill be sending around this state object --- @class _99.State +--- @field completion _99.Completion --- @field model string --- @field md_files string[] ---- @field custom_rules string[] --- @field prompts _99.Prompts --- @field ai_stdout_rows number --- @field languages string[] @@ -146,19 +151,25 @@ end function _99.fill_in_function_prompt() local context = get_context("fill-in-function-with-prompt") + context.logger:debug("start") - Window.capture_input(function(success, response) - context.logger:debug( - "capture_prompt", - "success", - success, - "response", - response - ) - if success then - ops.fill_in_function(context, response) - end - end, {}) + Window.capture_input({ + cb = function(success, response) + context.logger:debug( + "capture_prompt", + "success", + success, + "response", + response + ) + if success then + ops.fill_in_function(context, response) + end + end, + on_load = function() + Extensions.setup_buffer(_99_state) + end, + }) end function _99.fill_in_function() @@ -168,18 +179,23 @@ end function _99.visual_prompt() local context = get_context("over-range-with-prompt") context.logger:debug("start") - Window.capture_input(function(success, response) - context.logger:debug( - "capture_prompt", - "success", - success, - "response", - response - ) - if success then - _99.visual(response) - end - end, {}) + Window.capture_input({ + cb = function(success, response) + context.logger:debug( + "capture_prompt", + "success", + success, + "response", + response + ) + if success then + _99.visual(response) + end + end, + on_load = function() + Extensions.setup_buffer(_99_state) + end, + }) end --- @param prompt string? @@ -250,7 +266,11 @@ function _99.setup(opts) opts = opts or {} _99_state = _99_State.new() _99_state.provider_override = opts.provider - _99_state.custom_rules = opts.custom_rules or {} + _99_state.completion = opts.completion + or { + source = nil, + custom_rules = {}, + } vim.api.nvim_create_autocmd("VimLeavePre", { callback = function() @@ -275,6 +295,7 @@ function _99.setup(opts) _99_state.display_errors = opts.display_errors or false Languages.initialize(_99_state) + Extensions.init(_99_state) end --- @param md string diff --git a/lua/99/window/init.lua b/lua/99/window/init.lua index 8f45433..3475d5a 100644 --- a/lua/99/window/init.lua +++ b/lua/99/window/init.lua @@ -257,9 +257,12 @@ local function set_defaul_win_options(win, name) vim.bo[win.buf_id].swapfile = false end ---- @param cb fun(success: boolean, result: string): nil ---- @param opts {} -function M.capture_input(cb, opts) +--- @class _99.window.CaptureInputOpts +--- @field cb fun(success: boolean, result: string): nil +--- @field on_load? fun(): nil + +--- @param opts _99.window.CaptureInputOpts +function M.capture_input(opts) _ = opts M.clear_active_popups() @@ -299,7 +302,7 @@ function M.capture_input(cb, opts) local lines = vim.api.nvim_buf_get_lines(win.buf_id, 0, -1, false) local result = table.concat(lines, "\n") M.clear_active_popups() - cb(true, result) + opts.cb(true, result) end, }) @@ -322,14 +325,18 @@ function M.capture_input(cb, opts) return end M.clear_active_popups() - cb(false, "") + opts.cb(false, "") end, }) vim.keymap.set("n", "q", function() M.clear_active_popups() - cb(false, "") + opts.cb(false, "") end, { buffer = win.buf_id, nowait = true }) + + if opts.on_load then + vim.schedule(opts.on_load) + end end function M.clear_active_popups() diff --git a/scratch/custom_rules/back-end.md b/scratch/custom_rules/back-end.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/scratch/custom_rules/back-end.md diff --git a/scratch/custom_rules/foo.md b/scratch/custom_rules/foo.md new file mode 100644 index 0000000..139597f --- /dev/null +++ b/scratch/custom_rules/foo.md @@ -0,0 +1,2 @@ + + diff --git a/scratch/custom_rules/front-end.md b/scratch/custom_rules/front-end.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/scratch/custom_rules/front-end.md diff --git a/scratch/custom_rules/vim.lsp.md b/scratch/custom_rules/vim.lsp.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/scratch/custom_rules/vim.lsp.md diff --git a/scratch/custom_rules/vim.md b/scratch/custom_rules/vim.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/scratch/custom_rules/vim.md diff --git a/scratch/custom_rules/vim.treesitter.md b/scratch/custom_rules/vim.treesitter.md new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/scratch/custom_rules/vim.treesitter.md diff --git a/scratch/refresh.lua b/scratch/refresh.lua index 0777f65..a1b6c63 100644 --- a/scratch/refresh.lua +++ b/scratch/refresh.lua @@ -2,140 +2,14 @@ local Window = require("99.window") Window.clear_active_popups() R("99") --- Modern Neovim completion example using vim.lsp.completion and custom completions --- This is the newer API that replaced omnifunc - --- Example 1: Using the built-in completion with a custom source -local function setup_custom_completion() - -- Set up completion options - vim.opt.completeopt = { "menu", "menuone", "noselect" } - - -- Create a custom completion function - -- This is called when user triggers completion (Ctrl-X Ctrl-U by default) - vim.api.nvim_buf_set_option(0, 'completefunc', 'v:lua.custom_complete') -end - --- Custom completion function --- Returns completion items when called -function _G.custom_complete(findstart, base) - if findstart == 1 then - -- First call: return the column where completion starts - local line = vim.api.nvim_get_current_line() - local col = vim.api.nvim_win_get_cursor(0)[2] - - -- Find start of the word - local start = col - while start > 0 and line:sub(start, start):match("[%w_]") do - start = start - 1 - end - - return start - else - -- Second call: return list of completions - -- 'base' is the text to complete - local completions = { - { word = "example_item", menu = "Example", kind = "Function" }, - { word = "example_variable", menu = "Example", kind = "Variable" }, - { word = "example_constant", menu = "Example", kind = "Constant" }, - } - - -- Filter completions based on what user typed - local matches = {} - for _, item in ipairs(completions) do - if item.word:find("^" .. base) then - table.insert(matches, item) - end - end - - return matches - end -end - --- Example 2: Using nvim-cmp style manual completion --- This is the modern approach used by popular plugins -local function create_completion_source() - return { - items = { - { label = "Window.clear_active_popups", kind = vim.lsp.protocol.CompletionItemKind.Method }, - { label = "Window.capture_input", kind = vim.lsp.protocol.CompletionItemKind.Method }, - { label = "Window.create_popup", kind = vim.lsp.protocol.CompletionItemKind.Method }, - { label = "custom_item", kind = vim.lsp.protocol.CompletionItemKind.Variable }, - }, - - complete = function(self, ctx) - local line = ctx.line - local cursor = ctx.cursor - - -- Return filtered items based on context - return vim.tbl_filter(function(item) - return item.label:find(ctx.query or "", 1, true) ~= nil - end, self.items) - end - } -end - --- Example 3: Set up omnifunc (older API, but still works) --- This is triggered with Ctrl-X Ctrl-O -function _G.my_omnifunc(findstart, base) - if findstart == 1 then - local line = vim.api.nvim_get_current_line() - local col = vim.api.nvim_win_get_cursor(0)[2] - return vim.fn.match(line:sub(1, col), [[\k*$]]) - else - -- Return completion matches - return { - "Window.clear_active_popups()", - "Window.capture_input(callback, opts)", - "Window.create_floating_window()", - { word = "advanced_item", abbr = "adv", menu = "Custom", info = "Detailed info here" }, +local Ext = require("99.extensions") +local _99 = require("99") +_99.setup({ + completion = { + source = "cmp", + custom_rules = { + "scratch/custom_rules" + } } - end -end - --- Set it up for the current buffer -vim.api.nvim_buf_set_option(0, 'omnifunc', 'v:lua.my_omnifunc') - --- Usage: --- Ctrl-X Ctrl-O to trigger omnifunc completion --- Ctrl-X Ctrl-U to trigger completefunc completion - --- Example: Auto-trigger completion when typing '@' in 99-prompt buffer - --- Custom completion function that provides context-aware items -function _G.prompt_at_complete(findstart, base) - if findstart == 1 then - -- Find the start position (right after the '@') - local line = vim.api.nvim_get_current_line() - local col = vim.api.nvim_win_get_cursor(0)[2] - - -- Find the '@' symbol - local start = col - while start > 0 and line:sub(start, start) ~= '@' do - start = start - 1 - end - - return start - else - -- Return completion items based on what comes after '@' - local completions = { - { word = "@file", menu = "Reference a file", kind = "File" }, - { word = "@buffer", menu = "Reference a buffer", kind = "Reference" }, - { word = "@selection", menu = "Reference current selection", kind = "Reference" }, - { word = "@codebase", menu = "Search entire codebase", kind = "Keyword" }, - { word = "@web", menu = "Search the web", kind = "Keyword" }, - } - - -- Filter based on what user has typed - local matches = {} - for _, item in ipairs(completions) do - if item.word:find("^@" .. vim.pesc(base), 1) then - table.insert(matches, item) - end - end - - return matches - end -end - -setup_custom_completion() - +}) +Ext.setup_buffer(_99.__get_state()) |
