├── .gitignore
├── LICENSE
├── README.md
├── plugin
└── vim_codex.vim
└── python
└── plugin.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
131 |
132 | AUTH.py
133 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 Tom Dörr
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
🤖 Vim Codex
2 |
3 |
4 | An AI plugin that does the work for you.
5 |
6 |
7 |
8 |
13 |
18 |
23 |
24 |
25 |
26 |
27 |
28 |
29 | This is a simple plugin for Vim that will allow you to use OpenAI Codex.
30 | To use this plugin you need to get access to OpenAI's [Codex API](https://openai.com/blog/openai-codex/).
31 |
32 |
33 |
34 | ## Installation
35 |
36 | The easiest way to install the plugin is to install it as a bundle.
37 | For example, using [Pathogen](https://github.com/tpope/vim-pathogen):
38 |
39 | 1. Get and install [pathogen.vim](https://github.com/tpope/vim-pathogen). You can skip this step
40 | if you already have it installed.
41 |
42 | 2. `cd ~/.vim/bundle`
43 |
44 | 3. `git clone git@github.com:tom-doerr/vim_codex.git`
45 |
46 | Bundle installs are known to work fine also when using [Vundle](https://github.com/gmarik/vundle). Other
47 | bundle managers are expected to work as well.
48 |
49 |
50 |
51 | After installing the plugin, you need to install the openai package::
52 | ```
53 | pip3 install openai
54 | ```
55 |
56 | After running `:CreateCompletion` once, the file `~/.config/openaiapirc` is created where you need to enter your OpenAI authentication information.
57 | You can find your authentication information on the [website](https://beta.openai.com/account/api-keys).
58 |
59 |
60 |
61 | ## Usage
62 | The plugin provides a `CreateCompletion` command which you can call by default using the mapping
63 | `co`.
64 | You can give the `CreateCompletion` command the number of tokens it should produce as an argument, e.g. `CreateCompletion 1000`.
65 | If you want to just complete the current line, run `CreateCompletionLine`.
66 |
67 | To complete the current text from insert and normal mode using Ctrl+x, you can add the following
68 | lines to your .vimrc::
69 | ```
70 | nnoremap :CreateCompletion
71 | inoremap liul:CreateCompletion
72 | ```
73 |
74 |
75 | ## Updating
76 |
77 | ### Manually
78 |
79 | In order to update the plugin, go to its bundle directory and use
80 | Git to update it:
81 |
82 | 1. `cd ~/.vim/bundle/vim_codex`
83 |
84 | 2. `git pull`
85 |
86 |
87 | ### With Vundle
88 |
89 | Use the `:BundleUpdate` command provided by Vundle, for example invoking
90 | Vim like this::
91 | ```
92 | % vim +BundleUpdate
93 | ```
94 |
95 | -------------------------------------------------------------------
96 |
97 | [Traffic Statistics](https://tom-doerr.github.io/github_repo_stats_data/tom-doerr/vim_codex/latest-report/report.html)
98 |
99 |
100 |
--------------------------------------------------------------------------------
/plugin/vim_codex.vim:
--------------------------------------------------------------------------------
1 | if !has("python3")
2 | echo "vim has to be compiled with +python3 to run this"
3 | finish
4 | endif
5 |
6 | if exists('g:vim_codex_loaded')
7 | finish
8 | endif
9 |
10 |
11 | let s:plugin_root_dir = fnamemodify(resolve(expand(':p')), ':h')
12 |
13 | python3 << EOF
14 | import sys
15 | from os.path import normpath, join
16 | import vim
17 | plugin_root_dir = vim.eval('s:plugin_root_dir')
18 | python_root_dir = normpath(join(plugin_root_dir, '..', 'python'))
19 | sys.path.insert(0, python_root_dir)
20 | import plugin
21 | EOF
22 |
23 |
24 |
25 | function! CreateCompletion(max_tokens)
26 | python3 plugin.create_completion()
27 | endfunction
28 |
29 | function! CreateCompletionLine()
30 | python3 plugin.create_completion(stop='\n')
31 | endfunction
32 |
33 |
34 |
35 | command! -nargs=? CreateCompletion call CreateCompletion()
36 | command! -nargs=0 CreateCompletionLine call CreateCompletionLine()
37 |
38 | map co :CreateCompletion
39 |
40 |
41 | let g:vim_codex_loaded = 1
42 |
--------------------------------------------------------------------------------
/python/plugin.py:
--------------------------------------------------------------------------------
1 | import urllib, urllib.request
2 | import json
3 | import os
4 | import configparser
5 | import sys
6 |
7 | try:
8 | import vim
9 | except:
10 | print("No vim module available outside vim")
11 | pass
12 |
13 |
14 | from openai import OpenAI
15 |
16 | client = None
17 |
18 | MAX_SUPPORTED_INPUT_LENGTH = 4096
19 | USE_STREAM_FEATURE = True
20 | MAX_TOKENS_DEFAULT = 64
21 |
22 | CONFIG_DIR = os.getenv('XDG_CONFIG_HOME', os.path.expanduser('~/.config'))
23 | API_KEYS_LOCATION = os.path.join(CONFIG_DIR, 'openaiapirc')
24 |
25 | def create_template_ini_file():
26 | """
27 | If the ini file does not exist create it and add the organization_id and
28 | secret_key
29 | """
30 | if not os.path.isfile(API_KEYS_LOCATION):
31 | with open(API_KEYS_LOCATION, 'w') as f:
32 | f.write('[openai]\n')
33 | f.write('organization_id=\n')
34 | f.write('secret_key=\n')
35 |
36 | print('OpenAI API config file created at {}'.format(API_KEYS_LOCATION))
37 | print('Please edit it and add your organization ID and secret key')
38 | print('If you do not yet have an organization ID and secret key, you\n'
39 | 'need to register for OpenAI Codex: \n'
40 | 'https://openai.com/blog/openai-codex/')
41 | sys.exit(1)
42 |
43 |
44 | def initialize_openai_api():
45 | """
46 | Initialize the OpenAI API
47 | """
48 | global client
49 | # Check if file at API_KEYS_LOCATION exists
50 | create_template_ini_file()
51 | config = configparser.ConfigParser()
52 | config.read(API_KEYS_LOCATION)
53 |
54 | client = OpenAI(api_key=config['openai']['secret_key'].strip('"').strip("'"))
55 |
56 |
57 | def complete_input_max_length(input_prompt, max_input_length=MAX_SUPPORTED_INPUT_LENGTH, stop=None, max_tokens=64):
58 | input_prompt = input_prompt[-max_input_length:]
59 | response = client.completions.create(model='gpt-3.5-turbo-instruct', prompt=input_prompt, best_of=1, temperature=0.5, max_tokens=max_tokens, stream=USE_STREAM_FEATURE, stop=stop)
60 | return response
61 |
62 | def complete_input(input_prompt, stop, max_tokens):
63 | try:
64 | response = complete_input_max_length(input_prompt, int(2.5 * MAX_SUPPORTED_INPUT_LENGTH), stop=stop, max_tokens=max_tokens)
65 | except openai.InvalidRequestError:
66 | response = complete_input_max_length(input_prompt, MAX_SUPPORTED_INPUT_LENGTH, stop=stop, max_tokens=max_tokens)
67 | print('Using shorter input.')
68 |
69 | return response
70 |
71 | def get_max_tokens():
72 | max_tokens = None
73 | if vim.eval('exists("a:max_tokens")') == '1':
74 | max_tokens_str = vim.eval('a:max_tokens')
75 | if max_tokens_str:
76 | max_tokens = int(max_tokens_str)
77 |
78 | if not max_tokens:
79 | max_tokens = MAX_TOKENS_DEFAULT
80 |
81 | return max_tokens
82 |
83 | def delete_current_line_if_empty_and_stop_below_matches_stop_string(stop):
84 | vim_buf = vim.current.buffer
85 | row, col = vim.current.window.cursor
86 | if row == len(vim_buf):
87 | return
88 | # Get next none empty line using get_first_line_below_cursor_with_text
89 | next_line = get_first_line_below_cursor_with_text()
90 | if next_line == stop:
91 | if len(vim_buf[row-1]) == 0:
92 | vim_buf[row-1:row] = []
93 |
94 | def delete_empty_inserted_lines_if_stop_matches_stop_string(stop):
95 | vim_buf = vim.current.buffer
96 | row, col = vim.current.window.cursor
97 | if row == len(vim_buf):
98 | return
99 | # Get next none empty line using get_first_line_below_cursor_with_text
100 | next_line = get_first_line_below_cursor_with_text()
101 | if next_line == stop:
102 | while True:
103 | if row >= len(vim_buf):
104 | break
105 | # Print the number of lines.
106 | if len(vim_buf[row-1]) == 0:
107 | vim_buf[row-1:row] = []
108 | else:
109 | break
110 | if len(vim_buf[row-1]) == 0:
111 | vim_buf[row-1:row] = []
112 |
113 | def get_first_line_below_cursor_with_text():
114 | vim_buf = vim.current.buffer
115 | row, col = vim.current.window.cursor
116 | while True:
117 | if row == len(vim_buf):
118 | return None
119 | if len(vim_buf[row]) > 0:
120 | return vim_buf[row]
121 | row += 1
122 |
123 |
124 | def create_completion(stop=None):
125 | if client is None:
126 | initialize_openai_api()
127 | max_tokens = get_max_tokens()
128 | vim_buf = vim.current.buffer
129 | input_prompt = '\n'.join(vim_buf[:])
130 |
131 | row, col = vim.current.window.cursor
132 | input_prompt = '\n'.join(vim_buf[row:])
133 | input_prompt += '\n'.join(vim_buf[:row-1])
134 | input_prompt += '\n' + vim_buf[row-1][:col]
135 | if not stop:
136 | stop = get_first_line_below_cursor_with_text()
137 | response = complete_input(input_prompt, stop=stop, max_tokens=max_tokens)
138 | write_response(response, stop=stop)
139 |
140 | def write_response(response, stop):
141 | vim_buf = vim.current.buffer
142 | vim_win = vim.current.window
143 | while True:
144 | # TODO: Fix bug that causes Vim to freeze when arrow keys are used.
145 | # Check if the user pressed any key.
146 | if vim_win.cursor[0] > len(vim_buf):
147 | return
148 | if vim_win.cursor[0] == len(vim_buf) and vim_win.cursor[1] > len(vim_buf[-1]):
149 | return
150 | if vim.eval('getchar(0)') != '0':
151 | return
152 |
153 | if USE_STREAM_FEATURE:
154 | single_response = next(response)
155 | else:
156 | single_response = response
157 | completion = single_response.choices[0].text
158 | if single_response.choices[0].finish_reason != None:
159 | if stop == '\n':
160 | completion += '\n'
161 | row, col = vim.current.window.cursor
162 | current_line = vim.current.buffer[row-1]
163 | new_line = current_line[:col] + completion + current_line[col:]
164 | if not USE_STREAM_FEATURE:
165 | if new_line == '':
166 | new_line = new_line
167 | elif new_line[-1] == '\n':
168 | new_line = new_line[:-1]
169 | new_lines = new_line.split('\n')
170 | new_lines.reverse()
171 | if len(vim_buf) == row:
172 | vim_buf.append('')
173 |
174 | vim_buf[row-1] = None
175 | cursor_pos_base = tuple(vim_win.cursor)
176 | for row_i in range(len(new_lines)):
177 | vim.current.buffer[row-1:row-1] = [new_lines[row_i]]
178 |
179 | if new_line == '':
180 | cursor_target_col = 0
181 | elif new_line[-1] != '\n':
182 | cursor_target_col = len(new_lines[0])
183 | else:
184 | cursor_target_col = 0
185 | vim_win.cursor = (cursor_pos_base[0] + row_i, cursor_target_col)
186 |
187 | if not USE_STREAM_FEATURE:
188 | break
189 |
190 | # Flush the vim buffer.
191 | vim.command("redraw")
192 | if USE_STREAM_FEATURE:
193 | if single_response.choices[0].finish_reason != None:
194 | # delete_current_line_if_empty_and_stop_below_matches_stop_string(stop)
195 | delete_empty_inserted_lines_if_stop_matches_stop_string(stop)
196 | break
197 |
198 |
199 |
--------------------------------------------------------------------------------