summaryrefslogtreecommitdiff
path: root/lua
diff options
context:
space:
mode:
authorThePrimeagen <the.primeagen@gmail.com>2026-02-28 14:17:09 -0700
committerGitHub <noreply@github.com>2026-02-28 14:17:09 -0700
commitd3e0a45d8ab67ab618a07dd40d7b15fa421ef0aa (patch)
tree59c8a518e35bb7690d4605adf37f26262c115fba /lua
parenta763a00bad9982dba7b3b755c6ced2169c99393a (diff)
parent89ada34a011bf8ed5cc5e192bedfb9c5b2dfa868 (diff)
downloada4-d3e0a45d8ab67ab618a07dd40d7b15fa421ef0aa.tar.xz
a4-d3e0a45d8ab67ab618a07dd40d7b15fa421ef0aa.zip
Merge pull request #141 from codegirl-007/native-completions
Support native completions
Diffstat (limited to 'lua')
-rw-r--r--lua/99/extensions/cmp.lua2
-rw-r--r--lua/99/extensions/completions.lua12
-rw-r--r--lua/99/extensions/init.lua15
-rw-r--r--lua/99/extensions/native.lua162
4 files changed, 182 insertions, 9 deletions
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,
+}