[
  {
    "path": ".gitignore",
    "content": "__pycache__\n*.db\n\nlua/gemini/*.so\n.cache\nbuild\n\ndoc/tags\n"
  },
  {
    "path": ".luacheckrs",
    "content": "read_globals = { \"vim\" }\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2025 Joseph\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "TESTS_INIT=tests/init.lua\nTESTS_DIR=tests/\n\nall:\n\tcmake -B build\n\tmake -C build -j\n\nclean:\n\trm -rf build\n\trm -rf lua/gemini/*.so\n\n\ntest:\n\t@nvim \\\n\t\t--headless \\\n\t\t--noplugin \\\n\t\t-u ${TESTS_INIT} \\\n\t\t-c \"PlenaryBustedDirectory ${TESTS_DIR} { minimal_init = '${TESTS_INIT}' }\"\n\n\n.PHONY: all clean test\n"
  },
  {
    "path": "README.md",
    "content": "# gemini.nvim\n\nThis plugin try to interface Google's Gemini API into neovim.\n\n\n## Features\n\n- Code Complete\n- Code Explain\n- Unit Test Generation\n- Code Review\n- Hints\n- Chat\n\n### Code Complete\nhttps://github.com/user-attachments/assets/11ae6719-4f3f-41db-8ded-56db20e6e9f4\n\nhttps://github.com/user-attachments/assets/34c38078-a028-47d2-acb1-49e03d0b4330\n\n### Do some changes\nhttps://github.com/user-attachments/assets/c0a001a2-a5fe-469d-ae01-3468d05b041c\n\n\n\n\n### Code Explain\nhttps://github.com/user-attachments/assets/6b2492ee-7c70-4bbc-937b-27bfa50f8944\n\n### Unit Test generation\nhttps://github.com/user-attachments/assets/0620a8a4-5ea6-431d-ba17-41c7d553f742\n\n### Code Review\nhttps://github.com/user-attachments/assets/9100ab70-f107-40de-96e2-fb4ea749c014\n\n### Hints\nhttps://github.com/user-attachments/assets/a36804e9-073f-4e3e-9178-56b139fd0c62\n\n### Chat\nhttps://github.com/user-attachments/assets/d3918d2a-4cf7-4639-bc21-689d4225ba6d\n\n\n## Installation\n\n- install `curl`\n\n```\nsudo apt install curl\n```\n\n\n\n\n\n```shell\nexport GEMINI_API_KEY=\"<your API key here>\"\n```\n\n* [lazy.nvim](https://github.com/folke/lazy.nvim)\n\n```lua\n{\n  'kiddos/gemini.nvim',\n  opts = {}\n}\n```\n\n\n* [packer.nvim](https://github.com/wbthomason/packer.nvim)\n\n\n```lua\nuse { 'kiddos/gemini.nvim', opts = {} }\n```\n\n## Settings\n\ndefault setting\n\n```lua\n{\n  model_config = {\n    model_id = 'gemini-2.5-flash',\n    temperature = 0.10,\n    top_k = 128,\n    response_mime_type = 'text/plain',\n  },\n  chat_config = {\n    enabled = true,\n  },\n  hints = {\n    enabled = true,\n    hints_delay = 2000,\n    insert_result_key = '<S-Tab>',\n    get_prompt = function(node, bufnr)\n      local code_block = vim.treesitter.get_node_text(node, bufnr)\n      local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n      local prompt = [[\n  In struction: Use 1 or 2 sentences to describe what the following {filetype} function does:\n\n  ```{filetype}\n  {code_block}\n  ``]] .. '`'\n      prompt = prompt:gsub('{filetype}', filetype)\n      prompt = prompt:gsub('{code_block}', code_block)\n      return prompt\n    end\n  },\n  completion = {\n    enabled = true,\n    blacklist_filetypes = { 'help', 'qf', 'json', 'yaml', 'toml', 'xml' },\n    blacklist_filenames = { '.env' },\n    completion_delay = 800,\n    insert_result_key = '<S-Tab>',\n    move_cursor_end = true,\n    can_complete = function()\n      return vim.fn.pumvisible() ~= 1\n    end,\n    get_system_text = function()\n      return \"You are a coding AI assistant that autocomplete user's code.\"\n        .. \"\\n* Your task is to provide code suggestion at the cursor location marked by <cursor></cursor>.\"\n        .. '\\n* Your response does not need to contain explaination.'\n    end,\n    get_prompt = function(bufnr, pos)\n      local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n      local prompt = 'Below is the content of a %s file `%s`:\\n'\n          .. '```%s\\n%s\\n```\\n\\n'\n          .. 'Suggest the most likely code at <cursor></cursor>.\\n'\n          .. 'Wrap your response in ``` ```\\n'\n          .. 'eg.\\n```\\n```\\n\\n'\n      local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)\n      local line = pos[1]\n      local col = pos[2]\n      local target_line = lines[line]\n      if target_line then\n        lines[line] = target_line:sub(1, col) .. '<cursor></cursor>' .. target_line:sub(col + 1)\n      else\n        return nil\n      end\n      local code = vim.fn.join(lines, '\\n')\n      local abs_path = vim.api.nvim_buf_get_name(bufnr)\n      local filename = vim.fn.fnamemodify(abs_path, ':.')\n      prompt = string.format(prompt, filetype, filename, filetype, code)\n      return prompt\n    end\n  },\n  instruction = {\n    enabled = true,\n    menu_key = '<Leader><Leader><Leader>g',\n    prompts = {\n      {\n        name = 'Unit Test',\n        command_name = 'GeminiUnitTest',\n        menu = 'Unit Test 🚀',\n        get_prompt = function(lines, bufnr)\n          local code = vim.fn.join(lines, '\\n')\n          local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n          local prompt = 'Context:\\n\\n```%s\\n%s\\n```\\n\\n'\n              .. 'Objective: Write unit test for the above snippet of code\\n'\n          return string.format(prompt, filetype, code)\n        end,\n      },\n      {\n        name = 'Code Review',\n        command_name = 'GeminiCodeReview',\n        menu = 'Code Review 📜',\n        get_prompt = function(lines, bufnr)\n          local code = vim.fn.join(lines, '\\n')\n          local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n          local prompt = 'Context:\\n\\n```%s\\n%s\\n```\\n\\n'\n              .. 'Objective: Do a thorough code review for the following code.\\n'\n              .. 'Provide detail explaination and sincere comments.\\n'\n          return string.format(prompt, filetype, code)\n        end,\n      },\n      {\n        name = 'Code Explain',\n        command_name = 'GeminiCodeExplain',\n        menu = 'Code Explain',\n        get_prompt = function(lines, bufnr)\n          local code = vim.fn.join(lines, '\\n')\n          local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n          local prompt = 'Context:\\n\\n```%s\\n%s\\n```\\n\\n'\n              .. 'Objective: Explain the following code.\\n'\n              .. 'Provide detail explaination and sincere comments.\\n'\n          return string.format(prompt, filetype, code)\n        end,\n      },\n    }\n  },\n  task = {\n    enabled = true,\n    get_system_text = function()\n      return 'You are an AI assistant that helps user write code.'\n        .. '\\n* You should output the new content for the Current Opened File'\n    end,\n    get_prompt = function(bufnr, user_prompt)\n      local buffers = vim.api.nvim_list_bufs()\n      local file_contents = {}\n\n      for _, b in ipairs(buffers) do\n        if vim.api.nvim_buf_is_loaded(b) then -- Only get content from loaded buffers\n          local lines = vim.api.nvim_buf_get_lines(b, 0, -1, false)\n          local abs_path = vim.api.nvim_buf_get_name(b)\n          local filename = vim.fn.fnamemodify(abs_path, ':.')\n          local filetype = vim.api.nvim_get_option_value('filetype', { buf = b })\n          local file_content = table.concat(lines, \"\\n\")\n          file_content = string.format(\"`%s`:\\n\\n```%s\\n%s\\n```\\n\\n\", filename, filetype, file_content)\n          table.insert(file_contents, file_content)\n        end\n      end\n\n      local current_filepath = vim.api.nvim_buf_get_name(bufnr)\n      current_filepath = vim.fn.fnamemodify(current_filepath, \":.\")\n\n      local context = table.concat(file_contents, \"\\n\\n\")\n      return string.format('%s\\n\\nCurrent Opened File: %s\\n\\nTask: %s',\n        context, current_filepath, user_prompt)\n    end\n    },\n}\n```\n"
  },
  {
    "path": "doc/gemini.nvim.txt",
    "content": "*gemini.nvim*    Google's Gemini neovim binding\n\nINTRODUCTION                                    *gemini.nvim-intro*\n\nThis plugin try to interface Google's Gemini API into neovim.\n\n\n\nINSTALLATION                                    *gemini.nvim-install*\n\ninstall curl\n\nsudo apt install curl\n\nsetup API key\n\nexport GEMINI_API_KEY=\"<your API key here>\"\n\nlazy.nvim:\n\n{\n  'kiddos/gemini.nvim',\n  opts = {}\n}\n\n\n\nCONFIGURATION                                    *gemini.nvim-config*\n\n{\n  model_config = {\n    completion_delay = 1000,\n    model_id = api.MODELS.GEMINI_2_0_FLASH,\n    temperature = 0.2,\n    top_k = 20,\n    max_output_tokens = 8196,\n    response_mime_type = 'text/plain',\n  },\n  chat_config = {\n    enabled = true,\n  },\n  hints = {\n    enabled = true,\n    hints_delay = 2000,\n    insert_result_key = '<S-Tab>',\n    get_prompt = function(node, bufnr)\n      local code_block = vim.treesitter.get_node_text(node, bufnr)\n      local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n      local prompt = \"\n  Instruction: Use 1 or 2 sentences to describe what the following {filetype} function does:\n  \n  ```{filetype}\n  {code_block}\n  ```\",\n      prompt = prompt:gsub('{filetype}', filetype)\n      prompt = prompt:gsub('{code_block}', code_block)\n      return prompt\n    end\n  }\n  completion = {\n    enabled = true,\n    blacklist_filetypes = { 'help', 'qf', 'json', 'yaml', 'toml' },\n    blacklist_filenames = { '.env' },\n    completion_delay = 600,\n    move_cursor_end = false,\n    insert_result_key = '<S-Tab>',\n    can_complete = function()\n      return vim.fn.pumvisible() ~= 1\n    end,\n    get_system_text = function()\n      return \"You are a coding AI assistant that autocomplete user's code.\"\n        .. \"\\n* Your task is to provide code suggestion at the cursor location marked by <cursor></cursor>.\"\n        .. '\\n* Do not wrap your code response in ```'\n    end,\n    get_prompt = function(bufnr, pos)\n      local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n      local prompt = 'Below is the content of a %s file `%s`:\\n'\n          .. '```%s\\n%s\\n```\\n\\n'\n          .. 'Suggest the most likely code at <cursor></cursor>.\\n'\n      local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)\n      local line = pos[1]\n      local col = pos[2]\n      local target_line = lines[line]\n      if target_line then\n        lines[line] = target_line:sub(1, col) .. '<cursor></cursor>' .. target_line:sub(col + 1)\n      else\n        return nil\n      end\n      local code = vim.fn.join(lines, '\\n')\n      local filename = vim.api.nvim_buf_get_name(bufnr)\n      prompt = string.format(prompt, filetype, filename, filetype, code)\n      return prompt\n    end\n  },\n  instruction = {\n    enabled = true,\n    menu_key = '<C-o>',\n    prompts = {\n      {\n        name = 'Unit Test',\n        command_name = 'GeminiUnitTest',\n        menu = 'Unit Test 🚀',\n        get_prompt = function(lines, bufnr)\n          local code = vim.fn.join(lines, '\\n')\n          local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n          local prompt = 'Context:\\n\\n```%s\\n%s\\n```\\n\\n'\n              .. 'Objective: Write unit test for the above snippet of code\\n'\n          return string.format(prompt, filetype, code)\n        end,\n      },\n      {\n        name = 'Code Review',\n        command_name = 'GeminiCodeReview',\n        menu = 'Code Review 📜',\n        get_prompt = function(lines, bufnr)\n          local code = vim.fn.join(lines, '\\n')\n          local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n          local prompt = 'Context:\\n\\n```%s\\n%s\\n```\\n\\n'\n              .. 'Objective: Do a thorough code review for the following code.\\n'\n              .. 'Provide detail explaination and sincere comments.\\n'\n          return string.format(prompt, filetype, code)\n        end,\n      },\n      {\n        name = 'Code Explain',\n        command_name = 'GeminiCodeExplain',\n        menu = 'Code Explain',\n        get_prompt = function(lines, bufnr)\n          local code = vim.fn.join(lines, '\\n')\n          local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n          local prompt = 'Context:\\n\\n```%s\\n%s\\n```\\n\\n'\n              .. 'Objective: Explain the following code.\\n'\n              .. 'Provide detail explaination and sincere comments.\\n'\n          return string.format(prompt, filetype, code)\n        end,\n      },\n    },\n  },\n  task = {\n    enabled = true,\n    get_system_text = function()\n      return 'You are an AI assistant that helps user write code.\\n'\n        .. 'Your output should be a code diff for git.'\n    end,\n    get_prompt = function(bufnr, user_prompt)\n      local buffers = vim.api.nvim_list_bufs()\n      local file_contents = {}\n\n      for _, b in ipairs(buffers) do\n        if vim.api.nvim_buf_is_loaded(b) then -- Only get content from loaded buffers\n          local lines = vim.api.nvim_buf_get_lines(b, 0, -1, false)\n          local filename = vim.api.nvim_buf_get_name(b)\n          filename = vim.fn.fnamemodify(filename, \":.\")\n          local filetype = vim.api.nvim_get_option_value('filetype', { buf = b })\n          local file_content = table.concat(lines, \"\\n\")\n          file_content = string.format(\"`%s`:\\n\\n```%s\\n%s\\n```\\n\\n\", filename, filetype, file_content)\n          table.insert(file_contents, file_content)\n        end\n      end\n\n      local current_filepath = vim.api.nvim_buf_get_name(bufnr)\n      current_filepath = vim.fn.fnamemodify(current_filepath, \":.\")\n\n      local context = table.concat(file_contents, \"\\n\\n\")\n      return string.format('%s\\n\\nCurrent Opened File: %s\\n\\nTask: %s',\n        context, current_filepath, user_prompt)\n    end\n  },\n}\n\n\n\nCOMMANDS                                                     *gemini.nvim-commands*\n\n:GeminiTask            ask Gemini to complete some task. this would use current opened buffer as context. this would also open up a diff pane in the current window\n\n:GeminiApply           this would apply the diff result from :GeminiTask\n\n:GeminiChat            ask Gemini to do something. this doesn't use any context\n\n:GeminiCodeExplain     select some code and ask Gemini what does it do\n\n:GeminiCodeReview      do some code review\n\n:GeminiUnitTest        let Gemini write your unit test\n\n:GeminiFunctionHint    give some function documentation\n\n\n\nMAPPINGS                                                     *gemini.nvim-mappings*\n\n<leader><leader><leader>g     open a popup menu to select some task to do\n\n<S-Tab>                       confirm gemini's autocomplete\n"
  },
  {
    "path": "lua/gemini/api.lua",
    "content": "local uv = vim.loop or vim.uv\n\nlocal M = {}\n\nlocal API = \"https://generativelanguage.googleapis.com/v1beta/models/\";\n\nM.MODELS = {\n  GEMINI_3_FLASH_PREVIEW = 'gemini-3-flash-preview',\n  GEMINI_FLASH_LATEST = 'gemini-flash-latest',\n  GEMINI_3_1_PRO_PREVIEW = 'gemini-3.1-pro-preview',\n  GEMINI_3_1_FLASH_LITE_PREVIEW = 'gemini-3.1-flash-lite-preview',\n  GEMINI_FLASH_LITE_LATEST = 'gemini-flash-lite-latest',\n  GEMINI_2_5_PRO = 'gemini-2.5-pro',\n  GEMINI_2_5_FLASH = 'gemini-2.5-flash',\n  GEMINI_2_5_FLASH_LITE = 'gemini-2.5-flash-lite',\n  GEMINI_2_0_FLASH = 'gemini-2.0-flash',\n  GEMINI_2_0_FLASH_LITE = 'gemini-2.0-flash-lite',\n}\n\nM.gemini_generate_content = function(user_text, system_text, model_name, generation_config, callback)\n  local api_key = os.getenv(\"GEMINI_API_KEY\")\n  if not api_key then\n    return ''\n  end\n\n  local api = API .. model_name .. ':generateContent?key=' .. api_key\n  local contents = {\n    {\n      parts = {\n        {\n          text = user_text\n        }\n      }\n    }\n  }\n  local data = {\n    contents = contents,\n    generationConfig = generation_config,\n  }\n  if system_text then\n    data.systemInstruction = {\n      parts = {\n        {\n          text = system_text,\n        }\n      }\n    }\n  end\n\n  local json_text = vim.json.encode(data)\n  local cmd = { 'curl', '--no-progress-meter', '-X', 'POST', api, '-H', 'Content-Type: application/json', '--data-binary', '@-' }\n  local opts = { stdin = json_text }\n  if callback then\n    return vim.system(cmd, opts, callback)\n  else\n    return vim.system(cmd, opts)\n  end\nend\n\nM.gemini_generate_content_stream = function(user_text, model_name, generation_config, callback)\n  local api_key = os.getenv(\"GEMINI_API_KEY\")\n  if not api_key then\n    return\n  end\n\n  if not callback then\n    return\n  end\n\n  local api = API .. model_name .. ':streamGenerateContent?alt=sse&key=' .. api_key\n  local data = {\n    contents = {\n      {\n        parts = {\n          {\n            text = user_text\n          }\n        }\n      }\n    },\n    generationConfig = generation_config,\n  }\n  local json_text = vim.json.encode(data)\n\n  local stdin = uv.new_pipe()\n  local stdout = uv.new_pipe()\n  local stderr = uv.new_pipe()\n  local options = {\n    stdio = { stdin, stdout, stderr },\n    args = { api, '-X', 'POST', '-s', '-H', 'Content-Type: application/json', '-d', json_text }\n  }\n\n  uv.spawn('curl', options, function(code, _)\n    print(\"gemini chat finished exit code\", code)\n  end)\n\n  local streamed_data = ''\n  uv.read_start(stdout, function(err, data)\n    if not err and data then\n      streamed_data = streamed_data .. data\n\n      local start_index = string.find(streamed_data, 'data:')\n      local end_index = string.find(streamed_data, '\\r')\n      local json_text = ''\n      while start_index and end_index do\n        if end_index >= start_index then\n          json_text = string.sub(streamed_data, start_index + 5, end_index - 1)\n          callback(json_text)\n        end\n        streamed_data = string.sub(streamed_data, end_index + 1)\n        start_index = string.find(streamed_data, 'data:')\n        end_index = string.find(streamed_data, '\\r')\n      end\n    end\n  end)\n\nend\n\nreturn M\n"
  },
  {
    "path": "lua/gemini/chat.lua",
    "content": "local config = require('gemini.config')\nlocal util = require('gemini.util')\nlocal api = require('gemini.api')\n\nlocal M = {}\n\nM.setup = function()\n  local model = config.get_config({ 'chat', 'model' })\n  if not model or not model.model_id then\n    return\n  end\n\n  vim.api.nvim_create_user_command('GeminiChat', M.start_chat, {\n    force = true,\n    desc = 'Google Gemini',\n    nargs = 1,\n  })\nend\n\nlocal context = {\n  chat_winnr = nil,\n  chat_number = 0,\n}\n\nlocal function get_bufnr(user_text)\n  local conf = config.get_config({ 'chat' })\n  if not conf then\n    vim.api.nvim_command('tabnew')\n    local bufnr = vim.api.nvim_get_current_buf()\n    vim.api.nvim_set_option_value('ft', 'markdown', { buf = bufnr })\n    return bufnr\n  end\n\n  local bufnr = nil\n  if not context.chat_winnr or not vim.api.nvim_win_is_valid(context.chat_winnr) or conf.window.position == 'new_tab' then\n    if conf.window.position == 'tab' or conf.window.position == 'new_tab' then\n      vim.api.nvim_command('tabnew')\n    elseif conf.window.position == 'left' then\n      vim.api.nvim_command('vertical topleft split new')\n      vim.api.nvim_win_set_width(0, conf.window.width or 80)\n    elseif conf.window.position == 'right' then\n      vim.api.nvim_command('rightbelow vnew')\n      vim.api.nvim_win_set_width(0, conf.window.width or 80)\n    end\n    context.chat_winnr = vim.api.nvim_tabpage_get_win(0)\n    bufnr = vim.api.nvim_win_get_buf(0)\n  end\n  vim.api.nvim_set_current_win(context.chat_winnr)\n  bufnr = bufnr or vim.api.nvim_win_get_buf(0)\n  vim.api.nvim_set_option_value('buftype', 'nofile', { buf = bufnr })\n  vim.api.nvim_set_option_value('ft', 'markdown', { buf = bufnr })\n  vim.api.nvim_buf_set_name(bufnr, 'Chat' .. context.chat_number .. ': ' .. user_text)\n\n  return vim.api.nvim_win_get_buf(0)\nend\n\nM.start_chat = function(cxt)\n  local user_text = cxt.args\n  context.chat_number = context.chat_number + 1\n  local bufnr = get_bufnr(user_text)\n  local lines = { 'Generating response...' }\n  vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)\n\n  local generation_config = config.get_gemini_generation_config('chat')\n  local text = ''\n  local model_id = config.get_config({ 'chat', 'model', 'model_id' })\n  api.gemini_generate_content_stream(user_text, model_id, generation_config, function(json_text)\n    local model_response = vim.json.decode(json_text)\n    model_response = util.table_get(model_response, { 'candidates', 1, 'content', 'parts', 1, 'text' })\n    if not model_response then\n      return\n    end\n\n    text = text .. model_response\n    vim.schedule(function()\n      lines = vim.split(text, '\\n')\n      vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines)\n    end)\n  end)\nend\n\nreturn M\n"
  },
  {
    "path": "lua/gemini/completion.lua",
    "content": "local config = require('gemini.config')\nlocal util = require('gemini.util')\nlocal api = require('gemini.api')\n\nlocal M = {}\n\nlocal context = {\n  namespace_id = vim.api.nvim_create_namespace('gemini_completion'),\n  completion = nil,\n}\n\nM.setup = function()\n  local model = config.get_config({ 'completion', 'model' })\n  if not model or not model.model_id then\n    return\n  end\n\n  vim.api.nvim_create_autocmd('CursorMovedI', {\n    callback = function()\n      M.gemini_complete()\n    end,\n  })\n\n  local trigger_completion_key = config.get_config({ 'completion', 'trigger_key' })\n  if trigger_completion_key then\n    vim.api.nvim_set_keymap('i', trigger_completion_key, '', {\n      callback = function()\n        M.gemini_complete()\n      end,\n    })\n  end\n\n  local insert_key = config.get_config({ 'completion', 'insert_result_key' }) or '<S-Tab>'\n  vim.api.nvim_set_keymap('i', insert_key, '', {\n    callback = function()\n      M.insert_completion_result()\n    end,\n  })\nend\n\nlocal get_prompt_text = function(bufnr, pos)\n  local get_prompt = config.get_config({ 'completion', 'get_prompt' })\n  if not get_prompt then\n    vim.notify('prompt function is not found', vim.log.levels.WARN)\n    return nil\n  end\n  return get_prompt(bufnr, pos)\nend\n\nlocal function handle_result(win, pos, json_text)\n  local model_response = vim.json.decode(json_text)\n  local text = util.table_get(model_response, { 'candidates', 1, 'content', 'parts', 1, 'text' })\n  if model_response ~= nil and #model_response > 0 then\n    vim.schedule(function()\n      if model_response then\n        local code_blocks = util.strip_code(text)\n        local response = vim.fn.join(code_blocks, '\\n\\n')\n        M.show_completion_result(response, win, pos)\n      end\n    end)\n  end\n\n  local error_msg = util.table_get(model_response, { 'error', 'message' })\n  if error_msg then\n    vim.schedule(function()\n      vim.api.nvim_echo({{ error_msg, 'WarningMsg' }}, false, {})\n    end)\n  end\nend\n\nM._gemini_complete = function()\n  local bufnr = vim.api.nvim_get_current_buf()\n  local win = vim.api.nvim_get_current_win()\n  local pos = vim.api.nvim_win_get_cursor(win)\n  local user_text = get_prompt_text(bufnr, pos)\n  if not user_text then\n    return\n  end\n\n  local system_text = nil\n  local get_system_text = config.get_config({ 'completion', 'get_system_text' })\n  if get_system_text then\n    system_text = get_system_text()\n  end\n\n  local generation_config = config.get_gemini_generation_config('completion')\n  local model_id = config.get_config({ 'completion', 'model', 'model_id' })\n  api.gemini_generate_content(user_text, system_text, model_id, generation_config, function(result)\n    local json_text = result.stdout\n    if json_text and #json_text > 0 then\n      handle_result(win, pos, json_text)\n    end\n  end)\nend\n\nM.gemini_complete = util.debounce(function()\n  local blacklist_filetypes = config.get_config({ 'completion', 'blacklist_filetypes' }) or {}\n  local blacklist_filenames = config.get_config({ 'completion', 'blacklist_filenames' }) or {}\n\n  local buf = vim.api.nvim_get_current_buf()\n  local filetype = vim.api.nvim_get_option_value('filetype', { buf = buf })\n  local filename = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(0), \":t\")\n  if util.is_blacklisted(blacklist_filetypes, filetype) or util.is_blacklisted(blacklist_filenames, filename) then\n    return\n  end\n\n  if vim.fn.mode() ~= 'i' then\n    return\n  end\n\n  local can_complete = config.get_config({'completion', 'can_complete'})\n  if not can_complete or not can_complete() then\n    return\n  end\n\n  print('-- gemini complete --')\n  M._gemini_complete()\nend, config.get_config({ 'completion', 'completion_delay' }) or 1000)\n\nM.show_completion_result = function(result, win_id, pos)\n  local win = vim.api.nvim_get_current_win()\n  if win ~= win_id then\n    return\n  end\n\n  local current_pos = vim.api.nvim_win_get_cursor(win)\n  if current_pos[1] ~= pos[1] or current_pos[2] ~= pos[2] then\n    return\n  end\n\n  if vim.fn.mode() ~= 'i' then\n    return\n  end\n\n  local can_complete = config.get_config({'completion', 'can_complete'})\n  if not can_complete or not can_complete() then\n    return\n  end\n\n  local bufnr = vim.api.nvim_get_current_buf()\n  local options = {\n    id = 1,\n    virt_text = {},\n    virt_lines = {},\n    hl_mode = 'combine',\n    virt_text_pos = 'inline',\n  }\n\n  local content = result:match(\"^%s*(.-)%s*$\")\n  for i, l in pairs(vim.split(content, '\\n')) do\n    if i == 1 then\n      options.virt_text[1] = { l, 'Comment' }\n    else\n      options.virt_lines[i - 1] = { { l, 'Comment' } }\n    end\n  end\n  local row = pos[1]\n  local col = pos[2]\n  local id = vim.api.nvim_buf_set_extmark(bufnr, context.namespace_id, row - 1, col, options)\n\n  context.completion = {\n    content = content,\n    row = row,\n    col = col,\n    bufnr = bufnr,\n  }\n\n  vim.api.nvim_create_autocmd({ 'CursorMoved', 'CursorMovedI', 'InsertLeavePre' }, {\n    buffer = bufnr,\n    callback = function()\n      context.completion = nil\n      vim.api.nvim_buf_del_extmark(bufnr, context.namespace_id, id)\n      vim.api.nvim_command('redraw')\n    end,\n    once = true,\n  })\nend\n\nM.insert_completion_result = function()\n  if not context.completion then\n    return\n  end\n\n  local bufnr = vim.api.nvim_get_current_buf()\n  if not context.completion.bufnr == bufnr then\n    return\n  end\n\n  local row = context.completion.row - 1\n  local col = context.completion.col\n  local first_line = vim.api.nvim_buf_get_lines(0, row, row + 1, false)[1]\n  local lines = vim.split(context.completion.content, '\\n')\n  lines[1] = string.sub(first_line, 1, col) .. lines[1] .. string.sub(first_line, col + 1)\n  vim.api.nvim_buf_set_lines(bufnr, row, row + 1, false, lines)\n\n  if config.get_config({ 'completion', 'move_cursor_end' }) == true then\n    local new_row = row + #lines\n    local new_col = #vim.api.nvim_buf_get_lines(0, new_row - 1, new_row, false)[1]\n    vim.api.nvim_win_set_cursor(0, { new_row, new_col })\n  end\n\n  context.completion = nil\nend\n\nreturn M\n"
  },
  {
    "path": "lua/gemini/config.lua",
    "content": "local api = require('gemini.api')\nlocal util = require('gemini.util')\n\nlocal M = {}\n\nlocal default_temperature = 0.06\nlocal default_top_k = 64\n\nlocal default_chat_config = {\n  model = {\n    model_id = api.MODELS.GEMINI_2_5_FLASH,\n    temperature = default_temperature,\n    top_k = default_top_k,\n  },\n  window = {\n    position = \"new_tab\",     -- left, right, new_tab, tab\n    width = 80,               -- number of columns of the left/right window\n  }\n}\n\nlocal default_instruction_config = {\n  model = {\n    model_id = api.MODELS.GEMINI_2_5_FLASH,\n    temperature = default_temperature,\n    top_k = default_top_k,\n  },\n  menu_key = '<Leader><Leader><Leader>g',\n  prompts = {\n    {\n      name = 'Unit Test',\n      command_name = 'GeminiUnitTest',\n      menu = 'Unit Test 🚀',\n      get_prompt = function(lines, bufnr)\n        local code = vim.fn.join(lines, '\\n')\n        local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n        local prompt = 'Context:\\n\\n```%s\\n%s\\n```\\n\\n'\n            .. 'Objective: Write unit test for the above snippet of code\\n'\n        return string.format(prompt, filetype, code)\n      end,\n    },\n    {\n      name = 'Code Review',\n      command_name = 'GeminiCodeReview',\n      menu = 'Code Review 📜',\n      get_prompt = function(lines, bufnr)\n        local code = vim.fn.join(lines, '\\n')\n        local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n        local prompt = 'Context:\\n\\n```%s\\n%s\\n```\\n\\n'\n            .. 'Objective: Do a thorough code review for the following code.\\n'\n            .. 'Provide detail explaination and sincere comments.\\n'\n        return string.format(prompt, filetype, code)\n      end,\n    },\n    {\n      name = 'Code Explain',\n      command_name = 'GeminiCodeExplain',\n      menu = 'Code Explain',\n      get_prompt = function(lines, bufnr)\n        local code = vim.fn.join(lines, '\\n')\n        local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n        local prompt = 'Context:\\n\\n```%s\\n%s\\n```\\n\\n'\n            .. 'Objective: Explain the following code.\\n'\n            .. 'Provide detail explaination and sincere comments.\\n'\n        return string.format(prompt, filetype, code)\n      end,\n    },\n  }\n}\n\nlocal default_completion_config = {\n  model = {\n    model_id = api.MODELS.GEMINI_2_5_FLASH,\n    temperature = default_temperature,\n    top_k = default_top_k,\n  },\n  blacklist_filetypes = { 'help', 'qf', 'json', 'yaml', 'toml', 'xml', 'ini' },\n  blacklist_filenames = { '.env' },\n  completion_delay = 800,\n  insert_result_key = '<S-Tab>',\n  move_cursor_end = true,\n  can_complete = function()\n    return vim.fn.pumvisible() ~= 1\n  end,\n  get_system_text = function()\n    return \"I need you to act as a pure code-completion tool.\"\n  end,\n  get_prompt = function(bufnr, pos)\n    local abs_path = vim.api.nvim_buf_get_name(bufnr)\n    local filename = vim.fn.fnamemodify(abs_path, ':.')\n    local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n\n    local radius = 10\n    local row = pos[1]\n    local col = pos[2]\n\n    local start_line = math.max(1, row - radius)\n    local line_count = vim.api.nvim_buf_line_count(bufnr)\n    local end_line = math.min(line_count, row + radius)\n    local lines = vim.api.nvim_buf_get_lines(bufnr, start_line, end_line, false)\n\n    local prompt = 'I have a %s file %s. Below is a snippet around the missing code block.\\n'\n    prompt = string.format(prompt, filetype, filename)\n    prompt = prompt .. '[CODE BEFORE GAP]\\n'\n\n    if start_line > 1 then\n      prompt = prompt .. \"// ... (earlier code omitted)\\n\"\n    end\n\n    for i, line in ipairs(lines) do\n      local current_buffer_row = start_line + i - 1\n      if current_buffer_row == row then\n        local line_prefix = line:sub(1, col)\n        local line_suffix = line:sub(col + 1)\n        prompt = prompt .. line_prefix .. '\\n\\n[CODE AFTER GAP]\\n' .. line_suffix .. '\\n'\n      else\n        prompt = prompt .. line .. '\\n'\n      end\n    end\n\n    if end_line < line_count then\n      prompt = prompt .. \"// ... (later code omitted)\\n\"\n    end\n\n    prompt = prompt .. '\\n[INSTRUCTION]\\n'\n    prompt = prompt .. 'Generate EXACTLY the code that fills the gap between the two blocks. '\n    prompt = prompt .. 'Return ONLY the code. No explanation. Stop immediately when the logic is complete.\\n'\n    return prompt\n  end\n}\n\nlocal default_task_config = {\n  model = {\n    model_id = api.MODELS.GEMINI_2_5_FLASH,\n    temperature = default_temperature,\n    top_k = default_top_k,\n  },\n  get_system_text = function()\n    return 'You are an AI assistant that helps user write code.'\n      .. '\\n* You should output the new content for the Current Opened File'\n  end,\n  get_prompt = function(bufnr, user_prompt)\n    local buffers = vim.api.nvim_list_bufs()\n    local file_contents = {}\n\n    for _, b in ipairs(buffers) do\n      if vim.api.nvim_buf_is_loaded(b) then -- Only get content from loaded buffers\n        local lines = vim.api.nvim_buf_get_lines(b, 0, -1, false)\n        local abs_path = vim.api.nvim_buf_get_name(b)\n        local filename = vim.fn.fnamemodify(abs_path, ':.')\n        local filetype = vim.api.nvim_get_option_value('filetype', { buf = b })\n        local file_content = table.concat(lines, \"\\n\")\n        file_content = string.format(\"`%s`:\\n\\n```%s\\n%s\\n```\\n\\n\", filename, filetype, file_content)\n        table.insert(file_contents, file_content)\n      end\n    end\n\n    local current_filepath = vim.api.nvim_buf_get_name(bufnr)\n    current_filepath = vim.fn.fnamemodify(current_filepath, \":.\")\n\n    local context = table.concat(file_contents, \"\\n\\n\")\n    return string.format('%s\\n\\nCurrent Opened File: %s\\n\\nTask: %s',\n      context, current_filepath, user_prompt)\n  end\n}\n\nM.set_config = function(opts)\n  opts = opts or {}\n\n  M.config = {\n    chat = vim.tbl_deep_extend('force', {}, default_chat_config, opts.chat_config or {}),\n    completion = vim.tbl_deep_extend('force', {}, default_completion_config, opts.completion or {}),\n    instruction = vim.tbl_deep_extend('force', {}, default_instruction_config, opts.instruction or {}),\n    task = vim.tbl_deep_extend('force', {}, default_task_config, opts.task or {})\n  }\nend\n\nM.get_config = function(keys)\n  return util.table_get(M.config, keys)\nend\n\nM.get_gemini_generation_config = function(space)\n  return {\n    temperature = M.get_config({ space, 'model', 'temperature' }) or default_temperature,\n    topK = M.get_config({ space, 'model', 'top_k' }) or default_top_k,\n    response_mime_type = 'text/plain',\n    thinkingConfig = {\n      thinkingBudget = 0\n    }\n  }\nend\n\nreturn M\n"
  },
  {
    "path": "lua/gemini/instruction.lua",
    "content": "local config = require('gemini.config')\nlocal util = require('gemini.util')\nlocal api = require('gemini.api')\n\nlocal M = {}\n\nM.setup = function()\n  local model = config.get_config({ 'instruction', 'model' })\n  if not model or not model.model_id then\n    return\n  end\n\n  local register_menu = function(prompt_item)\n    local get_prompt = prompt_item.get_prompt\n    if not get_prompt then\n      return\n    end\n\n    local command_name = prompt_item.command_name\n    if not command_name then\n      return\n    end\n\n    local gemini_generate = function(context)\n      local bufnr = vim.api.nvim_get_current_buf()\n      local lines\n      if not context.line1 or not context.line2 or context.line1 == context.line2 then\n        lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)\n      else\n        lines = vim.api.nvim_buf_get_lines(bufnr, context.line1 - 1, context.line2 - 1, false)\n      end\n      local user_text = get_prompt(lines, bufnr)\n\n      local generation_config = config.get_gemini_generation_config('instruction')\n\n      vim.api.nvim_command('tabnew')\n      local new_buf = vim.api.nvim_get_current_buf()\n      vim.api.nvim_set_option_value('filetype', 'markdown', { buf = new_buf })\n      local model_id = config.get_config({ 'instruction', 'model', 'model_id' })\n      local text = ''\n      api.gemini_generate_content_stream(user_text, model_id, generation_config, function(json_text)\n        local model_response = vim.json.decode(json_text)\n        model_response = util.table_get(model_response, { 'candidates', 1, 'content', 'parts', 1, 'text' })\n        text = text .. model_response\n        vim.schedule(function()\n          lines = vim.split(text, '\\n')\n          vim.api.nvim_buf_set_lines(new_buf, 0, -1, false, lines)\n        end)\n      end)\n    end\n\n    vim.api.nvim_create_user_command(command_name, gemini_generate, {\n      range = true,\n    })\n\n    local menu = prompt_item.menu\n    if menu then\n      menu = menu:gsub(' ', '\\\\ ')\n      vim.api.nvim_command('nnoremenu Gemini.' .. menu .. ' :' .. command_name .. '<CR>')\n    end\n  end\n\n  for _, item in pairs(config.get_config({ 'instruction', 'prompts' }) or {}) do\n    register_menu(item)\n  end\n\n  local register_keymap = function(mode, keymap)\n    vim.api.nvim_set_keymap(mode, keymap, '', {\n      expr = true,\n      noremap = true,\n      silent = true,\n      callback = function()\n        if vim.fn.pumvisible() == 0 then\n          vim.api.nvim_command('popup Gemini')\n        end\n      end\n    })\n  end\n\n  local modes = { 'n' }\n  for _, mode in pairs(modes) do\n    register_keymap(mode, config.get_config({ 'instruction', 'menu_key' }) or '<Leader><Leader><Leader>g')\n  end\nend\n\nreturn M\n"
  },
  {
    "path": "lua/gemini/task.lua",
    "content": "local config = require('gemini.config')\nlocal api = require('gemini.api')\nlocal util = require('gemini.util')\n\nlocal M = {}\n\nlocal context = {\n  bufnr = nil,\n  model_response = nil,\n  tmpfile = nil,\n}\n\nM.setup = function()\n  local model = config.get_config({ 'task', 'model' })\n  if not model or not model.model_id then\n    return\n  end\n\n  vim.api.nvim_create_user_command('GeminiTask', M.run_task, {\n    force = true,\n    desc = 'Google Gemini',\n    nargs = 1,\n  })\n\n  vim.api.nvim_create_user_command('GeminiApply', M.apply_patch, {\n    force = true,\n    desc = 'Apply patch',\n  })\nend\n\nlocal get_prompt_text = function(bufnr, user_prompt)\n  local get_prompt = config.get_config({ 'task', 'get_prompt' })\n  if not get_prompt then\n    vim.notify('prompt function is not found', vim.log.levels.WARN)\n    return nil\n  end\n  return get_prompt(bufnr, user_prompt)\nend\n\nlocal function open_file_in_split(filepath, ft)\n  local bufnr = vim.fn.bufnr(filepath, true)\n  if bufnr == 0 then\n    print(\"Error: Could not find or create buffer for file: \" .. filepath)\n    return\n  end\n  vim.api.nvim_set_option_value('filetype', ft, {buf = bufnr})\n\n  local win_id = vim.api.nvim_open_win(bufnr, false, {\n    split = 'right',\n    win = 0,\n  })\n  vim.api.nvim_set_current_win(win_id)\n\n  vim.api.nvim_set_option_value('diff', true, { win = win_id })\n  vim.api.nvim_set_option_value('scrollbind', true, { win = win_id })\n  vim.api.nvim_set_option_value('cursorbind', true, { win = win_id })\nend\n\nlocal function diff_with_current_file(bufnr, new_content)\n  local tmpfile = vim.fn.tempname()\n\n  -- Write to the temp file\n  local f = io.open(tmpfile, \"w\")\n  if f then\n    f:write(new_content)\n    f:close()\n  end\n\n\n  local ft = vim.api.nvim_get_option_value('filetype', {buf = bufnr})\n  open_file_in_split(vim.fn.fnameescape(tmpfile), ft)\n  return tmpfile\nend\n\nM.run_task = function(ctx)\n  local bufnr = vim.api.nvim_get_current_buf()\n  local user_prompt = ctx.args\n  local prompt = get_prompt_text(bufnr, user_prompt)\n\n  local system_text = nil\n  local get_system_text = config.get_config({ 'task', 'get_system_text' })\n  if get_system_text then\n    system_text = get_system_text()\n  end\n\n  print('-- running Gemini Task...')\n  local generation_config = config.get_gemini_generation_config('task')\n  local model_id = config.get_config({ 'task', 'model', 'model_id' })\n  api.gemini_generate_content(prompt, system_text, model_id, generation_config, function(result)\n    local json_text = result.stdout\n    if json_text and #json_text > 0 then\n      local model_response = vim.json.decode(json_text)\n      model_response = util.table_get(model_response, { 'candidates', 1, 'content', 'parts', 1, 'text' })\n      if model_response ~= nil and #model_response > 0 then\n        model_response = util.strip_code(model_response)\n        vim.schedule(function()\n          model_response = vim.fn.join(model_response, '\\n')\n          if #model_response then\n            context.bufnr = bufnr\n            context.model_response = model_response\n            context.tmpfile = diff_with_current_file(bufnr, model_response)\n          end\n        end)\n      end\n    end\n  end)\nend\n\nlocal function close_split_by_filename(tmpfile)\n  -- Get the buffer number for the temp file\n  local bufnr = vim.fn.bufnr(tmpfile)\n  if bufnr == -1 then\n    print(\"No buffer found for file: \" .. tmpfile)\n    return\n  end\n\n  -- Find the window displaying this buffer\n  for _, win in ipairs(vim.api.nvim_list_wins()) do\n    if vim.api.nvim_win_get_buf(win) == bufnr then\n      vim.api.nvim_win_close(win, true)  -- force close the window\n      vim.api.nvim_buf_delete(bufnr, { force = true, unload = true })\n      return\n    end\n  end\n  print(\"No window found showing the buffer for file: \" .. tmpfile)\nend\n\nM.apply_patch = function()\n  if not context.bufnr or not context.tmpfile then\n    vim.notify('No Gemini task to apply.', vim.log.levels.WARN)\n    return\n  end\n\n  print('-- apply changes from Gemini')\n\n  local tmp_bufnr = vim.fn.bufnr(vim.fn.fnamemodify(context.tmpfile, ':p'))\n  if tmp_bufnr == -1 then\n    vim.notify('Could not find the temporary buffer for edited changes.', vim.log.levels.ERROR)\n    return\n  end\n\n  local edited_lines = vim.api.nvim_buf_get_lines(tmp_bufnr, 0, -1, false)\n\n  vim.api.nvim_buf_set_lines(context.bufnr, 0, -1, false, edited_lines)\n\n  if context.tmpfile then\n    close_split_by_filename(context.tmpfile)\n  end\n\n  context.bufnr = nil\n  context.model_response = nil\n  context.tmpfile = nil\nend\n\nreturn M\n"
  },
  {
    "path": "lua/gemini/util.lua",
    "content": "local M = {}\n\nM.borderchars = { '─', '│', '─', '│', '╭', '╮', '╯', '╰' }\n\nM.open_window = function(content, options)\n  local popup = require('plenary.popup')\n  options.borderchars = M.borderchars\n  local win_id, result = popup.create(content, options)\n  local bufnr = vim.api.nvim_win_get_buf(win_id)\n  local border = result.border\n  vim.api.nvim_set_option_value('ft', 'markdown', { buf = bufnr })\n  vim.api.nvim_set_option_value('wrap', true, { win = win_id })\n\n  local close_popup = function()\n    vim.api.nvim_win_close(win_id, true)\n  end\n\n  local keys = { '<C-q>', 'q' }\n  for _, key in pairs(keys) do\n    vim.api.nvim_buf_set_keymap(bufnr, 'n', key, '', {\n      silent = true,\n      callback = close_popup,\n    })\n  end\n  return win_id, bufnr, border\nend\n\nM.treesitter_has_lang = function(bufnr)\n  local filetype = vim.api.nvim_get_option_value('filetype', { buf = bufnr })\n  local lang = vim.treesitter.language.get_lang(filetype)\n  return lang ~= nil\nend\n\nM.find_node_by_type = function(node_type)\n  local node = vim.treesitter.get_node()\n  while node do\n    local type = node:type()\n    if string.find(type, node_type) then\n      return node\n    end\n\n    local parent = node:parent()\n    if parent == node then\n      break\n    end\n    node = parent\n  end\n  return nil\nend\n\nM.debounce = function(callback, timeout)\n  local timer = nil\n  local f = function(...)\n    local t = { ... }\n    local handler = function()\n      callback(unpack(t))\n    end\n\n    if timer ~= nil then\n      timer:stop()\n    end\n    timer = vim.defer_fn(handler, timeout)\n  end\n  return f\nend\n\nM.table_get = function(t, id)\n  if type(id) ~= 'table' then return M.table_get(t, { id }) end\n  local success, res = true, t\n  for _, i in ipairs(id) do\n    success, res = pcall(function() return res[i] end)\n    if not success or res == nil then return end\n  end\n  return res\nend\n\nM.is_blacklisted = function(blacklist, filetype)\n  for _, ft in ipairs(blacklist) do\n    if string.find(filetype, ft, 1, true) ~= nil then\n      return true\n    end\n  end\n  return false\nend\n\nM.strip_code = function(text)\n  local code_blocks = {}\n  if not text then\n    return code_blocks\n  end\n\n  local pattern = \"```(%w+)%s*(.-)%s*```\"\n  for _, code_block in text:gmatch(pattern) do\n    table.insert(code_blocks, code_block)\n  end\n  if #code_blocks == 0 then\n    return { text }\n  end\n  return code_blocks\nend\n\nreturn M\n"
  },
  {
    "path": "lua/gemini.lua",
    "content": "local config = require('gemini.config')\n\nlocal M = {}\n\nlocal function is_nvim_version_ge(major, minor, patch)\n  local v = vim.version()\n  if v.major > major then\n    return true\n  elseif v.major == major then\n    if v.minor > minor then\n      return true\n    elseif v.minor == minor and v.patch >= patch then\n      return true\n    end\n  end\n  return false\nend\n\nM.setup = function(opts)\n  if not vim.fn.executable('curl') then\n    vim.notify('curl is not found', vim.log.levels.WARN)\n    return\n  end\n\n  if not is_nvim_version_ge(0, 10, 0) then\n    vim.notify('neovim version too old', vim.log.levels.WARN)\n    return\n  end\n\n  config.set_config(opts)\n\n  require('gemini.chat').setup()\n  require('gemini.instruction').setup()\n  require('gemini.completion').setup()\n  require('gemini.task').setup()\nend\n\nreturn M\n"
  },
  {
    "path": "tests/gemini/api_spec.lua",
    "content": "local api = require('gemini.api')\nlocal util = require('gemini.util')\n\ndescribe('api', function()\n  it('should send message', function()\n    local generation_config = {\n      temperature = 0.9,\n      top_k = 1.0,\n      max_output_tokens = 2048,\n      response_mime_type = 'text/plain',\n    }\n    local future = api.gemini_generate_content('hello there', nil, api.MODELS.GEMINI_2_0_FLASH, generation_config, nil)\n    local result = future:wait()\n    local stdout = result.stdout\n    print(stdout)\n    assert(#stdout > 0)\n\n    local result = vim.json.decode(stdout)\n    local model_response = util.table_get(result, { 'candidates', 1, 'content',\n      'parts', 1, 'text' })\n    assert(#model_response > 0)\n  end)\n\n  it('should send long message', function()\n    local generation_config = {\n      temperature = 0.9,\n      top_k = 1.0,\n      max_output_tokens = 2048,\n      response_mime_type = 'text/plain',\n    }\n    local long_message = string.rep('this is a very very long message ', 3000)\n    local future = api.gemini_generate_content(long_message, nil, api.MODELS.GEMINI_2_0_FLASH, generation_config, nil)\n    local result = future:wait()\n    local stdout = result.stdout\n    print(stdout)\n    assert(#stdout > 0)\n\n    local result = vim.json.decode(stdout)\n    local model_response = util.table_get(result, { 'candidates', 1, 'content',\n      'parts', 1, 'text' })\n    assert(#model_response > 0)\n  end)\nend)\n"
  },
  {
    "path": "tests/init.lua",
    "content": "local plenary_dir = os.getenv(\"PLENARY_DIR\") or \"/tmp/plenary.nvim\"\nlocal is_not_a_directory = vim.fn.isdirectory(plenary_dir) == 0\nif is_not_a_directory then\n  vim.fn.system({\"git\", \"clone\", \"https://github.com/nvim-lua/plenary.nvim\", plenary_dir})\nend\n\nvim.opt.rtp:append(\".\")\nvim.opt.rtp:append(plenary_dir)\n\nvim.cmd(\"runtime plugin/plenary.vim\")\nrequire(\"plenary.busted\")\n"
  }
]