diff --git a/lua/helpers.lua b/lua/helpers.lua index 1fa5ba7..b55f9a6 100644 --- a/lua/helpers.lua +++ b/lua/helpers.lua @@ -16,17 +16,24 @@ end helpers.edit_cf('h', '/lua/helpers.lua') ----@param opts? { cmd?: string } -helpers.open_term = function(opts) - opts = opts or { cmd = '' } +---@param opts? { cmd?: string, win_opts?: table, buf_opts?: table } +helpers.open_term = function(opts, buf_opts) + opts = opts or {} + local cmd = opts.cmd or '' + local win_opts = opts.win_opts + local buf_options = buf_opts or opts.buf_opts or { + bufhidden = 'wipe', + modifiable = false, + } local buf = vim.api.nvim_create_buf(false, true) - vim.api.nvim_set_option_value('bufhidden', 'wipe', { buf = buf }) - vim.api.nvim_set_option_value('modifiable', false, { buf = buf }) + for k, v in pairs(buf_options) do + vim.api.nvim_set_option_value(k, v, { buf = buf }) + end local height = math.ceil(vim.o.lines * 0.9) local width = math.ceil(vim.o.columns * 0.9) - local win = vim.api.nvim_open_win(buf, true, { + local win = vim.api.nvim_open_win(buf, true, win_opts or { style = 'minimal', relative = 'editor', width = width, @@ -38,7 +45,7 @@ helpers.open_term = function(opts) vim.api.nvim_set_current_win(win) - vim.fn.jobstart(opts.cmd, { + vim.fn.jobstart(cmd, { term = true, on_exit = function(_, _, _) if vim.api.nvim_win_is_valid(win) then @@ -154,4 +161,3 @@ end return helpers -- nnoremap ev :tabedit $MYVIMRC - diff --git a/lua/keymap.lua b/lua/keymap.lua index 7e1c73c..db081d5 100644 --- a/lua/keymap.lua +++ b/lua/keymap.lua @@ -121,6 +121,11 @@ vim.keymap.set('n', '', '', { desc = 'Move focus to the right win vim.keymap.set('n', '', (win_or_treesj)('j', 'Move focus to the lower window or treesj.toggle()')) vim.keymap.set('n', '', (win_or_treesj)('k', 'Move focus to the upper window or treesj.toggle()')) +vim.keymap.set('t', '', 'h', { desc = 'Move focus to the left window' }) +vim.keymap.set('t', '', 'l', { desc = 'Move focus to the right window' }) +vim.keymap.set('t', '', 'j', { desc = 'Move focus to the lower window' }) +vim.keymap.set('t', '', 'k', { desc = 'Move focus to the upper window' }) + vim.keymap.set({ 'i' }, '', function() local ls = require 'luasnip' if ls.choice_active() then @@ -177,8 +182,54 @@ vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', function() end, { desc = 'Search within the whole project' }) vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 'Telescope quickfix', { desc = 'Show quickfix list' }) vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 'Telescope quickfixhistory', { desc = 'Show quickfix history' }) -vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 'CodeCompanionChat Toggle', { desc = 'Open AI Actions' }) -vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 'CodeCompanionActions', { desc = 'Open AI Actions' }) +vim.keymap.set({ 'n', 'v', 'c', 'i', 't' }, '', function() + local ai_tools = { 'codex', 'agy', 'opencode' } + local ai_win_opts = { + width = math.floor(vim.o.columns / 3), + vertical = true, + split = 'right', + } + + local term_bufnr = -1 + for _, bufnr in ipairs(vim.api.nvim_list_bufs()) do + if vim.bo[bufnr].buftype == 'terminal' then + local name = vim.api.nvim_buf_get_name(bufnr) + + for _, tool in ipairs(ai_tools) do + if name:match(':' .. tool .. '$') or name:match('/' .. tool .. '$') then + term_bufnr = bufnr + end + end + end + end + + if term_bufnr ~= -1 then + local term_winid = vim.fn.bufwinid(term_bufnr) + if term_winid ~= -1 then + vim.api.nvim_win_hide(term_winid) + return + end + + vim.api.nvim_open_win(term_bufnr, true, ai_win_opts) + vim.cmd.startinsert() + return + end + for _, tool in ipairs(ai_tools) do + if vim.fn.executable(tool) == 1 then + helpers.open_term { + cmd = tool, + win_opts = ai_win_opts, + buf_opts = { + bufhidden = 'hide', + modifiable = false, + }, + } + return + end + end + vim.api.nvim_echo({ { 'No AI tool found. Please install one of the following: codex, agy, opencode', 'WarningMsg' } }, false, {}) +end, { desc = 'Toggle AI tool' }) +-- vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', 'vsplit term://codexi', { desc = 'Open AI tool' }) vim.keymap.set({ 'n', 'v', 'c', 'i' }, '', function() require('snacks').scratch() end, { desc = 'Open scratchpad' }) @@ -398,3 +449,4 @@ vim.keymap.set('n', 'cr', function() end, { desc = 'Open RegExp cheat sheet' }) require('helpers').edit_cf('k', '/lua/keymap.lua') + diff --git a/lua/plugins/aider.lua b/lua/plugins/aider.lua deleted file mode 100644 index 9ba68c4..0000000 --- a/lua/plugins/aider.lua +++ /dev/null @@ -1,67 +0,0 @@ -require('helpers').edit_cf('pa', '/lua/plugins/aider.lua') - -local change_model_function = function(model) - return function() - require('nvim_aider').api.send_command('/model', model) - end -end - -return { - 'GeorgesAlkhouri/nvim-aider', - cmd = 'Aider', - keys = { - { 'a/', 'Aider toggle', desc = 'Toggle Aider' }, - { 'as', 'Aider send', desc = 'Send to Aider', mode = { 'n', 'v' } }, - { 'ac', 'Aider command', desc = 'Aider Commands' }, - { 'ab', 'Aider buffer', desc = 'Send Buffer' }, - { 'a+', 'Aider add', desc = 'Add File' }, - { 'a-', 'Aider drop', desc = 'Drop File' }, - { 'ar', 'Aider add readonly', desc = 'Add Read-Only' }, - { 'aR', 'Aider reset', desc = 'Reset Session' }, - -- Example nvim-tree.lua integration if needed - { 'a+', 'AiderTreeAddFile', desc = 'Add File from Tree to Aider', ft = 'NvimTree' }, - { 'a-', 'AiderTreeDropFile', desc = 'Drop File from Tree from Aider', ft = 'NvimTree' }, - { 'am4', change_model_function 'gpt-4.1', desc = 'Switch aider model to GPT-4.1' }, - { 'amo', change_model_function 'openai/o4-mini', desc = 'Switch aider model to o4-mini' }, - { 'amg', change_model_function 'openai/gemini-2.5-pro', desc = 'Switch aider model to Gemini 2.5 Pro' }, - { 'ams', change_model_function 'openai/claude-sonnet-4', desc = 'Switch aider model to Claude Sonnet 4' }, - }, - dependencies = { - 'folke/snacks.nvim', - --- The below dependencies are optional - 'catppuccin/nvim', - 'nvim-tree/nvim-tree.lua', - --- Neo-tree integration - { - 'nvim-neo-tree/neo-tree.nvim', - opts = function(_, opts) - -- Example mapping configuration (already set by default) - -- opts.window = { - -- mappings = { - -- ["+"] = { "nvim_aider_add", desc = "add to aider" }, - -- ["-"] = { "nvim_aider_drop", desc = "drop from aider" } - -- ["="] = { "nvim_aider_add_read_only", desc = "add read-only to aider" } - -- } - -- } - require('nvim_aider.neo_tree').setup(opts) - end, - }, - }, - config = function() - require('nvim_aider').setup { - aider_cmd = 'aider', - args = { - '--config=$HOME/.config/aider/aider.yaml', - '--env-file=$(pwd)/aider.env', - '--watch', - '--architect', - }, - auto_reload = true, - win = { - wo = { winbar = 'Aider' }, - style = 'nvim_aider', - position = 'bottom', - }, - } - end, -} diff --git a/lua/plugins/codecompanion.lua b/lua/plugins/codecompanion.lua deleted file mode 100644 index ada360f..0000000 --- a/lua/plugins/codecompanion.lua +++ /dev/null @@ -1,339 +0,0 @@ -return { - 'olimorris/codecompanion.nvim', - config = function() - require('codecompanion').setup { - adapters = { - http = { - default = function() - if (require('helpers').has_copilot()) then - return require('codecompanion.adapters').extend('copilot', { - schema = { - model = { - default = vim.env.DEFAULT_AI_MODEL, - }, - max_tokens = { - default = 1000000, - }, - } - }) - end - return require('codecompanion.adapters').extend('openai_compatible', { - env = { - url = vim.env.DEFAULT_OPENAI_API_BASE, - api_key = vim.env.DEFAULT_OPENAI_API_KEY, - chat_url = '/v1/chat/completions', - models_endpoint = '/v1/models', - }, - schema = { - model = { - default = vim.env.DEFAULT_AI_MODEL, - }, - max_tokens = { - default = 1000000, - }, - }, - }) - end, - }, - }, - display = { - chat = { - show_settings = true, - start_in_insert_mode = false, - }, - }, - strategies = { - chat = { - adapter = 'default', - slash_commands = { - -- codebase = require('vectorcode.integrations').codecompanion.chat.make_slash_command(), - }, - tools = { - -- vectorcode = { - -- description = 'Run VectorCode to retrieve the project context.', - -- callback = require('vectorcode.integrations').codecompanion.chat.make_tool(), - -- }, - ['cmd_runner'] = { - opts = { - requires_approval = false, - }, - }, - }, - roles = { - ---@type string|fun(adapter: CodeCompanion.Adapter): string - llm = function(adapter) - return 'CodeCompanion (' .. adapter.formatted_name .. ': ' .. adapter.parameters.model .. ')' - end, - }, - keymaps = { - send = { - modes = { n = '', i = '' }, - }, - close = { - modes = { n = '', i = '' }, - }, - }, - }, - inline = { - adapter = { - name = 'default', - model = vim.env.FAST_MODEL, - }, - }, - cmd = { - adapter = { - name = 'default', - model = vim.env.FAST_MODEL, - }, - }, - }, - extensions = { - mcphub = { - callback = 'mcphub.extensions.codecompanion', - opts = { - show_result_in_chat = true, - make_vars = true, - make_slash_commands = true, - }, - }, - }, - system_prompt = function(opts) - local language = opts.language or 'English' - return string.format( - [[You are an AI programming assistant named "CodeCompanion". You are currently plugged into the Neovim text editor on a user's machine. - -Your core tasks include: -- Answering general programming questions. -- Explaining how the code in a Neovim buffer works. -- Reviewing the selected code from a Neovim buffer. -- Generating unit tests for the selected code. -- Proposing fixes for problems in the selected code. -- Scaffolding code for a new workspace. -- Finding relevant code to the user's query. -- Proposing fixes for test failures. -- Answering questions about Neovim. -- Running tools. - -You must: -- Follow the user's requirements carefully and to the letter. -- Keep your answers short and impersonal, especially if the user's context is outside your core tasks. -- Minimize additional prose unless clarification is needed. -- Use Markdown formatting in your answers. -- Include the programming language name at the start of each Markdown code block. -- Avoid including line numbers in code blocks. -- Avoid wrapping the whole response in triple backticks. -- Only return code that's directly relevant to the task at hand. You may omit code that isn’t necessary for the solution. -- Avoid using H1, H2 or H3 headers in your responses as these are reserved for the user. -- Use actual line breaks in your responses; only use "\n" when you want a literal backslash followed by 'n'. -- All non-code text responses must be written in the %s language indicated. -- Multiple, different tools can be called as part of the same response. -- Use full path names when using tools to read and modify files. - -When given a task: -1. Think step-by-step and, unless the user requests otherwise or the task is very simple, describe your plan in detailed pseudocode. -2. Output the final code in a single code block, ensuring that only relevant code is included. -3. End your response with a short suggestion for the next user turn that directly supports continuing the conversation. -4. Provide exactly one complete reply per conversation turn. -5. If necessary, execute multiple tools in a single turn.]], - language - ) - end, - prompt_library = { - ['Code Expert'] = { - strategy = 'chat', - description = 'Get some special advice from an LLM.', - opts = { - mapping = 'ce', - modes = { 'v' }, - short_name = 'expert', - auto_submit = true, - stop_context_insertion = true, - user_prompt = true, - }, - prompts = { - { - role = 'system', - content = function(context) - return 'I want you to act as a senior' - .. context.filetype - .. 'developer I will ask you specific questions and I want you to return concise explanations and codeblock examples.' - end, - }, - { - role = 'user', - content = function(context) - local text = require('codecompanion.helpers.actions').get_code(context.start_line, context.end_line) - - return 'I have the following code:\n\n```' .. context.filetype .. '\n' .. text .. '\n```\n\n' - end, - opts = { - contains_code = true, - }, - }, - }, - }, - ['Games Master'] = { - strategy = 'chat', - description = 'A personal Games Master Assistant.', - opts = { - user_prompt = false, - }, - prompts = { - { - role = 'system', - content = [[ -You are a personal Games Master Assistant. You are currently plugged in to the Neovim text editor on a user's machine. - -Your core tasks include: -- Crafting engaging role playing sessions -- Building immersive and authentic worlds -- Creating compelling characters for the players to interact with and drive the story -- Reviewing session summaries and building on them -- Providing advice on how to bring sessions to life -- Create exciting encounters to challenge the players - - Encounters can be combat, social, or exploration based - - There should always be a story reason for the encounter -- Combine player character backgrounds and motivations into the world -- Build tension and drama into the game -- Ensure each session provides opportunities for the players to engage with and drive the story - -You must: -- Follow the user's requirements carefully and to the letter. -- Keep answers focused on the task at hand. -- Provide original ideas and suggestions. -- Use Markdown formatting in your answers. -- Use actual line breaks instead of '\n' in your response to begin new lines. -- Use '\n' only when you want a literal backslash followed by a character 'n'. -- All responses must be written in English. -- Multiple, different tools can be called as part of the same response. -- Help sessions be fast paced and fun. -- Ensure there are multiple soltuions to each problem. -- Avoid railroading the players. - -When given a task: -1. Consider the existing world and characters. Use tools to gather information that may be relevant. -2. Provide exactly one complete reply per conversation turn. -3. If necessary, execute multiple tools in a single turn. -]], - }, - { - role = 'user', - content = '', - }, - }, - }, - ['PHPStan Fixer'] = { - strategy = 'workflow', - description = 'Use a workflow to fix PHPStan errors until there are none left.', - opts = { - short_name = 'phpstan', - }, - prompts = { - { - { - name = 'Run PHPStan', - role = 'user', - opts = { auto_submit = false }, - content = function() - -- Enable turbo mode!!! - vim.g.codecompanion_auto_tool_mode = true - - return [[PHPStan is a static analysis tool for PHP. It is currently reporting errors in the code. Your task is to fix these errors and run PHPStan again until there are no errors left. - -First of all use the @cmd_runner tool to run the `composer type-check` command. This will run PHPStan and output type errors in the code.]] - end, - }, - }, - { - { - name = 'Fetch files', - role = 'user', - opts = { auto_submit = false }, - content = function() - -- Enable turbo mode!!! - vim.g.codecompanion_auto_tool_mode = true - - return 'PHPStan has reported errors. Look at the output and see where the files are reported. Use the @mcp tool to read all the offending files so you can implement the fixes.' - end, - }, - }, - { - { - name = 'Fix errors and run', - role = 'user', - opts = { auto_submit = false }, - content = function() - -- Enable turbo mode!!! - vim.g.codecompanion_auto_tool_mode = true - - return [[### Instructions -Now you have the errors and the appropriate context you can fix the errors. - -### Steps to Follow - -You are required to analyse the PHPStan errors and write code to fix them. - -Reason through the errors and write out the possible causes and fixes for each. - -1. Then use the @mcp tool to update the files with the fixes. When editing files use the full path name including the project name /Users/chris/Code/Sites/hylark/ -2. After editing the files use the @cmd_runner tool again to run the `composer type-check` command to see if any errors remain. - -We'll repeat this cycle until there are no errors. Ensure no deviations from these steps. - -### Tips for fixing PHPStan errors -- Do not ignore the errors -- Use Webmozart Assert library to narrow types -- Most errors are due to missing docblocks or calling methods/parameters on types that PHPStan cannot infer -- Use fully qualified namespaces for classes in doc blocks -- The code is working so fixes should not change the behaviour of the code -- There are some custom types in this project that help define types - - closure creates T|Closure(): T, you can add a second argument to specify the depth so closure creates T|Closure(): T|Closure(): (Closure(): T). This is useful because GraphQL queries can return closures. - - promise creates T|SyncPromise, as with closure, you can add a second argument for depth. You can also add a third argument if T is an array to allow adding promises to each value, so promise will create array{a: int}|SyncPromise|array{a: SyncPromise}|SyncPromise}>. This is useful because GraphQL queries can return promises and arrays of promises. - - GraphQL schema types (these should only be used in the Query classes) - - GArgs will look at the GraphQL schema and create an array type for the arguments of a field on one of the root types (Query or Mutation) - - GArgs will create a type for the arguments of a field on a type - - GType will create an type for a specific GraphQL type, input, interface, or enum - - GVal will create a type for the return value of a field on one of the root types (Query or Mutation) - - GVal will create a type for the return value of a field on a type - - pick will create an array from T with only the keys in TKeys (e.g. pick will create array{a: int, b: int}) - - except works like pick only it excludes the keys in TKeys (e.g. except will create array{c: int}) - - union creates a union array of T and U (e.g. union will create array{a: int, b: int})]] - end, - }, - }, - { - { - name = 'Repeat On Failure', - role = 'user', - opts = { auto_submit = false }, - -- Scope this prompt to the cmd_runner tool - condition = function() - return _G.codecompanion_current_tool == 'cmd_runner' - end, - -- Repeat until the tests pass, as indicated by the testing flag - -- which the cmd_runner tool sets on the chat buffer - repeat_until = function(chat) - -- Check if the last message in the chat buffer contains "[ERROR] found" - local messages = chat.messages - local last_message = messages[#messages] - if last_message and last_message.role == 'assistant' then - local content = last_message.content - return not content:find '[ERROR] found' - end - return true - end, - content = 'PHPStan is still reporting errors. Edit the code to fix the errors and run PHPStan again.', - }, - }, - }, - }, - }, - } - end, - dependencies = { - 'nvim-lua/plenary.nvim', - 'nvim-treesitter/nvim-treesitter', - -- 'Davidyz/VectorCode', - 'ravitemer/mcphub.nvim', - }, -} diff --git a/lua/projects.lua b/lua/projects.lua index efd090a..e3e4135 100644 --- a/lua/projects.lua +++ b/lua/projects.lua @@ -229,28 +229,7 @@ local PROJECTS = { vim.api.nvim_create_autocmd({ 'BufWritePre' }, { pattern = { '*.php' }, callback = function(args) - local cwd = vim.fn.getcwd() - if vim.fn.expand('%:p'):match(dir .. '/server/') then - cwd = dir .. '/server/' - end - local file = vim.api.nvim_buf_get_name(args.buf) - local input = table.concat(vim.api.nvim_buf_get_lines(args.buf, 0, -1, false), '\n') - local result = vim.system({ - vim.fn.stdpath 'data' .. '/mason/bin/pint', - '--stdin-filename', - file, - }, { - text = true, - stdin = input, - cwd = dir .. '/server/', - }):wait() - - if result.code ~= 0 then - vim.notify(result.stderr, vim.log.levels.ERROR) - return - end - - vim.api.nvim_buf_set_lines(args.buf, 0, -1, false, vim.split(result.stdout, '\n', { plain = true })) + require('helpers').format_buffer('eslint_d', args.buf) end, }) @@ -274,8 +253,13 @@ local PROJECTS = { vim.api.nvim_create_autocmd({ 'BufWritePost' }, { pattern = { '*.php', '.env', '.graphql' }, callback = function() - vim.fn.jobstart('vendor/bin/sail artisan octane:reload', { stdout_buffered = true }) - vim.fn.jobstart('vendor/bin/sail artisan horizon:terminate', { stdout_buffered = true }) + if not vim.fn.expand('%:p'):match(dir .. '/server/') then + vim.fn.jobstart('vendor/bin/sail artisan octane:reload', { stdout_buffered = true }) + vim.fn.jobstart('vendor/bin/sail artisan horizon:terminate', { stdout_buffered = true }) + end + if vim.fn.search '#\\[TypeScript\\]' ~= 0 then + vim.fn.jobstart('php artisan typescript:transform', { cwd = dir .. '/server', stdout_buffered = true }) + end end, }) diff --git a/nvim.log b/nvim.log new file mode 100644 index 0000000..512bd33 --- /dev/null +++ b/nvim.log @@ -0,0 +1,2 @@ +WRN 2026-07-03T16:02:54.215 c/nvim.57084.0 server_start:197: Failed to start server: operation not permitted: /var/folders/j3/6yv08_8s1s740ggr40_kgvrr0000gn/T/nvim.chris/esnddj/nvim.57725.0 +WRN 2026-07-03T16:03:18.385 c/nvim.57084.0 server_start:197: Failed to start server: operation not permitted: /var/folders/j3/6yv08_8s1s740ggr40_kgvrr0000gn/T/nvim.chris/O4WnB1/nvim.57764.0