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,