vimlm 0.0.5__py3-none-any.whl → 0.0.7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- vimlm-0.0.7.dist-info/METADATA +204 -0
- vimlm-0.0.7.dist-info/RECORD +7 -0
- vimlm.py +410 -159
- vimlm-0.0.5.dist-info/METADATA +0 -108
- vimlm-0.0.5.dist-info/RECORD +0 -7
- {vimlm-0.0.5.dist-info → vimlm-0.0.7.dist-info}/LICENSE +0 -0
- {vimlm-0.0.5.dist-info → vimlm-0.0.7.dist-info}/WHEEL +0 -0
- {vimlm-0.0.5.dist-info → vimlm-0.0.7.dist-info}/entry_points.txt +0 -0
- {vimlm-0.0.5.dist-info → vimlm-0.0.7.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,204 @@
|
|
1
|
+
Metadata-Version: 2.2
|
2
|
+
Name: vimlm
|
3
|
+
Version: 0.0.7
|
4
|
+
Summary: VimLM - LLM-powered Vim assistant
|
5
|
+
Home-page: https://github.com/JosefAlbers/vimlm
|
6
|
+
Author: Josef Albers
|
7
|
+
Author-email: albersj66@gmail.com
|
8
|
+
Requires-Python: >=3.12.8
|
9
|
+
Description-Content-Type: text/markdown
|
10
|
+
License-File: LICENSE
|
11
|
+
Requires-Dist: nanollama==0.0.5
|
12
|
+
Requires-Dist: mlx_lm_utils==0.0.2
|
13
|
+
Requires-Dist: watchfiles==1.0.4
|
14
|
+
Dynamic: author
|
15
|
+
Dynamic: author-email
|
16
|
+
Dynamic: description
|
17
|
+
Dynamic: description-content-type
|
18
|
+
Dynamic: home-page
|
19
|
+
Dynamic: requires-dist
|
20
|
+
Dynamic: requires-python
|
21
|
+
Dynamic: summary
|
22
|
+
|
23
|
+
|
24
|
+
# VimLM - Local LLM-Powered Coding Assistant for Vim
|
25
|
+
|
26
|
+

|
27
|
+
|
28
|
+
LLM-powered coding companion for Vim, inspired by GitHub Copilot/Cursor. Integrates contextual code understanding, summarization, and AI assistance directly into your Vim workflow.
|
29
|
+
|
30
|
+
## Features
|
31
|
+
|
32
|
+
- **Model Agnostic** - Use any MLX-compatible model via a configuration file
|
33
|
+
- **Vim-Native UX** - Intuitive keybindings and split-window responses
|
34
|
+
- **Deep Context** - Understands code context from:
|
35
|
+
- Current file
|
36
|
+
- Visual selections
|
37
|
+
- Referenced files
|
38
|
+
- Project directory structure
|
39
|
+
- **Conversational Coding** - Iterative refinement with follow-up queries
|
40
|
+
- **Air-Gapped Security** - 100% offline - no APIs, no tracking, no data leaks
|
41
|
+
|
42
|
+
## Requirements
|
43
|
+
|
44
|
+
- Apple M-series chip
|
45
|
+
- Python 3.12.8
|
46
|
+
|
47
|
+
## Quick Start
|
48
|
+
|
49
|
+
```zsh
|
50
|
+
pip install vimlm
|
51
|
+
vimlm
|
52
|
+
```
|
53
|
+
|
54
|
+
## Basic Usage
|
55
|
+
|
56
|
+
### 1. **From Normal Mode**
|
57
|
+
**`Ctrl-l`**: Add current line + file to context
|
58
|
+
|
59
|
+
*Example prompt:* `"Regex for removing HTML tags from item.content"`
|
60
|
+
|
61
|
+
### 2. **From Visual Mode**
|
62
|
+
Select code → **`Ctrl-l`**: Add selected block + current file to context
|
63
|
+
|
64
|
+
*Example prompt:* `"Convert this to async/await syntax"`
|
65
|
+
|
66
|
+
### 3. **Follow-Up Conversations**
|
67
|
+
**`Ctrl-j`**: Continue current thread
|
68
|
+
|
69
|
+
*Example follow-up:* `"Use Manifest V3 instead"`
|
70
|
+
|
71
|
+
### 4. **Code Extraction & Replacement**
|
72
|
+
**`Ctrl-p`**: Insert code blocks from response into:
|
73
|
+
- Last visual selection (Normal mode)
|
74
|
+
- Active selection (Visual mode)
|
75
|
+
|
76
|
+
**Workflow Example**:
|
77
|
+
1. Select a block of code in Visual mode
|
78
|
+
2. Prompt with `Ctrl-l`: `"Convert this to async/await syntax"`
|
79
|
+
3. Press `Ctrl-p` to replace selection with generated code
|
80
|
+
|
81
|
+
### 5. **Inline Commands**
|
82
|
+
|
83
|
+
#### `!include` - Add External Context
|
84
|
+
```text
|
85
|
+
!include [PATH] # Add files/folders to context
|
86
|
+
```
|
87
|
+
- **`!include`** (no path): Current folder
|
88
|
+
- **`!include ~/projects/utils.py`**: Specific file
|
89
|
+
- **`!include ~/docs/api-specs/`**: Entire folder
|
90
|
+
|
91
|
+
*Example:* `"AJAX-ify this app !include ~/scrap/hypermedia-applications.summ.md"`
|
92
|
+
|
93
|
+
#### `!deploy` - Generate Project Files
|
94
|
+
```text
|
95
|
+
!deploy [DEST_DIR] # Extract code blocks to directory
|
96
|
+
```
|
97
|
+
- **`!deploy`** (no path): Current directory
|
98
|
+
- **`!deploy ./src`**: Specific directory
|
99
|
+
|
100
|
+
*Example:* `"Create REST API endpoint !deploy ./api"`
|
101
|
+
|
102
|
+
#### `!continue` - Resume Generation
|
103
|
+
```text
|
104
|
+
!continue [MAX_TOKENS] # Continue stopped response
|
105
|
+
```
|
106
|
+
- **`!continue`**: Default 2000 tokens
|
107
|
+
- **`!continue 3000`**: Custom token limit
|
108
|
+
|
109
|
+
*Example:* `"tl;dr !include large-file.txt !continue 5000"`
|
110
|
+
|
111
|
+
#### `!followup` - Thread Continuation
|
112
|
+
```text
|
113
|
+
!followup # Equivalent to Ctrl-j
|
114
|
+
```
|
115
|
+
*Example:*
|
116
|
+
|
117
|
+
Initial: `"Create Chrome extension"`
|
118
|
+
|
119
|
+
Follow-up: `"Add dark mode support !followup"`
|
120
|
+
|
121
|
+
#### **Command Combinations**
|
122
|
+
Chain multiple commands in one prompt:
|
123
|
+
```text
|
124
|
+
"Create HTMX component !include ~/lib/styles.css !deploy ./components !continue 4000"
|
125
|
+
```
|
126
|
+
|
127
|
+
### 6. **Command-Line Mode `:VimLM`**
|
128
|
+
```vim
|
129
|
+
:VimLM "prompt" [!command1] [!command2]...
|
130
|
+
```
|
131
|
+
Use predefined command chains for repetitive tasks:
|
132
|
+
|
133
|
+
**Example 1 – CI/CD Fixer Macro**:
|
134
|
+
```vim
|
135
|
+
" Debug CI failures using error logs
|
136
|
+
:VimLM Fix Dockerfile !include .gitlab-ci.yml !include $(tail -n 20 ci.log)
|
137
|
+
```
|
138
|
+
|
139
|
+
**Example 2 – Test Generation Workflow**:
|
140
|
+
```vim
|
141
|
+
" Generate unit tests for selected functions and save to test/
|
142
|
+
:VimLM Write pytest tests for this !include ./src !deploy ./test
|
143
|
+
```
|
144
|
+
|
145
|
+
**Example 3 – Documentation Helper**:
|
146
|
+
```vim
|
147
|
+
" Add docstrings to all Python functions in file
|
148
|
+
:VimLM Add Google-style docstrings !include % !continue 4000
|
149
|
+
```
|
150
|
+
|
151
|
+
### Key Bindings
|
152
|
+
|
153
|
+
| Binding | Mode | Action |
|
154
|
+
|------------|---------------|----------------------------------------|
|
155
|
+
| `Ctrl-l` | Normal/Visual | Send current file + selection to LLM |
|
156
|
+
| `Ctrl-j` | Normal | Continue conversation |
|
157
|
+
| `Ctrl-p` | Normal/Visual | Replace the selection with generated code |
|
158
|
+
| `Esc` | Prompt | Cancel input |
|
159
|
+
|
160
|
+
## Advanced Configuration
|
161
|
+
VimLM uses a JSON config file with the following configurable parameters:
|
162
|
+
```json
|
163
|
+
{
|
164
|
+
"LLM_MODEL": null,
|
165
|
+
"NUM_TOKEN": 2000,
|
166
|
+
"USE_LEADER": false,
|
167
|
+
"KEY_MAP": {},
|
168
|
+
"DO_RESET": true,
|
169
|
+
"SHOW_USER": false,
|
170
|
+
"SEP_CMD": "!",
|
171
|
+
"VERSION": "0.0.7",
|
172
|
+
"DEBUG": true
|
173
|
+
}
|
174
|
+
```
|
175
|
+
### Custom Model Setup
|
176
|
+
1. **Browse models**: [MLX Community Models on Hugging Face](https://huggingface.co/mlx-community)
|
177
|
+
2. **Edit config file**:
|
178
|
+
```json
|
179
|
+
{
|
180
|
+
"LLM_MODEL": "mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit",
|
181
|
+
"NUM_TOKEN": 9999
|
182
|
+
}
|
183
|
+
```
|
184
|
+
3. **Save to**: `~/vimlm/cfg.json`
|
185
|
+
4. **Restart VimLM**
|
186
|
+
|
187
|
+
### Custom Key Bindings
|
188
|
+
You can also configure shortcuts:
|
189
|
+
```json
|
190
|
+
{
|
191
|
+
"USE_LEADER": true, // Swap Ctrl for Leader key
|
192
|
+
"KEY_MAP": { // Remap default keys (l/j/p)
|
193
|
+
"l": "a", // <Leader>a instead of <Leader>l
|
194
|
+
"j": "s", // <Leader>s instead of <Leader>j
|
195
|
+
"p": "d" // <Leader>d instead of <Leader>p
|
196
|
+
}
|
197
|
+
}
|
198
|
+
```
|
199
|
+
|
200
|
+
## License
|
201
|
+
|
202
|
+
VimLM is licensed under the [Apache-2.0 license](LICENSE).
|
203
|
+
|
204
|
+
|
@@ -0,0 +1,7 @@
|
|
1
|
+
vimlm.py,sha256=QrboU-loWuQjTPO5HTJnt_CCxZcV6cclX6UOz438rEM,23126
|
2
|
+
vimlm-0.0.7.dist-info/LICENSE,sha256=f1xgK8fAXg_intwnbc9nLkHf7ODPLtgpHs7DetQHOro,11343
|
3
|
+
vimlm-0.0.7.dist-info/METADATA,sha256=0V2b8X4pq2uGlyuxyqCUlCYZ7tmlJhPOLZFaDjG0V6s,5668
|
4
|
+
vimlm-0.0.7.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
5
|
+
vimlm-0.0.7.dist-info/entry_points.txt,sha256=mU5V4MYsuIzCc6YB-Ro-6USSHWN5vHw8UDnTEoq0isw,36
|
6
|
+
vimlm-0.0.7.dist-info/top_level.txt,sha256=I8GjqoiP--scYsO3AfLhha-6Ax9ci3IvbWvVbPv8g94,6
|
7
|
+
vimlm-0.0.7.dist-info/RECORD,,
|
vimlm.py
CHANGED
@@ -24,14 +24,23 @@ import argparse
|
|
24
24
|
import tempfile
|
25
25
|
from pathlib import Path
|
26
26
|
from string import Template
|
27
|
+
import re
|
28
|
+
|
29
|
+
DEFAULTS = dict(
|
30
|
+
LLM_MODEL = None, # "mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit"
|
31
|
+
NUM_TOKEN = 2000,
|
32
|
+
USE_LEADER = False,
|
33
|
+
KEY_MAP = {},
|
34
|
+
DO_RESET = True,
|
35
|
+
SHOW_USER = False,
|
36
|
+
SEP_CMD = '!',
|
37
|
+
VERSION = '0.0.7',
|
38
|
+
DEBUG = True,
|
39
|
+
)
|
27
40
|
|
28
|
-
DEBUG = True
|
29
|
-
LLM_MODEL = "mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit"
|
30
|
-
NUM_TOKEN = 2000
|
31
|
-
SEP_CMD = '!@#$'
|
32
41
|
VIMLM_DIR = os.path.expanduser("~/vimlm")
|
33
42
|
WATCH_DIR = os.path.expanduser("~/vimlm/watch_dir")
|
34
|
-
CFG_FILE =
|
43
|
+
CFG_FILE = 'cfg.json'
|
35
44
|
LOG_FILE = "log.json"
|
36
45
|
LTM_FILE = "cache.json"
|
37
46
|
OUT_FILE = "response.md"
|
@@ -41,10 +50,42 @@ LOG_PATH = os.path.join(VIMLM_DIR, LOG_FILE)
|
|
41
50
|
LTM_PATH = os.path.join(VIMLM_DIR, LTM_FILE)
|
42
51
|
OUT_PATH = os.path.join(WATCH_DIR, OUT_FILE)
|
43
52
|
|
44
|
-
def
|
45
|
-
|
53
|
+
def is_old(config):
|
54
|
+
v_str = config.get('VERSION', 0)
|
55
|
+
for min_v, usr_v in zip(DEFAULTS['VERSION'].split('.'), v_str.split('.')):
|
56
|
+
if int(min_v) < int(usr_v):
|
57
|
+
return False
|
58
|
+
elif int(min_v) > int(usr_v):
|
59
|
+
return True
|
60
|
+
return False
|
61
|
+
|
62
|
+
if os.path.exists(WATCH_DIR):
|
63
|
+
shutil.rmtree(WATCH_DIR)
|
64
|
+
os.makedirs(WATCH_DIR)
|
65
|
+
|
66
|
+
try:
|
67
|
+
with open(CFG_PATH, "r") as f:
|
68
|
+
config = json.load(f)
|
69
|
+
if is_old(config):
|
70
|
+
for p in [CFG_PATH, LOG_PATH, LTM_PATH]:
|
71
|
+
if os.path.isfile(p):
|
72
|
+
os.remove(p)
|
73
|
+
raise ValueError(f'Version mismatch')
|
74
|
+
except Exception as e:
|
75
|
+
print(e)
|
76
|
+
config = DEFAULTS
|
77
|
+
with open(CFG_PATH, 'w') as f:
|
78
|
+
json.dump(DEFAULTS, f, indent=2)
|
79
|
+
|
80
|
+
for k, v in DEFAULTS.items():
|
81
|
+
globals()[k] = config.get(k, v)
|
82
|
+
|
83
|
+
def toout(s, key=None, mode=None):
|
84
|
+
key = '' if key is None else ':'+key
|
85
|
+
mode = 'w' if mode is None else mode
|
86
|
+
with open(OUT_PATH, mode, encoding='utf-8') as f:
|
46
87
|
f.write(s)
|
47
|
-
tolog(s, key)
|
88
|
+
tolog(s, key='tovim'+key+':'+mode)
|
48
89
|
|
49
90
|
def tolog(log, key='debug'):
|
50
91
|
if not DEBUG and key == 'debug':
|
@@ -58,31 +99,49 @@ def tolog(log, key='debug'):
|
|
58
99
|
with open(LOG_PATH, "w", encoding="utf-8") as log_f:
|
59
100
|
json.dump(logs, log_f, indent=2)
|
60
101
|
|
61
|
-
if os.path.exists(WATCH_DIR):
|
62
|
-
shutil.rmtree(WATCH_DIR)
|
63
|
-
os.makedirs(WATCH_DIR)
|
64
|
-
|
65
|
-
try:
|
66
|
-
with open(CFG_PATH, "r") as f:
|
67
|
-
config = json.load(f)
|
68
|
-
DEBUG = config.get("DEBUG", DEBUG)
|
69
|
-
LLM_MODEL = config.get("LLM_MODEL", LLM_MODEL)
|
70
|
-
NUM_TOKEN = config.get("NUM_TOKEN", NUM_TOKEN)
|
71
|
-
SEP_CMD = config.get("SEP_CMD", SEP_CMD)
|
72
|
-
except Exception as e:
|
73
|
-
tolog(str(e))
|
74
|
-
with open(CFG_PATH, 'w') as f:
|
75
|
-
json.dump(dict(DEBUG=DEBUG, LLM_MODEL=LLM_MODEL, NUM_TOKEN=NUM_TOKEN, SEP_CMD=SEP_CMD), f, indent=2)
|
76
|
-
|
77
102
|
toout('Loading LLM...')
|
78
103
|
if LLM_MODEL is None:
|
79
|
-
from
|
80
|
-
chat = Chat(
|
81
|
-
toout('LLM is ready')
|
104
|
+
from nanollama import Chat
|
105
|
+
chat = Chat(model_path='uncn_llama_32_3b_it')
|
106
|
+
toout(f'LLM is ready')
|
82
107
|
else:
|
83
108
|
from mlx_lm_utils import Chat
|
84
109
|
chat = Chat(model_path=LLM_MODEL)
|
85
|
-
toout(f'{
|
110
|
+
toout(f'{model_path.split('/')[-1]} is ready')
|
111
|
+
|
112
|
+
def deploy(dest=None, src=None, reformat=True):
|
113
|
+
prompt_deploy = 'Reformat the text to ensure each code block is preceded by a filename in **filename.ext** format, with only alphanumeric characters, dots, underscores, or hyphens in the filename. Remove any extraneous characters from filenames.'
|
114
|
+
tolog(f'deploy {dest=} {src=} {reformat=}')
|
115
|
+
if src:
|
116
|
+
chat.reset()
|
117
|
+
with open(src, 'r') as f:
|
118
|
+
prompt_deploy = f.read().strip() + '\n\n---\n\n' + prompt_deploy
|
119
|
+
if reformat:
|
120
|
+
response = chat(prompt_deploy, max_new=NUM_TOKEN, verbose=False, stream=False)['text']
|
121
|
+
toout(response, 'deploy')
|
122
|
+
lines = response.splitlines()
|
123
|
+
else:
|
124
|
+
with open(OUT_PATH, 'r') as f:
|
125
|
+
lines = f.readlines()
|
126
|
+
dest = get_path(dest)
|
127
|
+
os.makedirs(dest, exist_ok=True)
|
128
|
+
current_filename = None
|
129
|
+
code_block = []
|
130
|
+
in_code_block = False
|
131
|
+
for line in lines:
|
132
|
+
line = line.rstrip()
|
133
|
+
if line.startswith("```"):
|
134
|
+
if in_code_block and current_filename:
|
135
|
+
with open(os.path.join(dest, os.path.basename(current_filename)), "w", encoding="utf-8") as code_file:
|
136
|
+
code_file.write("\n".join(code_block) + "\n")
|
137
|
+
code_block = []
|
138
|
+
in_code_block = not in_code_block
|
139
|
+
elif in_code_block:
|
140
|
+
code_block.append(line)
|
141
|
+
else:
|
142
|
+
match = re.match(r"^\*\*(.+?)\*\*$", line)
|
143
|
+
if match:
|
144
|
+
current_filename = re.sub(r"[^a-zA-Z0-9_.-]", "", match.group(1))
|
86
145
|
|
87
146
|
def is_binary(file_path):
|
88
147
|
try:
|
@@ -123,7 +182,7 @@ def split_str(doc, max_len=2000, get_len=len):
|
|
123
182
|
return chunks
|
124
183
|
|
125
184
|
def retrieve(src_path, max_len=2000, get_len=len):
|
126
|
-
src_path =
|
185
|
+
src_path = get_path(src_path)
|
127
186
|
result = {}
|
128
187
|
if not os.path.exists(src_path):
|
129
188
|
tolog(f"The path {src_path} does not exist.", 'retrieve')
|
@@ -134,23 +193,31 @@ def retrieve(src_path, max_len=2000, get_len=len):
|
|
134
193
|
content = f.read()
|
135
194
|
result = {src_path:dict(timestamp=os.path.getmtime(src_path), list_str=split_str(content, max_len=max_len, get_len=get_len))}
|
136
195
|
except Exception as e:
|
137
|
-
tolog(f'
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
196
|
+
tolog(f'Failed to retrieve({filename}) due to {e}')
|
197
|
+
else:
|
198
|
+
for filename in os.listdir(src_path):
|
199
|
+
try:
|
200
|
+
file_path = os.path.join(src_path, filename)
|
201
|
+
if filename.startswith('.') or is_binary(file_path):
|
202
|
+
continue
|
203
|
+
if os.path.isfile(file_path):
|
204
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
205
|
+
content = f.read()
|
206
|
+
result[file_path] = dict(timestamp=os.path.getmtime(file_path), list_str=split_str(content, max_len=max_len, get_len=get_len))
|
207
|
+
except Exception as e:
|
208
|
+
tolog(f'Failed to retrieve({filename}) due to {e}')
|
143
209
|
continue
|
144
|
-
if os.path.isfile(file_path):
|
145
|
-
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
146
|
-
content = f.read()
|
147
|
-
result[file_path] = dict(timestamp=os.path.getmtime(file_path), list_str=split_str(content, max_len=max_len, get_len=get_len))
|
148
|
-
except Exception as e:
|
149
|
-
tolog(f'Skipped {filename} due to {e}', 'retrieve')
|
150
|
-
continue
|
151
210
|
return result
|
152
211
|
|
153
|
-
def
|
212
|
+
def get_path(s):
|
213
|
+
if not s:
|
214
|
+
s = '.'
|
215
|
+
s = s.strip()
|
216
|
+
s = os.path.expanduser(s)
|
217
|
+
s = os.path.abspath(s)
|
218
|
+
return s
|
219
|
+
|
220
|
+
def ingest(src, max_len=NUM_TOKEN):
|
154
221
|
def load_cache(cache_path=LTM_PATH):
|
155
222
|
if os.path.exists(cache_path):
|
156
223
|
with open(cache_path, 'r', encoding='utf-8') as f:
|
@@ -163,53 +230,149 @@ def ingest(src):
|
|
163
230
|
current_data[k] = v
|
164
231
|
with open(cache_path, 'w', encoding='utf-8') as f:
|
165
232
|
json.dump(current_data, f, indent=2)
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
233
|
+
src = get_path(src)
|
234
|
+
tolog(f'ingest {src=}')
|
235
|
+
result = ''
|
236
|
+
src_base = os.path.basename(src)
|
237
|
+
if os.path.isdir(src):
|
238
|
+
listdir = [i for i in os.listdir(src) if not i.startswith('.') and '.' in i]
|
239
|
+
result = '\n- '.join([f'--- {src_base} ---', *listdir]) + '\n\n'
|
240
|
+
elif os.path.isfile(src):
|
241
|
+
result = ''
|
242
|
+
else:
|
243
|
+
tolog(f'Failed to ingest({src})')
|
244
|
+
return ''
|
245
|
+
dict_doc = retrieve(src, max_len=max_len, get_len=chat.get_ntok)
|
246
|
+
toout(f'Ingesting {src}...')
|
247
|
+
format_ingest = '{volat}{incoming}\n\n---\n\nPlease provide a succint bullet point summary for above:'
|
248
|
+
format_volat = 'Here is a summary of part 1 of **{k}**:\n\n---\n\n{newsum}\n\n---\n\nHere is the next part:\n\n---\n\n'
|
170
249
|
dict_sum = {}
|
171
250
|
cache = load_cache()
|
251
|
+
max_new_accum = int(max_len/len(dict_doc)) if len(dict_doc) > 0 else max_len
|
172
252
|
for k, v in dict_doc.items():
|
173
|
-
v_stamp = v['timestamp']
|
174
|
-
if v_stamp == cache.get(k, {}).get('timestamp'):
|
175
|
-
dict_sum[k] = cache[k]
|
176
|
-
continue
|
177
253
|
list_str = v['list_str']
|
254
|
+
v_stamp = v['timestamp']
|
178
255
|
if len(list_str) == 0:
|
179
256
|
continue
|
180
|
-
if len(list_str)
|
181
|
-
|
257
|
+
if len(list_str) == 1 and chat.get_ntok(list_str[0]) <= max_new_accum:
|
258
|
+
chat_summary = list_str[0]
|
259
|
+
else:
|
260
|
+
k_base = os.path.basename(k)
|
261
|
+
if v_stamp == cache.get(k, {}).get('timestamp'):
|
262
|
+
dict_sum[k] = cache[k]
|
263
|
+
continue
|
264
|
+
max_new_sum = int(max_len/len(list_str))
|
182
265
|
volat = f'**{k}**:\n'
|
183
266
|
accum = ''
|
184
|
-
for s in list_str:
|
267
|
+
for i, s in enumerate(list_str):
|
185
268
|
chat.reset()
|
186
|
-
|
269
|
+
toout(f'\n\nIngesting {k_base} {i+1}/{len(list_str)}...\n\n', mode='a')
|
270
|
+
tolog(f'{s=}') # DEBUG
|
271
|
+
newsum = chat(format_ingest.format(volat=volat, incoming=s.rstrip()), max_new=max_new_sum, verbose=False, stream=OUT_PATH)['text'].rstrip()
|
272
|
+
tolog(f'{k} {i+1}/{len(list_str)}: {newsum=}') # DEBUG
|
187
273
|
accum += newsum + ' ...\n'
|
188
274
|
volat = format_volat.format(k=k, newsum=newsum)
|
189
|
-
|
190
|
-
accum
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
275
|
+
toout(f'\n\nIngesting {k_base}...\n\n', mode='a')
|
276
|
+
tolog(f'{accum=}') # DEBUG
|
277
|
+
if chat.get_ntok(accum) <= max_new_accum:
|
278
|
+
chat_summary = accum.strip()
|
279
|
+
else:
|
280
|
+
chat.reset()
|
281
|
+
chat_summary = chat(format_ingest.format(volat=f'**{k}**:\n', incoming=accum), max_new=max_new_accum, verbose=False, stream=OUT_PATH)['text'].strip()
|
282
|
+
dict_sum[k] = dict(timestamp=v_stamp, summary=chat_summary, ntok=chat.get_ntok(chat_summary))
|
195
283
|
dump_cache(dict_sum)
|
196
|
-
|
197
|
-
|
198
|
-
result += f'--- Summary of **{k}** ---\n{v["summary"]}\n\n'
|
284
|
+
for k, v in dict_sum.items():
|
285
|
+
result += f'--- **{os.path.basename(k)}** ---\n{v["summary"].strip()}\n\n'
|
199
286
|
result += '---\n\n'
|
287
|
+
toout(result, 'ingest')
|
200
288
|
return result
|
201
289
|
|
202
|
-
def process_command(data
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
return
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
290
|
+
def process_command(data):
|
291
|
+
if len(data['user']) == 0:
|
292
|
+
response = chat.resume(max_new=NUM_TOKEN, verbose=False, stream=OUT_PATH)
|
293
|
+
toout(response['text'], mode='a')
|
294
|
+
tolog(response)
|
295
|
+
data['user_prompt'] = ''
|
296
|
+
return data
|
297
|
+
if SEP_CMD in data['user']:
|
298
|
+
data['user_prompt'], *cmds = (x.strip() for x in data['user'].split(SEP_CMD))
|
299
|
+
else:
|
300
|
+
data['user_prompt'] = data['user'].strip()
|
301
|
+
cmds = []
|
302
|
+
tolog(f'process_command i {cmds=} {data=}')
|
303
|
+
|
304
|
+
do_reset = False if 'followup' in data else DO_RESET
|
305
|
+
for cmd in cmds:
|
306
|
+
if cmd.startswith('continue'):
|
307
|
+
arg = cmd.removeprefix('continue').strip('(').strip(')').strip().strip('"').strip("'").strip()
|
308
|
+
data['max_new'] = NUM_TOKEN if len(arg) == 0 else int(arg)
|
309
|
+
response = chat.resume(max_new=data['max_new'], verbose=False, stream=OUT_PATH)
|
310
|
+
toout(response['text'], mode='a')
|
311
|
+
tolog(response)
|
312
|
+
do_reset = False
|
313
|
+
break
|
314
|
+
|
315
|
+
if cmd.startswith('reset'):
|
316
|
+
do_reset = True
|
317
|
+
break
|
318
|
+
if cmd.startswith('followup'):
|
319
|
+
do_reset = False
|
320
|
+
break
|
321
|
+
if do_reset:
|
322
|
+
chat.reset()
|
323
|
+
|
324
|
+
full_path = data['tree']
|
325
|
+
data['dir'] = os.path.dirname(full_path)
|
326
|
+
data['file'] = os.path.basename(full_path)
|
327
|
+
data['ext'] = os.path.splitext(full_path)[1][1:]
|
328
|
+
if chat.stop:
|
329
|
+
data['file'] = ''
|
330
|
+
data['context'] = ''
|
331
|
+
if data['tree'] == OUT_PATH:
|
332
|
+
data['dir'] = os.getcwd()
|
333
|
+
data['file'] = ''
|
334
|
+
data['context'] = ''
|
335
|
+
data['ext'] = ''
|
336
|
+
if data['file'] == '.tmp':
|
337
|
+
data['file'] = ''
|
338
|
+
data['ext'] = ''
|
339
|
+
|
340
|
+
if len(cmds) == 1 and len(cmds[0]) == 0:
|
341
|
+
data['include'] = ingest(data['dir'])
|
342
|
+
return data
|
343
|
+
|
344
|
+
data['include'] = ''
|
345
|
+
for cmd in cmds:
|
346
|
+
if cmd.startswith('include'):
|
347
|
+
arg = cmd.removeprefix('include').strip().strip('(').strip(')').strip().strip('"').strip("'").strip()
|
348
|
+
src = data['dir'] if len(arg) == 0 else arg
|
349
|
+
if arg == '%':
|
350
|
+
continue
|
351
|
+
if src.startswith('`') or src.startswith('$('):
|
352
|
+
shell_cmd = src.strip('`') if src.startswith('`') else src.strip('$()')
|
353
|
+
shell_cmd = shell_cmd.strip()
|
354
|
+
try:
|
355
|
+
result = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True)
|
356
|
+
if result.returncode == 0:
|
357
|
+
data['include'] += f'--- **{shell_cmd}** ---\n```\n{result.stdout.strip()}\n```\n---\n\n'
|
358
|
+
else:
|
359
|
+
tolog(f'{shell_cmd} failed {result.stderr.strip()}')
|
360
|
+
except Exception as e:
|
361
|
+
tolog(f'Error executing {shell_cmd}: {e}')
|
362
|
+
else:
|
363
|
+
data['include'] += ingest(src)
|
364
|
+
|
365
|
+
for cmd in cmds:
|
366
|
+
if cmd.startswith('deploy'):
|
367
|
+
arg = cmd.removeprefix('deploy').strip().strip('(').strip(')').strip().strip('"').strip("'").strip()
|
368
|
+
if len(data['user_prompt']) == 0:
|
369
|
+
deploy(dest=arg)
|
370
|
+
data['user_prompt'] = ''
|
371
|
+
return data
|
372
|
+
data['user_prompt'] += "\n\nEnsure that each code block is preceded by a filename in **filename.ext** format. The filename should only contain alphanumeric characters, dots, underscores, or hyphens. Ensure that any extraneous characters are removed from the filenames."
|
373
|
+
data['deploy_dest'] = arg
|
374
|
+
return data
|
375
|
+
|
213
376
|
async def monitor_directory():
|
214
377
|
async for changes in awatch(WATCH_DIR):
|
215
378
|
found_files = {os.path.basename(f) for _, f in changes}
|
@@ -225,48 +388,63 @@ async def monitor_directory():
|
|
225
388
|
if 'followup' in os.listdir(WATCH_DIR):
|
226
389
|
os.remove(os.path.join(WATCH_DIR, 'followup'))
|
227
390
|
data['followup'] = True
|
228
|
-
|
391
|
+
if 'quit' in os.listdir(WATCH_DIR):
|
392
|
+
os.remove(os.path.join(WATCH_DIR, 'quit'))
|
393
|
+
data['quit'] = True
|
229
394
|
await process_files(data)
|
230
395
|
|
231
396
|
async def process_files(data):
|
232
|
-
tolog(f'{data=}')
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
str_template
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
str_template += "```{ext}\n{yank}\n```\n\n"
|
397
|
+
tolog(f'process_files i {data=}')
|
398
|
+
str_template = '{include}'
|
399
|
+
data = process_command(data)
|
400
|
+
if len(data['user_prompt']) == 0:
|
401
|
+
return
|
402
|
+
if len(data['file']) > 0:
|
403
|
+
str_template += '**{file}**\n'
|
404
|
+
if len(data['context']) > 0 and data['yank'] != data['context']:
|
405
|
+
str_template += '```{ext}\n{context}\n```\n\n'
|
406
|
+
if len(data['yank']) > 0:
|
407
|
+
if '\n' in data['yank']:
|
408
|
+
str_template += "```{ext}\n{yank}\n```\n\n"
|
409
|
+
else:
|
410
|
+
if data['user'] == 0:
|
411
|
+
str_template += "{yank}"
|
248
412
|
else:
|
249
413
|
str_template += "`{yank}` "
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
str_template = process_command(data, str_template)
|
254
|
-
else:
|
255
|
-
data['user_prompt'] = data['user']
|
256
|
-
chat.reset()
|
257
|
-
tolog(f'{str_template=}') # DEBUG
|
414
|
+
str_template += '{user_prompt}'
|
415
|
+
tolog(f'process_files o {data=}') # DEBUG
|
416
|
+
tolog(f'process_files {str_template=}') # DEBUG
|
258
417
|
prompt = str_template.format(**data)
|
259
418
|
tolog(prompt, 'tollm')
|
260
419
|
toout('')
|
261
|
-
|
262
|
-
|
420
|
+
max_new = data['max_new'] if 'max_new' in data else max(10, NUM_TOKEN - chat.get_ntok(prompt))
|
421
|
+
response = chat(prompt, max_new=max_new, verbose=False, stream=OUT_PATH)
|
422
|
+
if SHOW_USER:
|
423
|
+
toout(response['text'])
|
424
|
+
else:
|
425
|
+
toout(response['text'])
|
263
426
|
tolog(response['benchmark'])
|
427
|
+
if 'deploy_dest' in data:
|
428
|
+
deploy(dest=data['deploy_dest'], reformat=False)
|
264
429
|
|
430
|
+
KEYL = KEY_MAP.get('l', 'l')
|
431
|
+
KEYJ = KEY_MAP.get('j', 'j')
|
432
|
+
KEYP = KEY_MAP.get('p', 'p')
|
433
|
+
mapl, mapj, mapp = (f'<Leader>{KEYL}', f'<Leader>{KEYJ}', f'<Leader>{KEYP}') if USE_LEADER else (f'<C-{KEYL}>', f'<C-{KEYJ}>', f'<C-{KEYP}>')
|
265
434
|
VIMLMSCRIPT = Template(r"""
|
435
|
+
let s:register_names = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u']
|
266
436
|
let s:watched_dir = expand('$WATCH_DIR')
|
267
437
|
|
268
438
|
function! Monitor()
|
269
|
-
|
439
|
+
if exists('s:monitor_timer')
|
440
|
+
call timer_stop(s:monitor_timer)
|
441
|
+
unlet s:monitor_timer
|
442
|
+
endif
|
443
|
+
let response_path = s:watched_dir . '/response.md'
|
444
|
+
let bufnum = bufnr(response_path)
|
445
|
+
if bufnum != -1
|
446
|
+
execute 'bwipeout ' . bufnum
|
447
|
+
endif
|
270
448
|
let response_path = s:watched_dir . '/response.md'
|
271
449
|
rightbelow vsplit | execute 'view ' . response_path
|
272
450
|
setlocal autoread
|
@@ -288,41 +466,17 @@ function! CheckForUpdates(timer)
|
|
288
466
|
endfunction
|
289
467
|
|
290
468
|
function! s:CustomInput(prompt) abort
|
291
|
-
|
292
|
-
let
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
let c = getchar()
|
299
|
-
if type(c) == v:t_number
|
300
|
-
if c == 13 " Enter
|
301
|
-
break
|
302
|
-
elseif c == 27 " Esc
|
303
|
-
let aborted = v:true
|
304
|
-
break
|
305
|
-
elseif c == 8 || c == 127 " Backspace
|
306
|
-
if len(input) > 0
|
307
|
-
let input = input[:-2]
|
308
|
-
echon "\r" . clean_prompt . input . ' '
|
309
|
-
execute "normal! \<BS>"
|
310
|
-
endif
|
311
|
-
else
|
312
|
-
let char = nr2char(c)
|
313
|
-
let input .= char
|
314
|
-
echon char
|
315
|
-
endif
|
316
|
-
endif
|
317
|
-
endwhile
|
318
|
-
let input_length = aborted ? 0 : len(input)
|
319
|
-
let clear_length = len(clean_prompt) + input_length
|
320
|
-
echon "\r" . repeat(' ', clear_length) . "\r"
|
321
|
-
return aborted ? v:null : input
|
469
|
+
call inputsave()
|
470
|
+
let input = input(a:prompt)
|
471
|
+
call inputrestore()
|
472
|
+
if empty(input)
|
473
|
+
return v:null
|
474
|
+
endif
|
475
|
+
return input
|
322
476
|
endfunction
|
323
477
|
|
324
|
-
function! SaveUserInput()
|
325
|
-
let user_input = s:CustomInput(
|
478
|
+
function! SaveUserInput(prompt)
|
479
|
+
let user_input = s:CustomInput(a:prompt)
|
326
480
|
if user_input is v:null
|
327
481
|
echo "Input aborted"
|
328
482
|
return
|
@@ -334,12 +488,107 @@ function! SaveUserInput()
|
|
334
488
|
call writefile([current_file], tree_file, 'w')
|
335
489
|
endfunction
|
336
490
|
|
337
|
-
function!
|
338
|
-
|
339
|
-
|
340
|
-
|
491
|
+
function! VisualPrompt()
|
492
|
+
silent! execute "normal! \<ESC>"
|
493
|
+
silent execute "'<,'>w! " . s:watched_dir . "/yank"
|
494
|
+
silent execute "w! " . s:watched_dir . "/context"
|
495
|
+
" silent! execute "normal! `<V`>"
|
496
|
+
" silent! execute "normal! \<ESC>"
|
497
|
+
call SaveUserInput('VimLM: ')
|
498
|
+
endfunction
|
499
|
+
|
500
|
+
function! NormalPrompt()
|
501
|
+
silent! execute "normal! V\<ESC>"
|
502
|
+
silent execute "'<,'>w! " . s:watched_dir . "/yank"
|
503
|
+
silent execute "w! " . s:watched_dir . "/context"
|
504
|
+
" silent! execute "normal! \<ESC>"
|
505
|
+
call SaveUserInput('VimLM: ')
|
506
|
+
endfunction
|
507
|
+
|
508
|
+
function! FollowUpPrompt()
|
509
|
+
call writefile([], s:watched_dir . '/yank', 'w')
|
510
|
+
call writefile([], s:watched_dir . '/context', 'w')
|
511
|
+
call writefile([], s:watched_dir . '/followup', 'w')
|
512
|
+
call SaveUserInput('... ')
|
513
|
+
endfunction
|
514
|
+
|
515
|
+
function! ExtractAllCodeBlocks()
|
516
|
+
let filepath = s:watched_dir . '/response.md'
|
517
|
+
if !filereadable(filepath)
|
518
|
+
echoerr "File not found: " . filepath
|
519
|
+
return
|
520
|
+
endif
|
521
|
+
let lines = readfile(filepath)
|
522
|
+
let in_code_block = 0
|
523
|
+
let code_blocks = []
|
524
|
+
let current_block = []
|
525
|
+
for line in lines
|
526
|
+
if line =~ '^```'
|
527
|
+
if in_code_block
|
528
|
+
call add(code_blocks, current_block)
|
529
|
+
let current_block = []
|
530
|
+
let in_code_block = 0
|
531
|
+
else
|
532
|
+
let in_code_block = 1
|
533
|
+
endif
|
534
|
+
elseif in_code_block
|
535
|
+
call add(current_block, line)
|
536
|
+
endif
|
537
|
+
endfor
|
538
|
+
if in_code_block
|
539
|
+
call add(code_blocks, current_block)
|
540
|
+
endif
|
541
|
+
for idx in range(len(code_blocks))
|
542
|
+
if idx >= len(s:register_names)
|
543
|
+
break
|
544
|
+
endif
|
545
|
+
let code_block_text = join(code_blocks[idx], "\n")
|
546
|
+
let register_name = s:register_names[idx]
|
547
|
+
call setreg(register_name, code_block_text, 'v')
|
548
|
+
endfor
|
549
|
+
return len(code_blocks)
|
550
|
+
endfunction
|
551
|
+
|
552
|
+
function! PasteIntoLastVisualSelection(...)
|
553
|
+
let num_blocks = ExtractAllCodeBlocks()
|
554
|
+
if a:0 > 0
|
555
|
+
let register_name = a:1
|
556
|
+
else
|
557
|
+
echo "Extracted " . num_blocks . " blocks into registers @a-@" . s:register_names[num_blocks - 1] . ". Enter register name: "
|
558
|
+
let register_name = nr2char(getchar())
|
559
|
+
endif
|
560
|
+
|
561
|
+
if register_name !~ '^[a-z]$'
|
562
|
+
echoerr "Invalid register name. Please enter a single lowercase letter (e.g., a, b, c)."
|
563
|
+
return
|
564
|
+
endif
|
565
|
+
|
566
|
+
let register_content = getreg(register_name)
|
567
|
+
if register_content == ''
|
568
|
+
echoerr "Register @" . register_name . " is empty."
|
569
|
+
return
|
570
|
+
endif
|
571
|
+
|
572
|
+
let current_mode = mode()
|
573
|
+
if current_mode == 'v' || current_mode == 'V' || current_mode == ''
|
574
|
+
execute 'normal! "' . register_name . 'p'
|
575
|
+
else
|
576
|
+
normal! gv
|
577
|
+
execute 'normal! "' . register_name . 'p'
|
578
|
+
endif
|
579
|
+
endfunction
|
580
|
+
|
581
|
+
function! VimLM(...) range
|
582
|
+
let user_input = join(a:000, ' ')
|
583
|
+
if empty(user_input)
|
584
|
+
echo "Usage: :VimLM <prompt> [!command1] [!command2] ..."
|
341
585
|
return
|
342
586
|
endif
|
587
|
+
if line("'<") == line("'>")
|
588
|
+
silent! execute "normal! V\<ESC>"
|
589
|
+
endif
|
590
|
+
silent execute "'<,'>w! " . s:watched_dir . "/yank"
|
591
|
+
silent execute "w! " . s:watched_dir . "/context"
|
343
592
|
let user_file = s:watched_dir . '/user'
|
344
593
|
call writefile([user_input], user_file, 'w')
|
345
594
|
let current_file = expand('%:p')
|
@@ -347,24 +596,23 @@ function! SaveUserInput()
|
|
347
596
|
call writefile([current_file], tree_file, 'w')
|
348
597
|
endfunction
|
349
598
|
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
|
355
|
-
|
356
|
-
|
357
|
-
vnoremap <C-l> :w! $WATCH_DIR/yank<CR>:w! $WATCH_DIR/context<CR>:call SaveUserInput()<CR>
|
358
|
-
nnoremap <C-l> V:w! $WATCH_DIR/yank<CR>:w! $WATCH_DIR/context<CR>:call SaveUserInput()<CR>
|
359
|
-
nnoremap <C-r> :call FollowUpInput()<CR>
|
360
|
-
|
599
|
+
command! -range -nargs=+ VimLM call VimLM(<f-args>)
|
600
|
+
nnoremap $mapp :call PasteIntoLastVisualSelection()<CR>
|
601
|
+
vnoremap $mapp <Cmd>:call PasteIntoLastVisualSelection()<CR>
|
602
|
+
vnoremap $mapl <Cmd>:call VisualPrompt()<CR>
|
603
|
+
nnoremap $mapl :call NormalPrompt()<CR>
|
604
|
+
nnoremap $mapj :call FollowUpPrompt()<CR>
|
361
605
|
call Monitor()
|
362
|
-
""").safe_substitute(dict(WATCH_DIR=WATCH_DIR))
|
606
|
+
""").safe_substitute(dict(WATCH_DIR=WATCH_DIR, mapl=mapl, mapj=mapj, mapp=mapp))
|
363
607
|
|
364
608
|
async def main():
|
365
609
|
parser = argparse.ArgumentParser(description="VimLM - LLM-powered Vim assistant")
|
610
|
+
parser.add_argument('--test', action='store_true')
|
366
611
|
parser.add_argument("vim_args", nargs=argparse.REMAINDER, help="Vim arguments")
|
367
612
|
args = parser.parse_args()
|
613
|
+
if args.test:
|
614
|
+
test()
|
615
|
+
return
|
368
616
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.vim', delete=False) as f:
|
369
617
|
f.write(VIMLMSCRIPT)
|
370
618
|
vim_script = f.name
|
@@ -373,14 +621,17 @@ async def main():
|
|
373
621
|
vim_command.extend(args.vim_args)
|
374
622
|
else:
|
375
623
|
vim_command.append('.tmp')
|
376
|
-
monitor_task = asyncio.create_task(monitor_directory())
|
377
|
-
vim_process = await asyncio.create_subprocess_exec(*vim_command)
|
378
|
-
await vim_process.wait()
|
379
|
-
monitor_task.cancel()
|
380
624
|
try:
|
381
|
-
|
382
|
-
|
383
|
-
|
625
|
+
monitor_task = asyncio.create_task(monitor_directory())
|
626
|
+
vim_process = await asyncio.create_subprocess_exec(*vim_command)
|
627
|
+
await vim_process.wait()
|
628
|
+
finally:
|
629
|
+
monitor_task.cancel()
|
630
|
+
try:
|
631
|
+
await monitor_task
|
632
|
+
except asyncio.CancelledError:
|
633
|
+
pass
|
634
|
+
os.remove(vim_script)
|
384
635
|
|
385
636
|
def run():
|
386
637
|
asyncio.run(main())
|
vimlm-0.0.5.dist-info/METADATA
DELETED
@@ -1,108 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.2
|
2
|
-
Name: vimlm
|
3
|
-
Version: 0.0.5
|
4
|
-
Summary: VimLM - LLM-powered Vim assistant
|
5
|
-
Home-page: https://github.com/JosefAlbers/vimlm
|
6
|
-
Author: Josef Albers
|
7
|
-
Author-email: albersj66@gmail.com
|
8
|
-
Requires-Python: >=3.12.8
|
9
|
-
Description-Content-Type: text/markdown
|
10
|
-
License-File: LICENSE
|
11
|
-
Requires-Dist: nanollama==0.0.5b0
|
12
|
-
Requires-Dist: mlx_lm_utils==0.0.1a0
|
13
|
-
Requires-Dist: watchfiles==1.0.4
|
14
|
-
Dynamic: author
|
15
|
-
Dynamic: author-email
|
16
|
-
Dynamic: description
|
17
|
-
Dynamic: description-content-type
|
18
|
-
Dynamic: home-page
|
19
|
-
Dynamic: requires-dist
|
20
|
-
Dynamic: requires-python
|
21
|
-
Dynamic: summary
|
22
|
-
|
23
|
-
# VimLM - Local LLM-Powered Coding Assistant for Vim
|
24
|
-
|
25
|
-

|
26
|
-
|
27
|
-
An LLM-powered coding companion for Vim, inspired by GitHub Copilot/Cursor. Integrates contextual code understanding, summarization, and AI assistance directly into your Vim workflow.
|
28
|
-
|
29
|
-
## Features
|
30
|
-
|
31
|
-
- **Model Agnostic** - Use any MLX-compatible model via config file
|
32
|
-
- **Vim-Native UX** - Ctrl-l/Ctrl-r keybindings and split-window responses
|
33
|
-
- **Deep Context** - Understands code context from:
|
34
|
-
- Current file
|
35
|
-
- Visual selections
|
36
|
-
- Referenced files (`!@#$` syntax)
|
37
|
-
- Project directory structure
|
38
|
-
- **Conversational Coding** - Iterative refinement with follow-up queries
|
39
|
-
- **Air-Gapped Security** - 100% offline - no APIs, no tracking, no data leaks
|
40
|
-
|
41
|
-
## Requirements
|
42
|
-
|
43
|
-
- Apple M-series chip (M1/M2/M3/M4)
|
44
|
-
- Python 3.12.8
|
45
|
-
|
46
|
-
## Installation
|
47
|
-
|
48
|
-
```zsh
|
49
|
-
pip install vimlm
|
50
|
-
```
|
51
|
-
|
52
|
-
## Quick Start
|
53
|
-
|
54
|
-
1. Launch with default model (DeepSeek-R1-Distill-Qwen-7B-4bit):
|
55
|
-
|
56
|
-
```zsh
|
57
|
-
vimlm your_file.js
|
58
|
-
```
|
59
|
-
|
60
|
-
2. **From Normal Mode**:
|
61
|
-
- `Ctrl-l`: Send current line + file context
|
62
|
-
- Example prompt: "Regex for removing html tags in item.content"
|
63
|
-
|
64
|
-
3. **From Visual Mode**:
|
65
|
-
- Select code → `Ctrl-l`: Send selection + file context
|
66
|
-
- Example prompt: "Convert this to async/await syntax"
|
67
|
-
|
68
|
-
4. **Add Context**: Use `!@#$` to include additional files/folders:
|
69
|
-
- `!@#$` (no path): Current folder
|
70
|
-
- `!@#$ ~/scrap/jph00/hypermedia-applications.summ.md`: Specific folder
|
71
|
-
- `!@#$ ~/wtm/utils.py`: Specific file
|
72
|
-
- Example prompt: "AJAX-ify this app !@#$ ~/scrap/jph00/hypermedia-applications.summ.md"
|
73
|
-
|
74
|
-
5. **Follow-Up**: After initial response:
|
75
|
-
- `Ctrl-r`: Continue thread
|
76
|
-
- Example follow-up: "In Manifest V3"
|
77
|
-
|
78
|
-
## Advanced Configuration
|
79
|
-
|
80
|
-
### Custom Model Setup
|
81
|
-
|
82
|
-
1. **Browse models**: [MLX Community Models on Hugging Face](https://huggingface.co/mlx-community)
|
83
|
-
|
84
|
-
2. **Edit config file**:
|
85
|
-
|
86
|
-
```json
|
87
|
-
{
|
88
|
-
"LLM_MODEL": "/path/to/your/mlx_model"
|
89
|
-
}
|
90
|
-
```
|
91
|
-
|
92
|
-
3. **Save to**:
|
93
|
-
|
94
|
-
```
|
95
|
-
~/vimlm/cfg.json
|
96
|
-
```
|
97
|
-
|
98
|
-
4. **Restart VimLM**
|
99
|
-
|
100
|
-
## Key Bindings
|
101
|
-
|
102
|
-
| Binding | Mode | Action |
|
103
|
-
|------------|---------------|----------------------------------------|
|
104
|
-
| `Ctrl-l` | Normal/Visual | Send current file + selection to LLM |
|
105
|
-
| `Ctrl-r` | Normal | Continue conversation |
|
106
|
-
| `Esc` | Prompt | Cancel input |
|
107
|
-
|
108
|
-
|
vimlm-0.0.5.dist-info/RECORD
DELETED
@@ -1,7 +0,0 @@
|
|
1
|
-
vimlm.py,sha256=6bXIkDHF0fLTT_NRCbDLcP5CDTu5e3uacuMdzyGUrhI,13643
|
2
|
-
vimlm-0.0.5.dist-info/LICENSE,sha256=f1xgK8fAXg_intwnbc9nLkHf7ODPLtgpHs7DetQHOro,11343
|
3
|
-
vimlm-0.0.5.dist-info/METADATA,sha256=d7xZr9bBIJlYuPVNnFpa8w3-1XSFVMiWwikQVt5sLBw,3009
|
4
|
-
vimlm-0.0.5.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
|
5
|
-
vimlm-0.0.5.dist-info/entry_points.txt,sha256=mU5V4MYsuIzCc6YB-Ro-6USSHWN5vHw8UDnTEoq0isw,36
|
6
|
-
vimlm-0.0.5.dist-info/top_level.txt,sha256=I8GjqoiP--scYsO3AfLhha-6Ax9ci3IvbWvVbPv8g94,6
|
7
|
-
vimlm-0.0.5.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|