summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md32
-rw-r--r--autoload/vim_ai.vim7
-rw-r--r--autoload/vim_ai_config.vim5
-rw-r--r--doc/vim-ai.txt21
-rw-r--r--plugin/vim-ai.vim10
-rw-r--r--py/chat.py12
-rw-r--r--py/complete.py9
-rw-r--r--py/roles.py23
-rw-r--r--py/utils.py57
-rw-r--r--roles-example.ini22
10 files changed, 186 insertions, 12 deletions
diff --git a/README.md b/README.md
index 18ac762..5731541 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,7 @@ To get an idea what is possible to do with AI commands see the [prompts](https:/
- Generate text or code, answer questions with AI
- Edit selected text in-place with AI
- Interactive conversation with ChatGPT
+- Supports custom roles and more
## How it works
@@ -89,13 +90,15 @@ To use an AI command, type the command followed by an instruction prompt. You ca
**Tip:** Setup your own [key bindings](#key-bindings) or use command shortcuts - `:AIE`, `:AIC`, `:AIR`
+**Tip:** A [custom role](#roles) {role} can be passed to the above commands by an initial parameter /{role}, for example `:AIEdit /grammar`.
+
**Tip:** Combine commands with a range `:help range`, for example to select the whole buffer - `:%AIE fix grammar`
If you are interested in more tips or would like to level up your Vim with more commands like [`:GitCommitMessage`](https://github.com/madox2/vim-ai/wiki/Custom-commands#suggest-a-git-commit-message) - suggesting a git commit message, visit the [Community Wiki](https://github.com/madox2/vim-ai/wiki).
## Reference
-In the documentation below, `<selection>` denotes a visual selection or any other range, `{instruction}` an instruction prompt and `?` symbol an optional parameter.
+In the documentation below, `<selection>` denotes a visual selection or any other range, `{instruction}` an instruction prompt, `{role}` a [custom role](#roles) and `?` symbol an optional parameter.
### `:AI`
@@ -107,18 +110,24 @@ In the documentation below, `<selection>` denotes a visual selection or any oth
`<selection> :AI {instruction}` - complete the selection using the instruction
+`<selection>? :AI /{role} {instruction}?` - use role to complete
+
### `:AIEdit`
`<selection>? :AIEdit` - edit the current line or the selection
`<selection>? :AIEdit {instruction}` - edit the current line or the selection using the instruction
+`<selection>? :AIEdit /{role} {instruction}?` - use role to edit
+
### `:AIChat`
`:AIChat` - continue or start a new conversation.
`<selection>? :AIChat {instruction}?` - start a new conversation given the selection, the instruction or both
+`<selection>? :AIChat /{role} {instruction}?` - use role to complete
+
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.
#### `.aichat` files
@@ -171,6 +180,27 @@ As a parameter you put an open chat command preset shortcut - `below`, `tab` or
Use this immediately after `AI`/`AIEdit`/`AIChat` command in order to re-try or get an alternative completion.
Note that the randomness of responses heavily depends on the [`temperature`](https://platform.openai.com/docs/api-reference/completions/create#completions/create-temperature) parameter.
+## Roles
+
+In the context of this plugin, a role means a re-usable AI instruction and/or configuration. Roles are defined in the configuration `.ini` file. For example by defining a `grammar` role:
+
+```vim
+let g:vim_ai_roles_config_file = '/path/to/my/roles.ini'
+```
+
+```ini
+# /path/to/my/roles.ini
+
+[grammar]
+prompt = fix spelling and grammar
+
+[grammar.options]
+temperature = 0.4
+```
+
+Now you can select text and run it with command `:AIEdit /grammar`.
+
+See [roles-example.ini](./roles-example.ini) for more examples.
## Key bindings
diff --git a/autoload/vim_ai.vim b/autoload/vim_ai.vim
index 875258c..a37ddad 100644
--- a/autoload/vim_ai.vim
+++ b/autoload/vim_ai.vim
@@ -3,6 +3,7 @@ call vim_ai_config#load()
let s:plugin_root = expand('<sfile>:p:h:h')
let s:complete_py = s:plugin_root . "/py/complete.py"
let s:chat_py = s:plugin_root . "/py/chat.py"
+let s:roles_py = s:plugin_root . "/py/roles.py"
" remembers last command parameters to be used in AIRedoRun
let s:last_is_selection = 0
@@ -245,3 +246,9 @@ function! vim_ai#AIRedoRun()
call vim_ai#AIChatRun(0, s:last_config)
endif
endfunction
+
+function! vim_ai#RoleCompletion(A,L,P) abort
+ execute "py3file " . s:roles_py
+ call map(l:role_list, '"/" . v:val')
+ return filter(l:role_list, 'v:val =~ "^' . a:A . '"')
+endfunction
diff --git a/autoload/vim_ai_config.vim b/autoload/vim_ai_config.vim
index ef3c863..41932de 100644
--- a/autoload/vim_ai_config.vim
+++ b/autoload/vim_ai_config.vim
@@ -1,3 +1,5 @@
+let s:plugin_root = expand('<sfile>:p:h:h')
+
let g:vim_ai_complete_default = {
\ "engine": "complete",
\ "options": {
@@ -73,6 +75,9 @@ endif
if !exists("g:vim_ai_token_file_path")
let g:vim_ai_token_file_path = "~/.config/openai.token"
endif
+if !exists("g:vim_ai_roles_config_file")
+ let g:vim_ai_roles_config_file = s:plugin_root . "/roles-example.ini"
+endif
function! vim_ai_config#ExtendDeep(defaults, override) abort
let l:result = a:defaults
diff --git a/doc/vim-ai.txt b/doc/vim-ai.txt
index 72535b3..c7aefde 100644
--- a/doc/vim-ai.txt
+++ b/doc/vim-ai.txt
@@ -161,6 +161,27 @@ You can also customize the options in the chat header: >
generate a paragraph of lorem ipsum
...
+ROLES
+
+Roles are defined in the `.ini` file: >
+
+ let g:vim_ai_roles_config_file = '/path/to/my/roles.ini'
+
+Example of a role: >
+
+ [grammar]
+ prompt = fix spelling and grammar
+
+ [grammar.options]
+ temperature = 0.4
+
+See roles-example.ini for more examples.
+
+The roles in g:vim_ai_roles_config_file are converted to a Vim dictionary whose
+labels are the names of the roles. Optionally, roles can be added by setting
+g:vim_ai_roles_config_function to the name of a Vimscript function returning a
+dictionary of the same format as g:vim_ai_roles_config_file.
+
KEY BINDINGS
Examples how configure key bindings and customize commands: >
diff --git a/plugin/vim-ai.vim b/plugin/vim-ai.vim
index 0e4957f..1ed5326 100644
--- a/plugin/vim-ai.vim
+++ b/plugin/vim-ai.vim
@@ -12,11 +12,11 @@ augroup vim_ai
\ let g:vim_ai_is_selection_pending = mode() =~# "^[vV\<C-v>]"
augroup END
-command! -range -nargs=? AI <line1>,<line2>call vim_ai#AIRun({}, <q-args>)
-command! -range -nargs=? AIEdit <line1>,<line2>call vim_ai#AIEditRun({}, <q-args>)
+command! -range -nargs=? -complete=customlist,vim_ai#RoleCompletion AI <line1>,<line2>call vim_ai#AIRun({}, <q-args>)
+command! -range -nargs=? -complete=customlist,vim_ai#RoleCompletion AIEdit <line1>,<line2>call vim_ai#AIEditRun({}, <q-args>)
" Whereas AI and AIEdit default to passing the current line as range
" AIChat defaults to passing nothing which is achieved by -range=0 and passing
" <count> as described at https://stackoverflow.com/a/20133772
-command! -range=0 -nargs=? AIChat <line1>,<line2>call vim_ai#AIChatRun(<count>, {}, <q-args>)
-command! -nargs=? AINewChat call vim_ai#AINewChatRun(<f-args>)
-command! AIRedo call vim_ai#AIRedoRun()
+command! -range=0 -nargs=? -complete=customlist,vim_ai#RoleCompletion AIChat <line1>,<line2>call vim_ai#AIChatRun(<count>, {}, <q-args>)
+command! -nargs=? AINewChat call vim_ai#AINewChatRun(<f-args>)
+command! AIRedo call vim_ai#AIRedoRun()
diff --git a/py/chat.py b/py/chat.py
index ff70904..67f2d5d 100644
--- a/py/chat.py
+++ b/py/chat.py
@@ -4,10 +4,14 @@ import vim
plugin_root = vim.eval("s:plugin_root")
vim.command(f"py3file {plugin_root}/py/utils.py")
+prompt, role_options = parse_prompt_and_role(vim.eval("l:prompt"))
config = normalize_config(vim.eval("l:config"))
-config_options = config['options']
+config_options = {
+ **config['options'],
+ **role_options['options_default'],
+ **role_options['options_chat'],
+}
config_ui = config['ui']
-prompt = vim.eval("l:prompt").strip()
def initialize_chat_window():
lines = vim.eval('getline(1, "$")')
@@ -38,7 +42,7 @@ def initialize_chat_window():
vim.command("normal! i\n>>> user\n\n")
if prompt:
- vim.command("normal! a" + prompt)
+ vim.command("normal! i" + prompt)
vim_break_undo_sequence()
vim.command("redraw")
@@ -72,7 +76,7 @@ try:
**openai_options
}
printDebug("[chat] request: {}", request)
- url = config_options['endpoint_url']
+ url = options['endpoint_url']
response = openai_request(url, request, http_options)
def map_chunk(resp):
printDebug("[chat] response: {}", resp)
diff --git a/py/complete.py b/py/complete.py
index debe275..f340e96 100644
--- a/py/complete.py
+++ b/py/complete.py
@@ -6,11 +6,16 @@ vim.command(f"py3file {plugin_root}/py/utils.py")
config = normalize_config(vim.eval("l:config"))
engine = config['engine']
-config_options = config['options']
+
+prompt, role_options = parse_prompt_and_role(vim.eval("l:prompt"))
+config_options = {
+ **config['options'],
+ **role_options['options_default'],
+ **role_options['options_complete'],
+}
openai_options = make_openai_options(config_options)
http_options = make_http_options(config_options)
-prompt = vim.eval("l:prompt").strip()
is_selection = vim.eval("l:is_selection")
def complete_engine(prompt):
diff --git a/py/roles.py b/py/roles.py
new file mode 100644
index 0000000..b7d19e1
--- /dev/null
+++ b/py/roles.py
@@ -0,0 +1,23 @@
+import vim
+
+# import utils
+plugin_root = vim.eval("s:plugin_root")
+vim.command(f"py3file {plugin_root}/py/utils.py")
+
+roles_config_path = os.path.expanduser(vim.eval("g:vim_ai_roles_config_file"))
+if not os.path.exists(roles_config_path):
+ raise Exception(f"Role config file does not exist: {roles_config_path}")
+
+roles = configparser.ConfigParser()
+roles.read(roles_config_path)
+
+enhance_roles_with_custom_function(roles)
+
+role_names = [name for name in roles.sections() if not '.' in name]
+
+role_list = [f'"{name}"' for name in role_names]
+role_list = ", ".join(role_list)
+
+role_list = f"[{role_list}]"
+
+vim.command(f'let l:role_list = {role_list}')
diff --git a/py/utils.py b/py/utils.py
index 19de7c4..963377d 100644
--- a/py/utils.py
+++ b/py/utils.py
@@ -11,6 +11,7 @@ import re
from urllib.error import URLError
from urllib.error import HTTPError
import traceback
+import configparser
is_debugging = vim.eval("g:vim_ai_debug") == "1"
debug_log_file = vim.eval("g:vim_ai_debug_log_file")
@@ -261,3 +262,59 @@ def handle_completion_error(error):
def clear_echo_message():
# https://neovim.discourse.group/t/how-to-clear-the-echo-message-in-the-command-line/268/3
vim.command("call feedkeys(':','nx')")
+
+def enhance_roles_with_custom_function(roles):
+ if vim.eval("exists('g:vim_ai_roles_config_function')") == '1':
+ roles_config_function = vim.eval("g:vim_ai_roles_config_function")
+ if not vim.eval("exists('*" + roles_config_function + "')"):
+ raise Exception(f"Role config function does not exist: {roles_config_function}")
+ else:
+ roles.update(vim.eval(roles_config_function + "()"))
+
+def load_role_config(role):
+ roles_config_path = os.path.expanduser(vim.eval("g:vim_ai_roles_config_file"))
+ if not os.path.exists(roles_config_path):
+ raise Exception(f"Role config file does not exist: {roles_config_path}")
+
+ roles = configparser.ConfigParser()
+ roles.read(roles_config_path)
+
+ enhance_roles_with_custom_function(roles)
+
+ if not role in roles:
+ raise Exception(f"Role `{role}` not found")
+
+ options = roles[f"{role}.options"] if f"{role}.options" in roles else {}
+ options_complete =roles[f"{role}.options-complete"] if f"{role}.options-complete" in roles else {}
+ options_chat = roles[f"{role}.options-chat"] if f"{role}.options-chat" in roles else {}
+
+ return {
+ 'role': dict(roles[role]),
+ 'options': {
+ 'options_default': dict(options),
+ 'options_complete': dict(options_complete),
+ 'options_chat': dict(options_chat),
+ },
+ }
+
+empty_role_options = {
+ 'options_default': {},
+ 'options_complete': {},
+ 'options_chat': {},
+}
+
+def parse_prompt_and_role(raw_prompt):
+ prompt = raw_prompt.strip()
+ role = re.split(' |:', prompt)[0]
+ if not role.startswith('/'):
+ # does not require role
+ return (prompt, empty_role_options)
+
+ prompt = prompt[len(role):].strip()
+ role = role[1:]
+
+ config = load_role_config(role)
+ if 'prompt' in config['role'] and config['role']['prompt']:
+ delim = '' if prompt.startswith(':') else ':\n'
+ prompt = config['role']['prompt'] + delim + prompt
+ return (prompt, config['options'])
diff --git a/roles-example.ini b/roles-example.ini
new file mode 100644
index 0000000..90d32bc
--- /dev/null
+++ b/roles-example.ini
@@ -0,0 +1,22 @@
+# .ini file structure:
+# - https://docs.python.org/3/library/configparser.html#supported-ini-file-structure
+
+[grammar]
+prompt = fix spelling and grammar
+
+[refactor]
+prompt =
+ You are a Clean Code expert, I have the following code,
+ please refactor it in a more clean and concise way so that my colleagues
+ can maintain the code more easily. Also, explain why you want to refactor
+ the code so that I can add the explanation to the Pull Request.
+
+# common options for all engines
+[refactor.options]
+temperature = 0.4
+
+# engine specific options:
+[refactor.options-chat]
+model = gpt-4
+
+[refactor.options-complete]