diff --git a/lua/gemini/completion.lua b/lua/gemini/completion.lua index 647975d..956f84d 100644 --- a/lua/gemini/completion.lua +++ b/lua/gemini/completion.lua @@ -14,6 +14,26 @@ local current_suggestion = { max_context_lines = 10, -- Maximum number of context lines to send } +-- Helper function to get visible lines around cursor +local function get_visible_lines() + local win = vim.api.nvim_get_current_win() + local cursor_line = vim.api.nvim_win_get_cursor(0)[1] + local top_line = vim.fn.line('w0') + local bottom_line = vim.fn.line('w$') + + -- Get all visible lines + local lines = vim.api.nvim_buf_get_lines(0, top_line - 1, bottom_line, false) + local relative_cursor = cursor_line - top_line + 1 + + return lines, relative_cursor +end + +-- Helper function to get current line indent +local function get_line_indent(line) + local indent = line:match("^%s+") or "" + return indent +end + -- Debug function local function debug_print(...) vim.notify(string.format(...), vim.log.levels.INFO) @@ -52,44 +72,27 @@ end local function show_suggestion(suggestion, start_col) clear_suggestion() - -- Split suggestion into lines + local cursor = vim.api.nvim_win_get_cursor(0) + local line = cursor[1] - 1 + local current_line = vim.api.nvim_get_current_line() + + -- Get current line indent + local indent = get_line_indent(current_line) + + -- Split suggestion into lines and apply indent to all lines except first local suggestion_lines = vim.split(suggestion, "\n") + for i = 2, #suggestion_lines do + suggestion_lines[i] = indent .. suggestion_lines[i] + end + if #suggestion_lines == 0 then return end - -- Get current line info - local line = vim.api.nvim_win_get_cursor(0)[1] - 1 - local line_text = vim.api.nvim_buf_get_lines(0, line, line + 1, true)[1] - - -- Validate start_col - if not line_text then return end - start_col = math.min(start_col, #line_text) - if start_col < 0 then start_col = 0 end - - -- Get text before cursor on current line - local input_before_cursor = string.sub(line_text, 1, start_col) - local first_line = suggestion_lines[1] - - -- If the suggestion starts with what's already typed, remove that part - if vim.startswith(first_line, input_before_cursor) then - first_line = string.sub(first_line, #input_before_cursor + 1) - end - - if first_line == "" and #suggestion_lines == 1 then return end - - current_suggestion.text = suggestion - current_suggestion.start_col = start_col - - -- Show first line as virtual text - if first_line ~= "" then - -- Ensure we're not exceeding line length - local safe_col = math.min(start_col, #line_text) - - vim.api.nvim_buf_set_extmark(0, current_suggestion.namespace_id, line, safe_col, { - virt_text = {{first_line, 'GeminiSuggestion'}}, - virt_text_pos = 'inline', - virt_text_hide = true, - }) - end + -- Show ghost text + vim.api.nvim_buf_set_extmark(0, current_suggestion.namespace_id, line, start_col, { + virt_text = {{suggestion_lines[1], 'GeminiSuggestion'}}, + virt_text_pos = 'overlay', + hl_mode = 'combine', + }) -- Show remaining lines as virtual lines if #suggestion_lines > 1 then @@ -98,16 +101,14 @@ local function show_suggestion(suggestion, start_col) table.insert(virt_lines, {{suggestion_lines[i], 'GeminiSuggestion'}}) end - -- Use safe column position for virtual lines - local safe_col = math.min(start_col, #line_text) - - vim.api.nvim_buf_set_extmark(0, current_suggestion.namespace_id, line, safe_col, { + vim.api.nvim_buf_set_extmark(0, current_suggestion.namespace_id, line, start_col, { virt_lines = virt_lines, virt_lines_above = false, }) end - debug_print("Showing suggestion: '%s'", first_line) + current_suggestion.text = table.concat(suggestion_lines, "\n") + current_suggestion.start_col = start_col end function M.accept_suggestion() @@ -206,66 +207,45 @@ local function get_context_around_cursor(lines, current_line, max_lines) end function M.trigger_completion() - debug_print("Triggering completion...") - - -- Clear any existing timer if current_suggestion.timer then vim.fn.timer_stop(current_suggestion.timer) end - -- Set up debounce timer current_suggestion.timer = vim.fn.timer_start(150, function() local cursor = vim.api.nvim_win_get_cursor(0) - local line = cursor[1] + local line = cursor[1] - 1 local col = cursor[2] - local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false) - local current_line = lines[line] - -- Don't trigger if at end of file or empty line + -- Get visible context + local visible_lines, relative_cursor = get_visible_lines() + local current_line = visible_lines[relative_cursor] + if not current_line or current_line == "" then clear_suggestion() return end - -- Get text before cursor - local prefix = string.sub(current_line, 1, col) - - -- Don't trigger if we're in comments or strings - local syntax_group = vim.fn.synIDattr(vim.fn.synID(line, col, 1), "name") + -- Don't trigger in comments or strings + local syntax_group = vim.fn.synIDattr(vim.fn.synID(cursor[1], col, 1), "name") if syntax_group:match("Comment") or syntax_group:match("String") then - debug_print("Skipping: in comment or string") clear_suggestion() return end - -- Check minimum character threshold - local trimmed_prefix = vim.trim(prefix) - if #trimmed_prefix < current_suggestion.min_chars then - debug_print("Skipping: not enough characters") - clear_suggestion() - return - end - - -- Get focused context around cursor - local context = get_context_around_cursor(lines, line, current_suggestion.max_context_lines) - - -- Add cursor position marker - local cursor_marker = string.rep(" ", col) .. "^" - table.insert(context, string.format("L%d (current): %s", line, current_line)) - table.insert(context, cursor_marker) - - local full_context = table.concat(context, "\n") + -- Prepare context for AI + local context = table.concat(visible_lines, "\n") local file_type = vim.bo.filetype - -- Improved prompt for better completions - local prompt = string.format([[In this %s file, complete the code at the cursor (^) position: + local prompt = string.format([[ +In this %s file, complete the code at line %d, column %d: %s Return ONLY the completion text that would naturally continue from the cursor position. Focus on completing the current statement or block. -Consider the syntax, style, and patterns in the surrounding code. -Do not repeat any text that appears before the cursor.]], file_type, full_context) +Consider the visible context, syntax, and code style. +Do not repeat any text that appears before the cursor.]], + file_type, cursor[1], col + 1, context) -- Check cache and rate limiting local cache_key = get_cache_key(prompt) @@ -274,28 +254,19 @@ Do not repeat any text that appears before the cursor.]], file_type, full_contex return end - if should_rate_limit() then - debug_print("Rate limited") - return - end + if should_rate_limit() then return end -- Get completion from Gemini api.get_response(prompt, nil, function(response, error) - if error then - debug_print("Completion error: %s", error) - return - end + if error then return end if type(response) == "string" and #response > 0 then - -- Clean up response and remove any leading whitespace/indentation response = vim.trim(response) - response = response:gsub("^%s+", "") current_suggestion.cache[cache_key] = response vim.schedule(function() - -- Verify cursor position hasn't changed significantly local new_cursor = vim.api.nvim_win_get_cursor(0) - if new_cursor[1] == line and math.abs(new_cursor[2] - col) <= 1 then + if new_cursor[1] == cursor[1] and math.abs(new_cursor[2] - col) <= 1 then show_suggestion(response, col) end end) @@ -310,12 +281,12 @@ function M.setup() vim.api.nvim_set_hl(0, 'GeminiSuggestion', { fg = '#666666', italic = true, + blend = 15 -- Makes the ghost text slightly transparent }) - -- Map Tab to accept suggestion or behave normally + -- Map Tab to accept suggestion vim.keymap.set('i', '', function() if not M.accept_suggestion() then - -- If no suggestion to accept, send regular Tab key vim.api.nvim_feedkeys(vim.api.nvim_replace_termcodes('', true, true, true), 'n', true) end end, { expr = false, noremap = true }) @@ -324,7 +295,6 @@ function M.setup() vim.api.nvim_create_autocmd("TextChangedI", { pattern = "*", callback = function() - -- Only trigger if enabled if vim.b.gemini_completion_enabled then M.trigger_completion() end @@ -338,8 +308,6 @@ function M.setup() clear_suggestion() end }) - - debug_print("Gemini completion setup complete") end return M