diff --git a/README.md b/README.md index 5a29311..57e7a91 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ To use an AI command, type the command followed by an instruction prompt. You ca :AI complete text :AIEdit edit text -:AIChat continue or open new chat +:AIChat open/focus chat window (tab-local) :AIStopChat stop the generation of the AI response for the AIChat :AIImage generate image @@ -208,13 +208,14 @@ In the documentation below, `` denotes a visual selection or any oth ### `:AIChat` -`:AIChat` - continue or start a new conversation. +`:AIChat` - open chat window on current tab, jump to it if already open, reopen it if hidden. +Each tab keeps its own chat buffer. -`? :AIChat {instruction}?` - start a new conversation given the selection, the instruction or both +`? :AIChat {instruction}?` - open or focus the tab-local chat and optionally initialize it with selection/instruction -`? :AIChat /{role} {instruction}?` - use role to complete +`? :AIChat /{role} {instruction}?` - open/focus chat and apply role to chat initialization -When the AI finishes answering, you can continue the conversation by entering insert mode, adding your prompt, and then using the command `:AIChat` once again. +Use `:AIChat` inside an `aichat` buffer to continue the conversation. [Pre-defined](./roles-default.ini) chat roles: `/right`, `/below`, `/tab` diff --git a/autoload/vim_ai.vim b/autoload/vim_ai.vim index 1b0bf5f..8bdb247 100644 --- a/autoload/vim_ai.vim +++ b/autoload/vim_ai.vim @@ -21,19 +21,19 @@ function! s:ImportPythonModules() endfor endfunction -function! s:StartsWith(longer, shorter) abort - return a:longer[0:len(a:shorter)-1] ==# a:shorter +function! vim_ai#ImportPythonModules() abort + call s:ImportPythonModules() endfunction -function! s:GetLastScratchBufferName() - let l:all_buffer_names = map(map(filter(copy(getbufinfo()), 'v:val.listed'), 'v:val.bufnr'), 'bufname(v:val)') - let l:buffer_name = -1 - for l:name in l:all_buffer_names - if s:StartsWith(l:name, s:scratch_buffer_name) - let l:buffer_name = l:name - endif - endfor - return l:buffer_name +function! s:GetTabChatBuffer(tabnr) abort + let l:chat_bufnr = gettabvar(a:tabnr, 'vim_ai_chat_bufnr', -1) + if l:chat_bufnr == -1 || !bufexists(l:chat_bufnr) + return -1 + endif + if getbufvar(l:chat_bufnr, '&filetype') !=# 'aichat' + return -1 + endif + return l:chat_bufnr endfunction " Configures ai-chat scratch window. @@ -41,45 +41,26 @@ endfunction " - opens new ai-chat every time " - excludes buffer from buffer list " - scratch_buffer_keep_open = 1 -" - opens last ai-chat buffer (unless force_new = 1) +" - keeps hidden ai-chat buffer for later reuse in the same tab " - keeps the buffer in the buffer list -function! s:OpenChatWindow(open_conf, force_new) abort - " open new buffer that will be used as a chat - let l:open_cmd = has_key(g:vim_ai_open_chat_presets, a:open_conf) - \ ? g:vim_ai_open_chat_presets[a:open_conf] - \ : a:open_conf +function! s:OpenChatWindow(open_conf) abort + let l:open_cmd = get(g:vim_ai_open_chat_presets, a:open_conf, a:open_conf) execute l:open_cmd - " reuse chat in keep-open mode - let l:keep_open = g:vim_ai_chat['ui']['scratch_buffer_keep_open'] == '1' - let l:last_scratch_buffer_name = s:GetLastScratchBufferName() - if l:keep_open && bufexists(l:last_scratch_buffer_name) && !a:force_new - let l:current_buffer = bufnr('%') - " reuse chat buffer - execute "buffer " . l:last_scratch_buffer_name - " close new buffer that was created by l:open_cmd - execute "bd " . l:current_buffer - return - endif + let l:keep_open = g:vim_ai_chat['ui']['scratch_buffer_keep_open'] ==# '1' setlocal buftype=nofile setlocal noswapfile - setlocal ft=aichat - if l:keep_open - setlocal bufhidden=hide - else - setlocal bufhidden=wipe - endif - if bufexists(s:scratch_buffer_name) - " spawn another window if chat already exist - let l:index = 2 - while bufexists(s:scratch_buffer_name . " " . l:index) - let l:index += 1 - endwhile - execute "file " . s:scratch_buffer_name . " " . l:index - else - execute "file " . s:scratch_buffer_name - endif + setlocal filetype=aichat + execute 'setlocal bufhidden=' . (l:keep_open ? 'hide' : 'wipe') + + let l:buffer_name = s:scratch_buffer_name + let l:index = 2 + while bufexists(l:buffer_name) + let l:buffer_name = s:scratch_buffer_name . ' ' . l:index + let l:index += 1 + endwhile + execute 'file ' . fnameescape(l:buffer_name) endfunction let s:is_handling_paste_mode = 0 @@ -241,43 +222,59 @@ function! vim_ai#AIImageRun(uses_range, config, ...) range abort py3 run_ai_image(unwrap('l:context')) endfunction -function! s:ReuseOrCreateChatWindow(config) - let l:open_conf = a:config['ui']['open_chat_command'] +function! s:ReuseOrCreateChatWindow(config) abort + let l:tabnr = tabpagenr() + let l:force_new = a:config['ui']['force_new_chat'] ==# '1' - if a:config['ui']['force_new_chat'] == '1' - call s:OpenChatWindow(l:open_conf, 1) + if &filetype ==# 'aichat' + call settabvar(l:tabnr, 'vim_ai_chat_bufnr', bufnr('%')) return endif - if &filetype != 'aichat' - " reuse chat in active window or tab - let l:chat_win_ids = win_findbuf(bufnr(s:scratch_buffer_name)) - if !empty(l:chat_win_ids) - call win_gotoid(l:chat_win_ids[0]) - return - endif + let l:chat_bufnr = s:GetTabChatBuffer(l:tabnr) + let l:chat_win_id = -1 + let l:is_visible_outside_tab = 0 + + if l:chat_bufnr != -1 + for l:win_id in win_findbuf(l:chat_bufnr) + let l:win_tabnr = win_id2tabwin(l:win_id)[0] + if l:win_tabnr == l:tabnr + let l:chat_win_id = l:win_id + else + let l:is_visible_outside_tab = 1 + endif + endfor - " reuse .aichat file on the same tab - let buffer_list_tab = tabpagebuflist(tabpagenr()) - let buffer_list_tab = filter(buffer_list_tab, 'getbufvar(v:val, "&filetype") ==# "aichat"') - if len(buffer_list_tab) > 0 - call win_gotoid(win_findbuf(buffer_list_tab[0])[0]) - return + if l:is_visible_outside_tab + let l:chat_bufnr = -1 + let l:chat_win_id = -1 endif + endif - " reuse any .aichat buffer in the session - let buffer_list = [] - for i in range(tabpagenr('$')) - call extend(buffer_list, tabpagebuflist(i + 1)) - endfor - let buffer_list = filter(buffer_list, 'getbufvar(v:val, "&filetype") ==# "aichat"') - if len(buffer_list) > 0 - call win_gotoid(win_findbuf(buffer_list[0])[0]) - return - endif + if !l:force_new && l:chat_win_id != -1 + call win_gotoid(l:chat_win_id) + call settabvar(tabpagenr(), 'vim_ai_chat_bufnr', bufnr('%')) + return + endif + + call s:OpenChatWindow(a:config['ui']['open_chat_command']) + let l:new_tabnr = tabpagenr() + let l:new_chat_bufnr = bufnr('%') - " open new chat window if no active buffer found - call s:OpenChatWindow(l:open_conf, 0) + if !l:force_new && l:chat_bufnr != -1 && l:new_tabnr == l:tabnr + execute 'buffer ' . l:chat_bufnr + execute 'bwipeout ' . l:new_chat_bufnr + call settabvar(l:new_tabnr, 'vim_ai_chat_bufnr', bufnr('%')) + return + endif + + call settabvar(l:new_tabnr, 'vim_ai_chat_bufnr', l:new_chat_bufnr) + + if l:force_new + \ && l:new_tabnr == l:tabnr + \ && l:chat_bufnr != -1 + \ && l:chat_bufnr != l:new_chat_bufnr + execute 'bwipeout ' . l:chat_bufnr endif endfunction @@ -322,12 +319,12 @@ function! vim_ai#AIChatRun(uses_range, config, ...) range abort let l:started_from_chat = &filetype == 'aichat' let l:config_input = { - \ "config_default": g:vim_ai_chat, - \ "config_extension": a:config, - \ "user_instruction": l:instruction, - \ "user_selection": l:selection, - \ "is_selection": l:is_selection, - \ "command_type": 'chat', + \ 'config_default': g:vim_ai_chat, + \ 'config_extension': a:config, + \ 'user_instruction': l:instruction, + \ 'user_selection': l:selection, + \ 'is_selection': l:is_selection, + \ 'command_type': 'chat', \} let l:context = py3eval("make_ai_context(unwrap('l:config_input'))") let l:config = l:context['config'] @@ -346,15 +343,12 @@ function! vim_ai#AIChatRun(uses_range, config, ...) range abort return endif - let s:last_command = "chat" + let s:last_command = 'chat' let s:last_config = a:config if py3eval("run_ai_chat(unwrap('l:context'))") if g:vim_ai_async_chat == 1 - call setbufvar(l:bufnr, 'vim_ai_chat_start_last_line', line('$')) - " if user switches to a different buffer, setup autocommand that - " will clean undo history after returning back augroup AichatUndo au! autocmd BufEnter call s:AIChatUndoCleanup() @@ -380,7 +374,6 @@ function! vim_ai#AIChatStopRun() abort call s:AIChatUndoCleanup() endfunction - " Function called in a timer that check if there are new lines from AI and " appned them in a buffer. It ends when AI thread is finished (or when " stopped). diff --git a/doc/vim-ai.txt b/doc/vim-ai.txt index b5c82d5..d6d088d 100644 --- a/doc/vim-ai.txt +++ b/doc/vim-ai.txt @@ -18,6 +18,7 @@ https://github.com/madox2/vim-ai :AI {prompt} complete the prompt :AI complete the selection :AI {instruction} complete the selection using the instruction +In aichat buffers: :AI continue chat completion Options: > let s:initial_complete_prompt =<< trim END @@ -117,9 +118,11 @@ https://platform.openai.com/docs/api-reference/completions *:AIChat* -:AIChat continue or start a new conversation. -? :AIChat {instruction}? start a new conversation given the selection, - the instruction or both +:AIChat open/focus tab-local chat window + Each tab keeps its own chat buffer. + In aichat buffers this continues the chat. +? :AIChat {instruction}? open/focus tab-local chat and optionally + initialize it using selection/instruction Options: > let s:initial_chat_prompt =<< trim END diff --git a/ftplugin/aichat.vim b/ftplugin/aichat.vim index bafba07..f78b9ed 100644 --- a/ftplugin/aichat.vim +++ b/ftplugin/aichat.vim @@ -133,3 +133,5 @@ augroup Aichat autocmd InsertEnter,InsertLeave call s:MarkdownRefreshSyntax(0) autocmd CursorHold,CursorHoldI call s:MarkdownRefreshSyntax(0) augroup END + +command! -buffer -range -nargs=? -complete=customlist,vim_ai#RoleCompletionChat AI ,call vim_ai#AIChatRun(, {}, )