diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b8a452a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,31 @@ +# Augment Vim Plugin Changelog + +This file documents the notable changes for each stable version of the Augment +Vim plugin. The following list is not necessarily comprehensive, but should +include any changes that may impact the user experience. + +## Unreleased + +- Add the `:Augment chat-input` command, which opens a floating window for + composing a chat message before sending it (Neovim only). It is range-aware + like `:Augment chat`, and falls back to the standard `input()` prompt on Vim. +- Add the `:Augment help [command]` command, which lists the available commands + or shows more detailed help for a specific command. +- Fix Neovim API deprecation warnings: migrate from `vim.lsp.start_client` to + `vim.lsp.start` (with `attach = false` to preserve the plugin's explicit + buffer-attach logic) and use the colon-method syntax for `client:notify` and + `client:request` on Neovim 0.11+, with a compatibility fallback for 0.10. + +## 0.25.1 + +- Deprecate the `Enable` and `Disable` commands in favor of the + `g:augment_disable_completions` option which disables inline completions but + not the chat feature. See `:help g:augment_disable_completions` for more + details. +- Check for the `winfixbuf` option before setting it to avoid an error on older + versions of Vim. +- Perform a runtime compatibility check on startup to warn users if they are + running an unsupported version of Node.js. +- Improve the auth flow by significantly shortening the auth URL (addressing + issues with truncated URLs) and improving the error messages on failure. +- Add support for filepaths containing spaces. diff --git a/README.md b/README.md index 94ec538..81c39af 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,35 @@ # Augment Vim & Neovim Plugin -> [!WARNING] -> This plugin is in early alpha development stage. Features may be incomplete, -> unstable, or change without notice. While basic functionality is available, -> you may encounter bugs, performance issues, or unexpected behavior. Current -> platform support is limited to MacOS and Linux, with Windows to be added at a -> later date. +## A Quick Tour -## Installation +Augment's Vim/Neovim plugin provides inline code completions and multi-turn +chat conversations specially tailored to your codebase. The plugin is designed +to work with any modern Vim or Neovim setup, and features the same underlying +context engine that powers our VSCode and IntelliJ plugins. -1. Both Vim and Neovim are supported, but the plugin may require a newer version - than what's installed on your system by default. +Once you've installed the plugin, tell Augment about your project by adding +[workspace folders](#workspace-folders) to your config file, and then sign-in +to the Augment service. You can now open a source file in your project, begin +typing, and you should receive context-aware code completions. Use tab to +accept a suggestion, or keep typing to refine the suggestions. To ask questions +about your codebase or request specific changes, use the `:Augment chat` command +to start a chat conversation. - - [Vim](https://github.com/vim/vim?tab=readme-ov-file#installation) version 9.1.0 or newer. +## Getting Started - - [Neovim](https://github.com/neovim/neovim/tree/master?tab=readme-ov-file#install-from-package), version - 0.10.0 or newer. +1. Sign up for a free trial of Augment at + [augmentcode.com](https://augmentcode.com). + +1. Ensure you have a compatible editor version installed. Both Vim and Neovim + are supported, but the plugin may require a newer version than what is + installed on your system by default. + + - For [Vim](https://github.com/vim/vim?tab=readme-ov-file#installation), + version 9.1.0 or newer. + + - For + [Neovim](https://github.com/neovim/neovim/tree/master?tab=readme-ov-file#install-from-package), + version 0.10.0 or newer. 1. Install [Node.js](https://nodejs.org/en/download/package-manager/all), version 22.0.0 or newer, which is a required dependency. @@ -48,6 +62,8 @@ { 'augmentcode/augment.vim' }, ``` +1. Add workspace folders to your config file. This is really essential to getting the most out of augment! See the [Workspace Folders](#workspace-folders) section for more information. + 1. Open Vim and sign in to Augment with the `:Augment signin` command. ## Basic Usage @@ -58,19 +74,64 @@ appear. The following commands are provided: ```vim -:Augment status " View the current status of the plugin -:Augment signin " Start the sign in flow -:Augment signout " Sign out of Augment -:Augment enable " Globally enable suggestions (on by default) -:Augment disable " Globally disable suggestions -:Augment log " View the plugin log -:Augment chat " Start a chat with Augment AI +:Augment status " View the current status of the plugin +:Augment signin " Start the sign in flow +:Augment signout " Sign out of Augment +:Augment log " View the plugin log +:Augment chat " Send a chat message to Augment AI +:Augment chat-input " Compose a chat message in a floating window (Neovim only) +:Augment chat-new " Start a new chat conversation +:Augment chat-toggle " Toggle the chat panel visibility +:Augment help " List the available commands, or `:Augment help ` for details ``` +## Workspace Folders + +Workspace folders help Augment understand your codebase better by providing +additional context. Adding your project's root directory as a workspace folder +allows Augment to take advantage of context from across your project, rather +than just the currently open file, improving the accuracy and style of +completions and chat. + +You can configure workspace folders by setting +`g:augment_workspace_folders` in your vimrc: + +```vim +let g:augment_workspace_folders = ['/path/to/project', '~/another-project'] +``` + +Workspace folders can be specified using absolute paths or paths relative to +your home directory (~). Adding your project's root directory as a workspace +folder helps Augment generate completions that match your codebase's patterns +and conventions. + +Note: This option must be set before the plugin is loaded. + +After adding a workspace folder and restarting vim, the output of the +`:Augment status` command will include the syncing progress for the added +folder. + +If you want to ignore particular files or directories from your workspace, you +can create a `.augmentignore` file in the root of your workspace folder. This +file is treated similar to a `.gitignore` file. For example, to ignore all +files within the `node_modules` directory, you can add +the following lines to your `.augmentignore` file: + +``` +node_modules/ +``` + +For more information on how to use the `.augmentignore` file, see the [documentation](https://docs.augmentcode.com/setup-augment/sync). + + ## Chat -The chat command allows you to interact with Augment AI in a conversational -manner. You can use it in two ways: +Augment chat supports multi-turn conversations using your project's full +context. Once a conversation is started, subsequent chat exchanges will include +the history from the previous exchanges. This is useful for asking follow-up +questions or getting context-specific help. + +You can interact with chat in two ways: 1. Direct command with message: @@ -84,32 +145,47 @@ manner. You can use it in two ways: - Type `:Augment chat` followed by your question about the selection -The response will appear in a new buffer with markdown formatting. Note that -chat is currently limited to single-turn conversations - each chat command -starts a new conversation. +The response will appear in a separate chat buffer with markdown formatting. -## Workspace Folders +### Floating chat input (Neovim only) -Workspace folders help Augment understand your codebase better by providing -additional context. You can configure workspace folders by setting -`g:augment_workspace_folders` in your vimrc: +The `:Augment chat-input` command opens a centered floating window with a +markdown scratch buffer where you can compose a chat message before sending it. +This is handy for writing longer, multi-line prompts. The window opens in insert +mode, and its title shows the available keys: + +- `` (insert or normal mode) or `` (normal mode) submits the message +- `` (normal mode) or `` (insert or normal mode) cancels + +Like `:Augment chat`, it is range-aware: invoking it from visual mode (or with a +range) includes the selected text in the chat request once you submit. + +If an input window is already open, running the command again refocuses it +rather than opening a new one, so you won't lose what you've typed if focus +moves away. + +This command requires Neovim's floating window support. In Vim it falls back to +the standard `input()` prompt used by `:Augment chat`, with no change to +existing behavior. The plugin does not define a default mapping for it, so map +it yourself if you'd like a shortcut, for example: ```vim -let g:augment_workspace_folders = ['/path/to/project', '~/another-project'] +nnoremap ai :Augment chat-input +vnoremap ai :Augment chat-input ``` -Workspace folders can be specified using absolute paths or paths relative to -your home directory (~). Adding your project's root directory as a workspace -folder helps Augment generate completions that match your codebase's patterns -and conventions. +To start a new conversation, use the `:Augment chat-new` command. This will +clear the chat history from your context. -Note: This option must be set before the plugin is loaded. +Use the `:Augment chat-toggle` command to open and close the chat panel. When +the chat panel is closed, the chat conversation will be preserved and can be +reopened with the same command. ## Alternate Keybinds By default, tab is used to accept a suggestion. If you want to use a different key, create a mapping that calls `augment#Accept()`. The function -takes an optional arugment used to specify the fallback text to insert if no +takes an optional argument used to specify the fallback text to insert if no suggestion is available. ```vim @@ -121,14 +197,63 @@ inoremap call augment#Accept() inoremap call augment#Accept("\n") ``` +or in neovim + +```lua +-- Use Ctrl-Y to accept a suggestion +vim.keymap.set('i', '', 'call augment#Accept()', { noremap = true }) +-- Use enter to accept a suggestion, falling back to a newline if no suggestion is available +vim.keymap.set('i', '', 'call augment#Accept()', { noremap = true }) +``` + The default tab mapping can be disabled by setting `g:augment_disable_tab_mapping = v:true` before the plugin is loaded. +Completions can be disabled entirely by setting +`g:augment_disable_completions = v:true` in your vimrc or at any time during +editing. + If another plugin uses tab in insert mode, the Augment tab mapping may be overridden depending on the order in which the plugins are loaded. If tab isn't working for you, the `imap ` command can be used to check if the mapping is present. +## FAQ + +**Q: I'm not seeing any completions. Is the plugin working?** + +A: You may want to first check the output of the `:Augment status` command. +This command will show the current status of the plugin, including whether +you're signed in and whether your workspace folders are synced. If you're not +signed in, you'll need to sign in using the `:Augment signin` command. If those +are not indicating a problem, you can check the plugin log using the `:Augment +log` command. This will show any errors that may have occurred. + +**Q: Can I create shortcuts for the Augment commands?** + +A: Absolutely! You can create mappings for any of the Augment commands. For +example, to create a shortcut for the `:Augment chat*` commands, you can add the +following to your vimrc: + +```vim +nnoremap ac :Augment chat +vnoremap ac :Augment chat +nnoremap an :Augment chat-new +nnoremap at :Augment chat-toggle +``` + +**Q: My workspace is taking a long time to sync. What should I do?** + +A: It may take a while to sync if you have a very large codebase that has not +been synced before. It's also not uncommon to inadvertenly include a large +directory like `node_modules/`. You can use `:Augment status` to see the +progress of the sync. If the sync is making progress but just slow, it may be +worth checking if you have a large directory that you don't need to sync. You +can add these directories to your `.augmentignore` file to exclude it from the +sync. If you're still having trouble, please file a github issue with a +description of the problem and include the output of `:Augment log`. + + ## Licensing and Distribution This repository includes two main components: @@ -142,4 +267,4 @@ For details on usage restrictions, refer to the [LICENSE.md](LICENSE.md) file. We encourage users to report any bugs or issues directly to us. Please use the [Issues](https://github.com/augmentcode/augment.vim/issues) section of this repository to share your feedback. -For any other questions, feel free to reach out to support@augmentcode.com. +For any other questions, feel free to reach out to [Augment Support](https://support.augmentcode.com/). diff --git a/autoload/augment.vim b/autoload/augment.vim index fd2eb7d..34e63ba 100644 --- a/autoload/augment.vim +++ b/autoload/augment.vim @@ -23,6 +23,11 @@ function! s:OpenBuffer() abort return endif + " Ignore non-file buffers + if &buftype != '' + return + endif + let client = augment#client#Client() if has('nvim') call luaeval('require("augment").open_buffer(_A[1], _A[2])', [client.client_id, bufnr('%')]) @@ -46,6 +51,11 @@ function! s:UpdateBuffer() abort return endif + " Ignore non-file buffers + if &buftype != '' + return + endif + " The nvim lsp client does this automatically if !has('nvim') " Only send a change notification if the buffer has changed (as @@ -73,8 +83,13 @@ function! s:RequestCompletion() abort return endif - " Don't send a request if disabled - if exists('g:augment_enabled') && !g:augment_enabled + " Ignore non-file buffers + if &buftype != '' + return + endif + + " Don't send a request if completions are disabled + if exists('g:augment_disable_completions') && g:augment_disable_completions return endif @@ -85,7 +100,16 @@ function! s:RequestCompletion() abort endif let b:_augment_comp_tick = b:changedtick - let uri = 'file://' . expand('%:p') + if has('nvim') + " NOTE(mpauly): On neovim, we use the built-in lsp client which + " requires the uri to be in the format defined by + " vim.uri_from_fname(). There isn't a straightforward way to format + " the uri on vim and it isn't causing any issues, so punting on it for + " now. + let uri = v:lua.vim.uri_from_fname(expand('%:p')) + else + let uri = 'file://' . expand('%:p') + endif let text = join(getline(1, '$'), "\n") " TODO: remove version-- we use it elsewhere but it's not in the spec call augment#client#Client().Request('textDocument/completion', { @@ -129,15 +153,21 @@ function! s:CommandSignOut(...) abort call augment#client#Client().Request('augment/logout', {}) endfunction +" NOTE: The enable/disable commands are deprecated function! s:CommandEnable(...) abort - let g:augment_enabled = v:true + call augment#DisplayError('The `Enable` and `Disable` commands are deprecated in favor of the `g:augment_disable_completions` option. See `:help g:augment_disable_completions` for more details.') endfunction function! s:CommandDisable(...) abort - let g:augment_enabled = v:false + call augment#DisplayError('The `Enable` and `Disable` commands are deprecated in favor of the `g:augment_disable_completions` option. See `:help g:augment_disable_completions` for more details.') endfunction function! s:CommandStatus(...) abort + if !exists('g:augment_initialized') || !g:augment_initialized + call augment#DisplayError('The Augment plugin failed to initialize. See ":Augment log" for more details.') + return + endif + if !s:IsRunning() echohl WarningMsg echo s:NOT_RUNNING_MSG @@ -149,13 +179,6 @@ function! s:CommandStatus(...) abort endfunction function! s:CommandChat(range, args) abort - if exists('g:augment_enabled') && !g:augment_enabled - echohl WarningMsg - echo 'Augment: Not enabled. Run ":Augment enable" to enable the plugin.' - echohl None - return - endif - if !s:IsRunning() echohl WarningMsg echo s:NOT_RUNNING_MSG @@ -171,32 +194,33 @@ function! s:CommandChat(range, args) abort let selected_text = '' endif + let uri = augment#chat#GetUri() + let history = augment#chat#GetHistory() + " Use the message from the additional command arguments if provided, or " prompt the user for a message let message = empty(a:args) ? input('Message: ') : a:args - " Handle cancellation or empty input - if message ==# '' || message =~# '^\s*$' + " Handle cancellation or empty input. \_s matches whitespace including + " newlines, so a message that is only blank lines is treated as cancel. + if message ==# '' || message =~# '^\_s*$' redraw echo 'Chat cancelled' return endif - " Create new buffer for chat response - let chat_bufname = 'AugmentChat-' . strftime("%Y%m%d-%H%M%S") - let current_win = bufwinid(bufnr('%')) - call augment#chat#CreateBuffer(chat_bufname) - call win_gotoid(current_win) + call augment#chat#OpenChatPanel() + call augment#chat#AppendMessage(message) call augment#log#Info( - \ 'Making chat request in buffer ' . chat_bufname - \ . ' with selected_text="' . selected_text + \ 'Making chat request with file=' . uri + \ . ' selected_text="' . selected_text \ . '"' . ' message="' . message . '"') let params = { \ 'textDocumentPosition': { \ 'textDocument': { - \ 'uri': 'file://' . expand('%:p'), + \ 'uri': uri, \ }, \ 'position': { \ 'line': line('.') - 1, @@ -204,17 +228,177 @@ function! s:CommandChat(range, args) abort \ }, \ }, \ 'message': message, - \ 'partialResultToken': chat_bufname, \ } - " Add selected text if available + " Add selected text and history if available if !empty(selected_text) let params['selectedText'] = selected_text endif + if !empty(history) + let params['history'] = history + endif call augment#client#Client().Request('augment/chat', params) endfunction +" Open a floating window to compose a chat message before sending it. The +" floating input is Neovim-only; in Vim (and when a message is supplied +" directly) this falls back to the standard chat command, which prompts for a +" message via input() when none is given. +function! s:CommandChatInput(range, args) abort + if !s:IsRunning() + echohl WarningMsg + echo s:NOT_RUNNING_MSG + echohl None + return + endif + + " Determine whether a selection range is active. Leave visual mode so the + " '< and '> marks are set for the chat flow to pick up on submit. + let was_visual = index(['v', 'V', "\"], mode()) >= 0 + if was_visual + execute "normal! \" + endif + let ranged = a:range == 2 || was_visual + + " A message passed directly on the command line skips the floating input. + " Vim has no editable floating window, so it falls back to the input() + " prompt provided by the standard chat command. + if !empty(a:args) || !has('nvim') + call s:CommandChat(ranged ? 2 : 0, a:args) + return + endif + + let source_win = win_getid() + let Callback = function('s:ChatInputSubmit', [source_win, ranged]) + call augment#chat#OpenInputWindow(Callback) +endfunction + +" Handle a message submitted from the floating chat input +function! s:ChatInputSubmit(source_win, ranged, message) abort + " \_s matches whitespace including newlines, so a buffer of only blank + " lines is treated as cancel rather than sending an empty message. + if a:message ==# '' || a:message =~# '^\_s*$' + redraw + echo 'Chat cancelled' + return + endif + + " Restore focus to the window the input was opened from + if win_id2win(a:source_win) != 0 + call win_gotoid(a:source_win) + endif + + " Re-select the original range so it is passed through to the chat request, + " mirroring the behavior of `:Augment chat` in visual mode. The '< and '> + " marks were set when the command left visual mode, so `gv` works whether + " invoked from visual mode or via an explicit `:'<,'>` range. + if a:ranged + normal! gv + endif + + call s:CommandChat(a:ranged ? 2 : 0, a:message) +endfunction + +function! s:CommandChatNew(range, args) abort + call augment#chat#Reset() +endfunction + +function! s:CommandChatToggle(range, args) abort + call augment#chat#Toggle() +endfunction + +" Help text for the available commands. The order of this list determines the +" order shown by `:Augment help`. Each entry has a usage string (shown in the +" detail header), a one-line summary (shown in the command list), and a list of +" detail lines (shown by `:Augment help `). +let s:command_help = [ + \ {'name': 'status', 'usage': 'status', 'summary': 'View the current status of the plugin.', 'detail': [ + \ 'View the current status of the plugin, including whether you are', + \ 'signed in and the syncing progress of any configured workspace folders.', + \ ]}, + \ {'name': 'signin', 'usage': 'signin', 'summary': 'Sign in to Augment.', 'detail': [ + \ 'Authenticate with the Augment service using OAuth. This is required', + \ 'before using the plugin for the first time.', + \ ]}, + \ {'name': 'signout', 'usage': 'signout', 'summary': 'Sign out of Augment.', 'detail': [ + \ 'Sign out of Augment.', + \ ]}, + \ {'name': 'log', 'usage': 'log', 'summary': 'View the plugin log.', 'detail': [ + \ 'View the plugin log. This is useful for debugging.', + \ ]}, + \ {'name': 'chat', 'usage': 'chat [message]', 'summary': 'Send a chat message to Augment AI.', 'detail': [ + \ 'Start a chat with Augment AI. In visual mode, the selected text will', + \ 'be included in the chat request. If no message is provided, you will', + \ 'be prompted to enter one.', + \ ]}, + \ {'name': 'chat-input', 'usage': 'chat-input', 'summary': 'Compose a chat message in a floating window (Neovim only).', 'detail': [ + \ 'Open a centered floating window with a markdown scratch buffer for', + \ 'composing a chat message before sending it. Submit with or, in', + \ 'normal mode, ; cancel with or . Like ":Augment chat" it', + \ 'is range-aware. Requires Neovim; in Vim it falls back to the input()', + \ 'prompt used by ":Augment chat".', + \ ]}, + \ {'name': 'chat-new', 'usage': 'chat-new', 'summary': 'Start a new chat conversation.', 'detail': [ + \ 'Start a new chat conversation with Augment AI, clearing the history', + \ 'from your context.', + \ ]}, + \ {'name': 'chat-toggle', 'usage': 'chat-toggle', 'summary': 'Toggle the chat panel visibility.', 'detail': [ + \ 'Open or close the chat conversation window. The conversation is', + \ 'preserved while the window is closed and can be reopened with the', + \ 'same command.', + \ ]}, + \ {'name': 'help', 'usage': 'help [command]', 'summary': 'Show help for Augment commands.', 'detail': [ + \ 'Show help for Augment commands. With no argument, list all available', + \ 'commands with a short description. With a command name, show detailed', + \ 'help for that command.', + \ ]}, + \ {'name': 'enable', 'usage': 'enable', 'summary': '(deprecated) See g:augment_disable_completions.', 'detail': [ + \ 'Deprecated. Use the g:augment_disable_completions option instead,', + \ 'which disables inline completions without affecting chat. See', + \ '":help g:augment_disable_completions" for more details.', + \ ]}, + \ {'name': 'disable', 'usage': 'disable', 'summary': '(deprecated) See g:augment_disable_completions.', 'detail': [ + \ 'Deprecated. Use the g:augment_disable_completions option instead,', + \ 'which disables inline completions without affecting chat. See', + \ '":help g:augment_disable_completions" for more details.', + \ ]}, + \ ] + +" Show help for the available commands. With no argument, list all commands; +" with a command name, show detailed help for that command. +function! s:CommandHelp(range, args) abort + let topic = empty(a:args) ? '' : split(a:args)[0] + + if empty(topic) + echohl Title + echo 'Augment commands' + echohl None + for entry in s:command_help + echo printf(' :Augment %-12s %s', entry.name, entry.summary) + endfor + echo 'Run ":Augment help " for more details about a command.' + return + endif + + for entry in s:command_help + " Note that ==? is case-insensitive comparison + if topic ==? entry.name + echohl Title + echo ':Augment ' . entry.usage + echohl None + for line in entry.detail + echo ' ' . line + endfor + return + endif + endfor + + echohl WarningMsg + echo 'Augment: Unknown command: "' . topic . '". Run ":Augment help" to list available commands.' + echohl None +endfunction + " Handle user commands let s:command_handlers = { \ 'log': function('s:CommandLog'), @@ -224,6 +408,10 @@ let s:command_handlers = { \ 'disable': function('s:CommandDisable'), \ 'status': function('s:CommandStatus'), \ 'chat': function('s:CommandChat'), + \ 'chat-input': function('s:CommandChatInput'), + \ 'chat-new': function('s:CommandChatNew'), + \ 'chat-toggle': function('s:CommandChatToggle'), + \ 'help': function('s:CommandHelp'), \ } function! augment#Command(range, args) abort range @@ -232,7 +420,15 @@ function! augment#Command(range, args) abort range return endif + " If the plugin failed to initialize, only allow status, log, and help + " commands let command = split(a:args)[0] + if (!exists('g:augment_initialized') || !g:augment_initialized) + \ && command !=# 'status' && command !=# 'log' && command !=# 'help' + call augment#DisplayError('The Augment plugin failed to initialize. Only `:Augment status`, `:Augment log`, and `:Augment help` commands are available.') + return + endif + for [name, Handler] in items(s:command_handlers) " Note that ==? is case-insensitive comparison if command ==? name @@ -261,6 +457,7 @@ endfunction function! augment#OnBufEnter() abort call s:OpenBuffer() + call augment#chat#SaveUri() endfunction function! augment#OnTextChanged() abort @@ -296,3 +493,22 @@ function! augment#Accept(...) abort call feedkeys(fallback, 'nt') endif endfunction + +" Display an error message to the user in addition to logging it +function! augment#DisplayError(message) abort + " If we have already entered the editor, display the error message + " immediately. Otherwise, wait for VimEnter. + if v:vim_did_enter + echohl ErrorMsg | echom 'Augment: ' . a:message | echohl None + else + " Shadow the message argument with a script-local variable. This means + " that subsequent calls will override the previous message, which + " should be fine for our use case. + let s:error_message = a:message + augroup augment_error + autocmd! + autocmd VimEnter * echohl ErrorMsg | echom 'Augment: ' . s:error_message | echohl None + augroup END + endif + call augment#log#Error(a:message) +endfunction diff --git a/autoload/augment/chat.vim b/autoload/augment/chat.vim index 29b46a0..a6afeea 100644 --- a/autoload/augment/chat.vim +++ b/autoload/augment/chat.vim @@ -3,6 +3,250 @@ " Utilities for chat +function! s:ResetChatContents() abort + let chat_buf = bufnr('AugmentChatHistory') + if chat_buf == -1 + call augment#log#Error('Chat reset failed: Could not find chat history buffer') + return + endif + + call setbufvar(chat_buf, '&modifiable', v:true) + silent call deletebufline(chat_buf, 1, '$') + call augment#chat#AppendText('# Augment Chat History' + \ . "\n\n" + \ . '`:Augment chat` Send a chat message in the current conversation' + \ . "\n" + \ . '`:Augment chat-new` Start a new conversation' + \ . "\n" + \ . '`:Augment chat-toggle` Toggle the chat panel visibility' + \ . "\n\n") +endfunction + +function! augment#chat#Toggle() abort + let chat_id = bufwinid('AugmentChatHistory') + if chat_id == -1 + call augment#chat#OpenChatPanel() + else + " Don't close if it's the last window + if winnr('$') > 1 + call win_execute(chat_id, 'close') + endif + endif +endfunction + +function! augment#chat#OpenChatPanel() abort + let current_win = win_getid() + + " Check if the panel already exists and has been setup + if bufexists('AugmentChatHistory') && !getbufvar('AugmentChatHistory', '&modifiable') + if bufwinid('AugmentChatHistory') == -1 + botright 80vnew AugmentChatHistory + endif + call win_gotoid(current_win) + return + endif + + " Open a buffer for the chat history with a width of 80 characters + botright 80vnew AugmentChatHistory + setlocal buftype=nofile " Buffer will never be written to a file + setlocal nomodifiable " Prevent any modifications + setlocal noswapfile " Don't create a swapfile + " NOTE(mpauly): winfixbuf is not available in some subversions of vim 9.1 + if exists('&winfixbuf') + setlocal winfixbuf " Keep buffer in window when splitting + endif + setlocal winfixwidth " Never change the window's width + setlocal bufhidden=hide " When buffer is abandoned, hide it + setlocal nobuflisted " Hide from :ls + setlocal wrap " Wrap long lines + setlocal linebreak " Wrap at word boundaries + setlocal filetype=markdown " Use markdown syntax highlighting + setlocal nonumber " Hide line numbers + setlocal norelativenumber " Hide relative line numbers + setlocal signcolumn=no " Hide sign column + setlocal nocursorline " Disable cursor line highlighting + setlocal nospell " Disable spell checking + setlocal nofoldenable " Disable folding + setlocal textwidth=0 " Disable text width limit + setlocal scrolloff=0 " Disable scrolloff + + " Add the chat header to the buffer + call s:ResetChatContents() + + " TODO(AU-6480): create another buffer for the chat input + " new AugmentChatInput + + call win_gotoid(current_win) +endfunction + +" Open a centered floating window with a scratch markdown buffer for composing +" a chat message. a:OnSubmit is a Funcref invoked with the composed message +" when the user submits. This relies on Neovim's floating window API and should +" only be called when running under Neovim. +function! augment#chat#OpenInputWindow(OnSubmit) abort + " If an input window is already open, refocus it instead of opening a new + " one. This avoids orphaning the existing float (and losing any typed + " content) when the command is invoked again after focus moved away. + if exists('s:input_win') && s:input_win != -1 && nvim_win_is_valid(s:input_win) + call nvim_set_current_win(s:input_win) + startinsert + return + endif + + let s:input_on_submit = a:OnSubmit + + " Create an unlisted scratch buffer (buftype=nofile, noswapfile) + let buf = nvim_create_buf(v:false, v:true) + + " Center the window, sizing it relative to the editor dimensions + let width = float2nr(&columns * 0.6) + let width = max([40, min([width, &columns - 4])]) + let height = max([5, min([10, &lines - 4])]) + let row = (&lines - height) / 2 + let col = (&columns - width) / 2 + + let opts = { + \ 'relative': 'editor', + \ 'width': width, + \ 'height': height, + \ 'row': row, + \ 'col': col, + \ 'style': 'minimal', + \ 'border': 'rounded', + \ 'title': ' Augment Chat (/ submit, cancel) ', + \ 'title_pos': 'center', + \ } + + let s:input_win = nvim_open_win(buf, v:true, opts) + let s:input_buf = buf + + setlocal filetype=markdown " Use markdown syntax highlighting + setlocal bufhidden=wipe " Discard the buffer when the window closes + setlocal wrap " Wrap long lines + setlocal linebreak " Wrap at word boundaries + + " Submit with (insert and normal) or (normal) + inoremap call InputSubmit() + nnoremap call InputSubmit() + nnoremap call InputSubmit() + " Cancel with (normal) or (insert and normal) + nnoremap call InputCancel() + inoremap call InputCancel() + nnoremap call InputCancel() + + " Start in insert mode so the user can type immediately + startinsert +endfunction + +function! s:CloseInputWindow() abort + if exists('s:input_win') && s:input_win != -1 && nvim_win_is_valid(s:input_win) + call nvim_win_close(s:input_win, v:true) + endif + let s:input_win = -1 +endfunction + +" Join the buffer contents into a message, close the window, and invoke the +" stored submit callback with the message. +function! s:InputSubmit() abort + if !exists('s:input_buf') || !nvim_buf_is_valid(s:input_buf) + call s:CloseInputWindow() + return + endif + + let lines = nvim_buf_get_lines(s:input_buf, 0, -1, v:false) + let message = join(lines, "\n") + let Callback = s:input_on_submit + + call s:CloseInputWindow() + + if type(Callback) == v:t_func + call Callback(message) + endif +endfunction + +function! s:InputCancel() abort + call s:CloseInputWindow() + redraw + echo 'Chat cancelled' +endfunction + +function! augment#chat#Reset() abort + call s:ResetChatContents() + call s:ResetHistory() +endfunction + +function! s:ResetHistory() abort + let g:_augment_chat_history = [] +endfunction + +function! augment#chat#AppendText(text) abort + let chat_buf = bufnr('AugmentChatHistory') + if chat_buf == -1 + call augment#log#Error('Chat append failed: Could not find chat history buffer') + return + endif + + let lines = split(a:text, "\n", v:true) + let last_line = getbufline(chat_buf, '$')[0] + + call setbufvar(chat_buf, '&modifiable', v:true) + call setbufline(chat_buf, '$', last_line . lines[0]) + call appendbufline(chat_buf, '$', lines[1:]) + call setbufvar(chat_buf, '&modifiable', v:false) +endfunction + +function! augment#chat#AppendMessage(message) abort + " If not the first message, scroll to the bottom + let chat_id = bufwinid('AugmentChatHistory') + if !empty(augment#chat#GetHistory()) && chat_id != -1 + let command = "call winrestview({'lnum': line('$'), 'topline': line('$')})" + call win_execute(chat_id, command) + endif + + let message_text = '================================================================================' + \ . "\n\n" + \ . "\t*You*" + \ . "\n\n" + \ . a:message + \ . "\n\n" + \ . '--------------------------------------------------------------------------------' + \ . "\n\n" + \ . "\t*Augment*" + \ . "\n\n" + call augment#chat#AppendText(message_text) +endfunction + +function! augment#chat#AppendHistory(request_message, response_text, request_id) abort + if !exists('g:_augment_chat_history') + let g:_augment_chat_history = [] + endif + call add(g:_augment_chat_history, { + \ 'request_message': a:request_message, + \ 'response_text': a:response_text, + \ 'request_id': a:request_id, + \ }) +endfunction + +function! augment#chat#GetHistory() abort + if exists('g:_augment_chat_history') + return g:_augment_chat_history + endif + return [] +endfunction + +function! augment#chat#SaveUri() abort + if bufname('%') !=# 'AugmentChatHistory' + let g:_augment_current_uri = 'file://' . expand('%:p') + endif +endfunction + +function! augment#chat#GetUri() abort + if exists('g:_augment_current_uri') + return g:_augment_current_uri + endif + return 'file://' . expand('%:p') +endfunction + function! s:GetBufSelection(line_start, col_start, line_end, col_end) abort if a:line_start == a:line_end return getline(a:line_start)[a:col_start - 1:a:col_end - 1] @@ -63,15 +307,3 @@ function! augment#chat#GetSelectedText() abort let [line_end, col_end] = getpos("'>")[1:2] return s:GetBufSelection(line_start, col_start, line_end, col_end) endfunction - -function! augment#chat#CreateBuffer(bufname) abort - botright vnew - setlocal buftype=nofile - setlocal bufhidden=hide - setlocal noswapfile - setlocal wrap - setlocal linebreak - execute 'file ' . a:bufname - setlocal readonly - setlocal filetype=markdown -endfunction diff --git a/autoload/augment/client.vim b/autoload/augment/client.vim index c51d0b1..d52f042 100644 --- a/autoload/augment/client.vim +++ b/autoload/augment/client.vim @@ -3,15 +3,27 @@ " Client for interacting with the server process +" Custom LSP response error codes +let s:AUGMENT_ERROR_UNAUTHORIZED = 401 + let s:client = {} -" If provided, launch the server from a user-provided command -if exists('g:augment_job_command') - let s:job_command = g:augment_job_command -else - let server_file = expand(':h:h:h') . '/dist/server.js' - let s:job_command = ['node', server_file, '--stdio'] -endif +function! augment#client#GetJobCommand() abort + " If provided, launch the server from a user-provided command + if exists('g:augment_job_command') + return g:augment_job_command + endif + + let server_file = expand('