From 2b7717bccf6f0c4180447d99b8d9109b2cc8e168 Mon Sep 17 00:00:00 2001 From: Jonas Widen <jonas.widen@widens.eu> Date: Tue, 18 Mar 2025 18:38:11 +0100 Subject: [PATCH] Review and cleanup --- lua/gemini/completion.lua | 154 ++++++++++++++++++++++++++++++++------ lua/gemini/config.lua | 8 +- 2 files changed, 136 insertions(+), 26 deletions(-) diff --git a/lua/gemini/completion.lua b/lua/gemini/completion.lua index 9d92f86..42cda6f 100644 --- a/lua/gemini/completion.lua +++ b/lua/gemini/completion.lua @@ -16,30 +16,116 @@ local current_suggestion = { -- Add at the top of the file with other state variables local completion_cache = { - last_line = nil, - last_col = nil, - suggestions = {}, - timestamp = 0, - ttl = 30000 -- Cache TTL in milliseconds (30 seconds) + entries = {}, + max_entries = 1000, + ttl = 30 * 60 * 1000, -- 30 minutes TTL + last_cleanup = 0 } --- Add this function -local function get_cached_suggestion(line, col) +-- Add completion triggers detection +local function should_trigger_completion(line_text, col) + local config = require("gemini.config") + local trigger_chars = config.options.completion.trigger_characters or "." + + -- Don't trigger on empty lines or spaces + if line_text:match("^%s*$") then return false end + + -- Check minimum characters + local text_before_cursor = line_text:sub(1, col) + if #text_before_cursor < current_suggestion.min_chars then return false end + + -- Check if we're in a comment + local filetype = vim.bo.filetype + local comment_string = vim.bo.commentstring + if comment_string then + local comment_start = comment_string:match("^(.-)%%s") + if comment_start and text_before_cursor:match(vim.pesc(comment_start)) then + return false + end + end + + -- Check for trigger characters + local last_char = text_before_cursor:sub(-1) + if trigger_chars:find(last_char, 1, true) then + return true + end + + -- Check for meaningful context + local meaningful_pattern = "[%w_][%w_%.%:]*$" -- Matches identifiers and dot/colon access + local context = text_before_cursor:match(meaningful_pattern) + if context and #context >= 2 then + return true + end + + return false +end + +-- Improved cache key generation +local function get_cache_key(context, line_text, col) + local prefix = line_text:sub(1, col) + local filetype = vim.bo.filetype + local key_parts = { + filetype, + prefix, + vim.inspect(context):sub(1, 100) -- Limited context hash + } + return vim.fn.sha256(table.concat(key_parts, "||")) +end + +-- Cache management +local function cleanup_cache() local now = vim.loop.now() - if completion_cache.last_line == line - and completion_cache.last_col == col - and (now - completion_cache.timestamp) < completion_cache.ttl then - return completion_cache.suggestions + if now - completion_cache.last_cleanup < 60000 then return end -- Cleanup max once per minute + + local count = 0 + local expired = {} + + for key, entry in pairs(completion_cache.entries) do + if now - entry.timestamp > completion_cache.ttl then + table.insert(expired, key) + end + count = count + 1 + end + + -- Remove expired entries + for _, key in ipairs(expired) do + completion_cache.entries[key] = nil + end + + -- If still too many entries, remove oldest + if count > completion_cache.max_entries then + local entries = {} + for k, v in pairs(completion_cache.entries) do + table.insert(entries, {key = k, timestamp = v.timestamp}) + end + table.sort(entries, function(a, b) return a.timestamp < b.timestamp end) + + for i = 1, count - completion_cache.max_entries do + completion_cache.entries[entries[i].key] = nil + end + end + + completion_cache.last_cleanup = now +end + +-- Cache access +local function get_cached_completion(context, line_text, col) + cleanup_cache() + local key = get_cache_key(context, line_text, col) + local entry = completion_cache.entries[key] + + if entry and (vim.loop.now() - entry.timestamp) < completion_cache.ttl then + return entry.completion end return nil end --- Add this function -local function cache_suggestion(line, col, suggestions) - completion_cache.last_line = line - completion_cache.last_col = col - completion_cache.suggestions = suggestions - completion_cache.timestamp = vim.loop.now() +local function cache_completion(context, line_text, col, completion) + local key = get_cache_key(context, line_text, col) + completion_cache.entries[key] = { + completion = completion, + timestamp = vim.loop.now() + } end -- Helper function to get visible lines around cursor @@ -279,11 +365,29 @@ function M.trigger_completion() local col = cursor[2] local current_line = vim.api.nvim_get_current_line() + -- Check if we should trigger completion + if not should_trigger_completion(current_line, col) then + clear_suggestion() + return + end + + -- Check rate limiting + if should_rate_limit() then return end + -- Get context local visible_lines = vim.api.nvim_buf_get_lines(0, math.max(0, line - 10), line + 10, false) - local context, detected_file_type = get_context_around_cursor(visible_lines, 10, current_suggestion.max_context_lines) + local context = get_context_around_cursor(visible_lines, 10, current_suggestion.max_context_lines) - -- Create a very specific prompt for raw completion + -- Check cache first + local cached_completion = get_cached_completion(context, current_line, col) + if cached_completion then + vim.schedule(function() + show_suggestion(cached_completion, col) + end) + return + end + + -- Create prompt for completion local prompt = string.format([[ You are an autocomplete engine. Respond ONLY with the direct completion text. DO NOT include: @@ -297,19 +401,23 @@ Language: %s Context: %s Complete this line: -%s]], detected_file_type, table.concat(context, "\n"), current_line) +%s]], vim.bo.filetype, table.concat(context, "\n"), current_line) -- Make API request api.get_response(prompt, nil, function(response, error) if error then - vim.notify("Completion error: " .. error, vim.log.levels.ERROR) + if error:match("code = 429") then + handle_rate_limit() + return + end return end if response then - -- Show the raw suggestion directly + -- Cache the successful completion + cache_completion(context, current_line, col, response) + vim.schedule(function() - -- Only show if cursor position hasn't changed local new_cursor = vim.api.nvim_win_get_cursor(0) if new_cursor[1] - 1 == line and new_cursor[2] == col then show_suggestion(response, col) diff --git a/lua/gemini/config.lua b/lua/gemini/config.lua index ec15d64..9d20a2d 100644 --- a/lua/gemini/config.lua +++ b/lua/gemini/config.lua @@ -22,11 +22,13 @@ M.defaults = { completion = { enabled = true, debounce_ms = 1000, - min_chars = 0, + min_chars = 2, max_context_lines = 10, - style = "ghost", -- or "inline" - trigger_characters = ".", -- Add trigger characters + style = "ghost", + trigger_characters = ".:", -- Trigger on dot and colon exclude_filetypes = { "TelescopePrompt", "neo-tree" }, + cache_ttl = 30 * 60 * 1000, -- 30 minutes + max_cache_entries = 1000, suggestion_highlight = { fg = '#666666', italic = true,