diff options
| author | ThePrimeagen <the.primeagen@gmail.com> | 2026-02-28 14:17:09 -0700 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-02-28 14:17:09 -0700 |
| commit | d3e0a45d8ab67ab618a07dd40d7b15fa421ef0aa (patch) | |
| tree | 59c8a518e35bb7690d4605adf37f26262c115fba | |
| parent | a763a00bad9982dba7b3b755c6ced2169c99393a (diff) | |
| parent | 89ada34a011bf8ed5cc5e192bedfb9c5b2dfa868 (diff) | |
| download | a4-d3e0a45d8ab67ab618a07dd40d7b15fa421ef0aa.tar.xz a4-d3e0a45d8ab67ab618a07dd40d7b15fa421ef0aa.zip | |
Merge pull request #141 from codegirl-007/native-completions
Support native completions
| -rw-r--r-- | README.md | 6 | ||||
| -rw-r--r-- | lua/99/extensions/cmp.lua | 2 | ||||
| -rw-r--r-- | lua/99/extensions/completions.lua | 12 | ||||
| -rw-r--r-- | lua/99/extensions/init.lua | 15 | ||||
| -rw-r--r-- | lua/99/extensions/native.lua | 162 |
5 files changed, 185 insertions, 12 deletions
@@ -88,8 +88,8 @@ through `search` and `work` -- exclude = { ".env", ".env.*", "node_modules", ".git", ... }, }, - --- What autocomplete you use. - source = "cmp" | "blink", + --- What autocomplete engine to use. Defaults to native (built-in) if not specified. + source = "native", -- "native" (default), "cmp", or "blink" }, --- WARNING: if you change cwd then this is likely broken @@ -421,7 +421,7 @@ When prompting, you can reference rules and files to add context to your request - `#` references rules — type `#` in the prompt to autocomplete rule files from your configured rule directories - `@` references files — type `@` to fuzzy-search project files -Referenced content is automatically resolved and injected into the AI context. Requires cmp (`source = "cmp"` in your completion config). +Referenced content is automatically resolved and injected into the AI context. Native completions work by default. For nvim-cmp or blink.cmp, set `source = "cmp"` or `source = "blink"`. ## Providers 99 supports multiple AI CLI backends. Set `provider` in your setup to switch. If you don't set `model`, the provider's default is used. diff --git a/lua/99/extensions/cmp.lua b/lua/99/extensions/cmp.lua index 4279634..30575bd 100644 --- a/lua/99/extensions/cmp.lua +++ b/lua/99/extensions/cmp.lua @@ -37,7 +37,7 @@ function CmpSource.complete(_, params, callback) -- Find which trigger is active local trigger = nil for _, char in ipairs(Completions.get_trigger_characters()) do - local pattern = char:gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])", "%%%1") .. "%S*$" + local pattern = Completions.escape_pattern(char) .. "%S*$" if before:match(pattern) then trigger = char break diff --git a/lua/99/extensions/completions.lua b/lua/99/extensions/completions.lua index 1ba36ca..4eeec08 100644 --- a/lua/99/extensions/completions.lua +++ b/lua/99/extensions/completions.lua @@ -14,6 +14,13 @@ local providers = {} local M = {} +--- Escape special Lua pattern characters in a string +--- @param str string The string to escape +--- @return string The escaped string safe for use in Lua patterns +function M.escape_pattern(str) + return str:gsub("([%%%^%$%(%)%.%[%]%*%+%-%?])", "%%%1") +end + --- @param provider _99.CompletionProvider function M.register(provider) for i, p in ipairs(providers) do @@ -48,10 +55,7 @@ end function M.parse(prompt_text) local refs = {} for _, provider in ipairs(providers) do - local pattern = provider.trigger:gsub( - "([%%%^%$%(%)%.%[%]%*%+%-%?])", - "%%%1" - ) .. "%S+" + local pattern = M.escape_pattern(provider.trigger) .. "%S+" for word in prompt_text:gmatch(pattern) do local token = word:sub(#provider.trigger + 1) if provider.is_valid(token) then diff --git a/lua/99/extensions/init.lua b/lua/99/extensions/init.lua index 6aae2bb..6a7d631 100644 --- a/lua/99/extensions/init.lua +++ b/lua/99/extensions/init.lua @@ -8,15 +8,22 @@ local Files = require("99.extensions.files") --- @param completion _99.Completion | nil --- @return _99.Extensions.Source | nil local function get_source(completion) - if not completion or not completion.source then - return + local source = completion and completion.source or "native" + + if source == "native" then + local ok, native = pcall(require, "99.extensions.native") + if not ok then + vim.notify("[99] Failed to load native completions", vim.log.levels.ERROR) + return + end + return native end - local source = completion.source + if source == "cmp" then local ok, cmp = pcall(require, "99.extensions.cmp") if not ok then vim.notify( - '[99] nvim-cmp is not installed. Install hrsh7th/nvim-cmp or use source = "blink"', + '[99] nvim-cmp is not installed. Install hrsh7th/nvim-cmp or use source = "blink" or "native"', vim.log.levels.WARN ) return diff --git a/lua/99/extensions/native.lua b/lua/99/extensions/native.lua new file mode 100644 index 0000000..b025410 --- /dev/null +++ b/lua/99/extensions/native.lua @@ -0,0 +1,162 @@ +local Agents = require("99.extensions.agents") +local Files = require("99.extensions.files") +local Completions = require("99.extensions.completions") + +local DEBOUNCE_MS = 100 + +--- @param items CompletionItem[] +--- @return table[] +local function to_native_items(items) + local out = {} + for _, item in ipairs(items) do + local info = "" + if item.documentation then + if type(item.documentation) == "string" then + info = item.documentation + elseif item.documentation.value then + info = item.documentation.value + end + end + table.insert(out, { + word = item.insertText or item.label, + abbr = item.label, + info = info, + icase = 1, + dup = 0, + }) + end + return out +end + +--- @param _99 _99.State +local function register_providers(_99) + Completions.register(Agents.completion_provider(_99)) + Completions.register(Files.completion_provider()) +end + +--- @param buf number +local function setup_completion_autocmd(buf) + local timer = vim.uv.new_timer() + local group = vim.api.nvim_create_augroup( + "99_native_completion_" .. buf, + { clear = true } + ) + + vim.api.nvim_create_autocmd("TextChangedI", { + group = group, + buffer = buf, + callback = function() + timer:stop() + timer:start( + DEBOUNCE_MS, + 0, + vim.schedule_wrap(function() + if vim.fn.mode() ~= "i" then + return + end + + if not vim.api.nvim_buf_is_valid(buf) then + timer:stop() + return + end + + local line = vim.api.nvim_get_current_line() + local col = vim.fn.col(".") + local before = line:sub(1, col - 1) + + local trigger = nil + local start_col = nil + for _, char in ipairs(Completions.get_trigger_characters()) do + local escaped = Completions.escape_pattern(char) + local pattern = escaped .. "%S*$" + local match_start = before:find(pattern) + if match_start then + trigger = char + start_col = match_start + break + end + end + + if not trigger or not start_col then + return + end + + local items = Completions.get_completions(trigger) + if #items == 0 then + return + end + + local native_items = to_native_items(items) + vim.fn.complete(start_col, native_items) + end) + ) + end, + }) + + vim.api.nvim_create_autocmd("BufWipeout", { + group = group, + buffer = buf, + callback = function() + timer:stop() + pcall(vim.api.nvim_del_augroup_by_id, group) + end, + }) +end + +--- @param buf number +local function setup_keymaps(buf) + vim.keymap.set("i", "<Tab>", function() + if vim.fn.pumvisible() == 1 then + return "<C-n>" + end + return "<Tab>" + end, { buffer = buf, expr = true, noremap = true }) + + vim.keymap.set("i", "<S-Tab>", function() + if vim.fn.pumvisible() == 1 then + return "<C-p>" + end + return "<S-Tab>" + end, { buffer = buf, expr = true, noremap = true }) +end + +--- @param _ _99.State +local function init_for_buffer(_) + local buf = vim.api.nvim_get_current_buf() + + vim.bo[buf].filetype = "99prompt" + vim.opt_local.completeopt = "menuone,noinsert,noselect,popup,fuzzy" + + setup_completion_autocmd(buf) + setup_keymaps(buf) +end + +--- @param _99 _99.State +local function init(_99) + local rule_dirs = {} + if _99.completion and _99.completion.custom_rules then + for _, dir in ipairs(_99.completion.custom_rules) do + table.insert(rule_dirs, dir) + end + end + + if _99.completion and _99.completion.files then + Files.setup(_99.completion.files, rule_dirs) + else + Files.setup({ enabled = true }, rule_dirs) + end + + register_providers(_99) +end + +--- @param _99 _99.State +local function refresh_state(_99) + register_providers(_99) +end + +--- @type _99.Extensions.Source +return { + init_for_buffer = init_for_buffer, + init = init, + refresh_state = refresh_state, +} |
