diff --git a/after/ftplugin/markdown.vim b/after/ftplugin/markdown.vim new file mode 100644 index 0000000..9b15c6e --- /dev/null +++ b/after/ftplugin/markdown.vim @@ -0,0 +1 @@ +syntax clear diff --git a/after/syntax/markdown.vim b/after/syntax/markdown.vim new file mode 100644 index 0000000..6c91004 --- /dev/null +++ b/after/syntax/markdown.vim @@ -0,0 +1,22 @@ +syntax clear + +syntax match UserHeader /^>>> USER/ containedin=ALL +syntax match AssistantHeader /^>>> ASSISTANT/ containedin=ALL + +hi def link UserHeader orangebg +hi def link AssistantHeader bluebg + +syntax match H1 '^#\s.*$' +syntax match H2 '^##\s.*$' +syntax match H3 '^###\s.*$' + +hi def link H1 base3 +hi def link H2 base2 +hi def link H3 base2 + +syntax region CodeBlock start='^\s*```.*$' end='^\s*```$' keepend + +syntax match InlineCode '`[^`]\+`' containedin=ALL + +hi def link markdownCodeBlock cyan +hi def link markdownInlineCode cyan diff --git a/ftplugin/markdown.vim b/ftplugin/markdown.vim new file mode 100644 index 0000000..114521e --- /dev/null +++ b/ftplugin/markdown.vim @@ -0,0 +1 @@ +" autocmd FileType markdown syntax clear diff --git a/plugin/gpt.vim b/plugin/gpt.vim new file mode 100644 index 0000000..747181c --- /dev/null +++ b/plugin/gpt.vim @@ -0,0 +1,185 @@ +" ChatGPT Vim Plugin + +if !has('python3') + echo "Python 3 support is required for ChatGPT plugin" + finish +endif + +if !exists("g:gpt_max_tokens") + let g:gpt_max_tokens = 2000 +endif + +if !exists("g:gpt_temperature") + let g:gpt_temperature = 0.7 +endif + +if !exists("g:gpt_model") + let g:gpt_model = 'gpt-4o' +endif + +if !exists("g:gpt_lang") + let g:gpt_lang = 'English' +endif + +if !exists("g:gpt_split_direction") + let g:gpt_split_direction = 'horizontal' +endif + +if !exists("g:gpt_split_ratio") + let g:gpt_split_ratio = 3 +endif + +if !exists("g:gpt_persona") + let g:gpt_persona = 'default' +endif + +let g:gpt_templates = { +\ 'Rewrite': 'Rewrite this more idiomatically', +\ 'Review': 'Review this code', +\ 'Document': 'Return documentation following language pattern conventions', +\ 'Explain': 'Explain how this works', +\ 'Test': 'Write a test', +\ 'Fix': 'Fix this error', +\} + +let g:gpt_keys = keys(g:gpt_templates) + +let g:gpt_personas = { +\ "default": '', +\ "bob": 'You are a helpful expert programmer we are working together to solve complex coding challenges, and I need your help. Please make sure to wrap all code blocks in ``` annotate the programming language you are using.', +\} + +function! DisplayChatGPTResponse(response, finish_reason, chat_gpt_session_id) " {{{ + let response = a:response + let finish_reason = a:finish_reason + + let chat_gpt_session_id = a:chat_gpt_session_id + + if !bufexists(chat_gpt_session_id) + if g:gpt_split_direction ==# 'vertical' + silent execute winwidth(0)/g:gpt_split_ratio.'vnew '. chat_gpt_session_id + else + silent execute winheight(0)/g:gpt_split_ratio.'new '. chat_gpt_session_id + endif + call setbufvar(chat_gpt_session_id, '&buftype', 'nofile') + call setbufvar(chat_gpt_session_id, '&bufhidden', 'hide') + call setbufvar(chat_gpt_session_id, '&swapfile', 0) + setlocal modifiable + setlocal wrap + setlocal linebreak + call setbufvar(chat_gpt_session_id, '&ft', 'markdown') + call setbufvar(chat_gpt_session_id, '&syntax', 'markdown') + endif + + if bufwinnr(chat_gpt_session_id) == -1 + if g:gpt_split_direction ==# 'vertical' + execute winwidth(0)/g:gpt_split_ratio.'vsplit ' . chat_gpt_session_id + else + execute winheight(0)/g:gpt_split_ratio.'split ' . chat_gpt_session_id + endif + endif + + let last_lines = getbufline(chat_gpt_session_id, '$') + let last_line = empty(last_lines) ? '' : last_lines[-1] + + let new_lines = substitute(last_line . response, '\n', '\r\n\r', 'g') + let lines = split(new_lines, '\n') + + let clean_lines = [] + for line in lines + call add(clean_lines, substitute(line, '\r', '', 'g')) + endfor + + call setbufline(chat_gpt_session_id, '$', clean_lines) + + execute bufwinnr(chat_gpt_session_id) . 'wincmd w' + " Move the viewport to the bottom of the buffer + normal! G + call cursor('$', 1) + + if finish_reason != '' + wincmd p + endif +endfunction + +" }}} + +function! ChatGPT(prompt, persist) abort " {{{ + " echo 'prompt: ' . a:prompt + " echo 'persist: ' . a:persist + + silent py3file ~/.vim/python/gpt.py +endfunction + +" }}} + +function! SendToChatGPT(prompt, bang) abort " {{{ + + let persist = (a:bang ==# '!') ? 1 : 0 + + let save_cursor = getcurpos() + let [current_line, current_col] = getcurpos()[1:2] + let save_reg = @@ + let save_regtype = getregtype('@') + + let [line_start, col_start] = getpos("'<")[1:2] + let [line_end, col_end] = getpos("'>")[1:2] + + if (col_end - col_start > 0 || line_end - line_start > 0) && + \ (current_line == line_start && current_col == col_start || + \ current_line == line_end && current_col == col_end) + + let current_line_start = line_start + let current_line_end = line_end + + if current_line_start == line_start && current_line_end == line_end + execute 'normal! ' . line_start . 'G' . col_start . '|v' . line_end . 'G' . col_end . '|y' + let snippet = "\n" . '```' . &syntax . "\n" . @@ . "\n" . '```' + else + let snippet = '' + endif + else + let snippet = '' + endif + + if has_key(g:gpt_templates, a:prompt) + let prompt = g:gpt_templates[a:prompt] + else + let prompt = a:prompt + endif + + let prompt = prompt . snippet + + call ChatGPT(prompt, persist) + + let @@ = save_reg + call setreg('@', save_reg, save_regtype) + + let curpos = getcurpos() + call setpos("'<", curpos) + call setpos("'>", curpos) + call setpos('.', save_cursor) +endfunction + +" }}} + +command! -range -bang -nargs=1 Gpt call SendToChatGPT(, '') + +for i in range(len(g:gpt_keys)) + execute "command! -range -bang -nargs=0 " . g:gpt_keys[i] . " call SendToChatGPT('" . g:gpt_keys[i] . "', '')" +endfor + +function! SetPersona(persona) " {{{ + let personas = keys(g:gpt_personas) + if index(personas, a:persona) != -1 + echo 'Persona set to: ' . a:persona + let g:gpt_persona = a:persona + else + let g:gpt_persona = 'default' + echo a:persona . ' is not a valid persona. Defaulting to: ' . g:gpt_persona + end +endfunction + +" }}} + +command! -nargs=1 Persona call SetPersona() diff --git a/python/gpt.py b/python/gpt.py new file mode 100644 index 0000000..167f398 --- /dev/null +++ b/python/gpt.py @@ -0,0 +1,145 @@ +import sys +import vim +import os + +try: + from openai import AzureOpenAI, OpenAI +except ImportError: + print("Error: openai module not found. Please install with Pip and ensure equality of the versions given by :!python3 -V, and :python3 import sys; print(sys.version)") + raise + +def safe_vim_eval(expression): + try: + return vim.eval(expression) + except vim.error: + return None + +def create_client(): + api_type = safe_vim_eval('g:gpt_api_type') + api_key = os.getenv('OPENAI_API_KEY') or safe_vim_eval('g:gpt_key') or safe_vim_eval('g:gpt_openai_api_key') + openai_base_url = os.getenv('OPENAI_PROXY') or os.getenv('OPENAI_API_BASE') or safe_vim_eval('g:gpt_openai_base_url') + + if api_type == 'azure': + azure_endpoint = safe_vim_eval('g:gpt_azure_endpoint') + azure_api_version = safe_vim_eval('g:gpt_azure_api_version') + azure_deployment = safe_vim_eval('g:gpt_azure_deployment') + assert azure_endpoint and azure_api_version and azure_deployment, "azure_endpoint, azure_api_version and azure_deployment not set property, please check your settings in `vimrc` or `enviroment`." + assert api_key, "api_key not set, please configure your `openai_api_key` in your `vimrc` or `enviroment`" + client = AzureOpenAI( + azure_endpoint=azure_endpoint, + azure_deployment=azure_deployment, + api_key=api_key, + api_version=azure_api_version, + ) + else: + client = OpenAI( + base_url=openai_base_url, + api_key=api_key, + ) + return client + + +def chat_gpt(prompt, persist=0): + token_limits = { + "gpt-3.5-turbo": 4097, + "gpt-3.5-turbo-16k": 16385, + "gpt-3.5-turbo-1106": 16385, + "gpt-4": 8192, + "gpt-4-turbo": 128000, + "gpt-4-turbo-preview": 128000, + "gpt-4-32k": 32768, + "gpt-4o": 128000, + "gpt-4o-mini": 128000, + } + + max_tokens = int(vim.eval('g:gpt_max_tokens')) + model = str(vim.eval('g:gpt_model')) + temperature = float(vim.eval('g:gpt_temperature')) + lang = str(vim.eval('g:gpt_lang')) + resp = f" And respond in {lang}." if lang != 'None' else "" + + personas = dict(vim.eval('g:gpt_personas')) + persona = str(vim.eval('g:gpt_persona')) + + systemCtx = {"role": "system", "content": f"{personas[persona]} {resp}"} + messages = [] + session_id = 'gpt-persistent-session' if persist == 0 else None + + # If session id exists and is in vim buffers + if session_id: + buffer = [] + + for b in vim.buffers: + # If the buffer name matches the session id + if session_id in b.name: + buffer = b[:] + break + + # Read the lines from the buffer + history = "\n".join(buffer).split('\n\n>>> ') + history.reverse() + + # Adding messages to history until token limit is reached + token_count = token_limits.get(model, 4097) - max_tokens - len(prompt) - len(str(systemCtx)) + + for line in history: + if line.startswith("USER\n"): + role = "user" + message = line.replace("USER\n", "").strip() + elif line.startswith("ASSISTANT\n"): + role = "assistant" + message = line.replace("ASSISTANT\n", "").strip() + else: + continue + token_count -= len(message) + if token_count > 0: + messages.insert(0, { + "role": role.lower(), + "content": message + }) + + if session_id: + content = '' + if len(buffer) == 0: + content += '# GPT' + content += '\n\n>>> USER\n' + prompt + '\n\n>>> ASSISTANT\n'.replace("'", "''") + + vim.command("call DisplayChatGPTResponse('{0}', '', '{1}')".format(content.replace("'", "''"), session_id)) + vim.command("redraw") + + messages.append({"role": "user", "content": prompt}) + messages.insert(0, systemCtx) + + try: + client = create_client() + response = client.chat.completions.create( + model=model, + messages=messages, + temperature=temperature, + max_tokens=max_tokens, + stream=True + ) + + # Iterate through the response chunks + for chunk in response: + # newer Azure API responses contain empty chunks in the first streamed + # response + if not chunk.choices: + continue + + chunk_session_id = session_id if session_id else chunk.id + choice = chunk.choices[0] + finish_reason = choice.finish_reason + + # Call DisplayChatGPTResponse with the finish_reason or content + if finish_reason: + vim.command("call DisplayChatGPTResponse('', '{0}', '{1}')".format(finish_reason.replace("'", "''"), chunk_session_id)) + elif choice.delta: + content = choice.delta.content + vim.command("call DisplayChatGPTResponse('{0}', '', '{1}')".format(content.replace("'", "''"), chunk_session_id)) + + vim.command("redraw") + except Exception as e: + print("Error:", str(e)) + +chat_gpt(vim.eval('a:prompt'), int(vim.eval('a:persist'))) diff --git a/vimrc b/vimrc index 622bb2e..73af368 100644 --- a/vimrc +++ b/vimrc @@ -3,6 +3,7 @@ " \ \ / / | '_ ` _ \| '__/ __| " \ V /| | | | | | | | | (__ " \_/ |_|_| |_| |_|_| \___| + syntax on " theme @@ -151,3 +152,14 @@ set completeopt=noinsert,menuone,noselect let g:slime_target = "tmux" let g:slime_default_config = {"socket_name": get(split($TMUX, ","), 0), "target_pane": ":.2"} + + +let g:gpt_openai_api_key='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' +let g:gpt_max_tokens=2000 +let g:gpt_model='gpt-4o' +let g:gpt_temperature = 0.7 +let g:gpt_lang = 'English' +" let g:chat_gpt_split_direction = 'vertical' +" let g:split_ratio=4 + +vmap 0 (chatgpt-menu)