vimlm 0.0.9__tar.gz → 0.1.1__tar.gz
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.9 → vimlm-0.1.1}/PKG-INFO +48 -10
- {vimlm-0.0.9 → vimlm-0.1.1}/README.md +45 -7
- {vimlm-0.0.9 → vimlm-0.1.1}/setup.py +1 -1
- {vimlm-0.0.9 → vimlm-0.1.1}/vimlm.egg-info/PKG-INFO +48 -10
- vimlm-0.1.1/vimlm.egg-info/requires.txt +3 -0
- {vimlm-0.0.9 → vimlm-0.1.1}/vimlm.py +288 -66
- vimlm-0.0.9/vimlm.egg-info/requires.txt +0 -3
- {vimlm-0.0.9 → vimlm-0.1.1}/LICENSE +0 -0
- {vimlm-0.0.9 → vimlm-0.1.1}/setup.cfg +0 -0
- {vimlm-0.0.9 → vimlm-0.1.1}/vimlm.egg-info/SOURCES.txt +0 -0
- {vimlm-0.0.9 → vimlm-0.1.1}/vimlm.egg-info/dependency_links.txt +0 -0
- {vimlm-0.0.9 → vimlm-0.1.1}/vimlm.egg-info/entry_points.txt +0 -0
- {vimlm-0.0.9 → vimlm-0.1.1}/vimlm.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: vimlm
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.1.1
|
4
4
|
Summary: VimLM - LLM-powered Vim assistant
|
5
5
|
Home-page: https://github.com/JosefAlbers/vimlm
|
6
6
|
Author: Josef Albers
|
@@ -8,8 +8,8 @@ Author-email: albersj66@gmail.com
|
|
8
8
|
Requires-Python: >=3.12.8
|
9
9
|
Description-Content-Type: text/markdown
|
10
10
|
License-File: LICENSE
|
11
|
-
Requires-Dist: nanollama>=0.0.
|
12
|
-
Requires-Dist: mlx_lm_utils>=0.0.
|
11
|
+
Requires-Dist: nanollama>=0.0.6
|
12
|
+
Requires-Dist: mlx_lm_utils>=0.0.4
|
13
13
|
Requires-Dist: watchfiles==1.0.4
|
14
14
|
Dynamic: author
|
15
15
|
Dynamic: author-email
|
@@ -20,7 +20,6 @@ Dynamic: requires-dist
|
|
20
20
|
Dynamic: requires-python
|
21
21
|
Dynamic: summary
|
22
22
|
|
23
|
-
|
24
23
|
# VimLM - AI-Powered Coding Assistant for Vim
|
25
24
|
|
26
25
|

|
@@ -48,7 +47,46 @@ pip install vimlm
|
|
48
47
|
vimlm
|
49
48
|
```
|
50
49
|
|
51
|
-
##
|
50
|
+
## Smart Autocomplete
|
51
|
+
|
52
|
+
### **Basic Usage**
|
53
|
+
|
54
|
+
| Key Binding | Mode | Action |
|
55
|
+
|-------------|---------|-----------------------------------------|
|
56
|
+
| `Ctrl-l` | Insert | Generate code suggestion |
|
57
|
+
| `Ctrl-p` | Insert | Insert generated code |
|
58
|
+
| `Ctrl-j` | Insert | Generate and insert code |
|
59
|
+
|
60
|
+
*Example Workflow*:
|
61
|
+
1. Place cursor where you need code
|
62
|
+
```python
|
63
|
+
def quicksort(arr):
|
64
|
+
if len(arr) <= 1:
|
65
|
+
return arr
|
66
|
+
pivot = arr[len(arr) // 2]
|
67
|
+
# <Cursor here>
|
68
|
+
middle = [x for x in arr if x == pivot]
|
69
|
+
right = [x for x in arr if x > pivot]
|
70
|
+
return quicksort(left) + middle + quicksort(right)
|
71
|
+
```
|
72
|
+
|
73
|
+
2. Use `Ctrl-j` to generate and insert the code (or `Ctrl-l` to trigger → `Ctrl-p` to insert)
|
74
|
+
|
75
|
+
### **Repository-Level Code Completion**
|
76
|
+
|
77
|
+
| Option | Description |
|
78
|
+
|------------|------------------------------------------|
|
79
|
+
| `--repo` | Paths to include as repository context |
|
80
|
+
|
81
|
+
The `--repo` option enhances autocomplete by providing repository-level context to the LLM.
|
82
|
+
|
83
|
+
*Example Workflow*:
|
84
|
+
1. Launch VimLM with repo context: `vimlm main.py --repo utils/*`
|
85
|
+
2. In Insert mode, place cursor where completion is needed
|
86
|
+
3. `Ctrl-l` to generate suggestions informed by repository context
|
87
|
+
4. `Ctrl-p` to accept and insert the code
|
88
|
+
|
89
|
+
## Conversational Assistance
|
52
90
|
|
53
91
|
| Key Binding | Mode | Action |
|
54
92
|
|-------------|---------------|----------------------------------------|
|
@@ -86,12 +124,12 @@ vimlm
|
|
86
124
|
|
87
125
|
`!` prefix to embed inline directives in prompts:
|
88
126
|
|
89
|
-
| Directive | Description
|
90
|
-
|
127
|
+
| Directive | Description |
|
128
|
+
|------------------|--------------------------------------------|
|
91
129
|
| `!include PATH` | Add file/directory/shell output to context |
|
92
|
-
| `!deploy DEST` | Save code blocks to directory
|
93
|
-
| `!continue N` | Continue stopped response
|
94
|
-
| `!followup` | Continue conversation
|
130
|
+
| `!deploy DEST` | Save code blocks to directory |
|
131
|
+
| `!continue N` | Continue stopped response |
|
132
|
+
| `!followup` | Continue conversation |
|
95
133
|
|
96
134
|
### 1. **Context Layering**
|
97
135
|
```text
|
@@ -1,4 +1,3 @@
|
|
1
|
-
|
2
1
|
# VimLM - AI-Powered Coding Assistant for Vim
|
3
2
|
|
4
3
|

|
@@ -26,7 +25,46 @@ pip install vimlm
|
|
26
25
|
vimlm
|
27
26
|
```
|
28
27
|
|
29
|
-
##
|
28
|
+
## Smart Autocomplete
|
29
|
+
|
30
|
+
### **Basic Usage**
|
31
|
+
|
32
|
+
| Key Binding | Mode | Action |
|
33
|
+
|-------------|---------|-----------------------------------------|
|
34
|
+
| `Ctrl-l` | Insert | Generate code suggestion |
|
35
|
+
| `Ctrl-p` | Insert | Insert generated code |
|
36
|
+
| `Ctrl-j` | Insert | Generate and insert code |
|
37
|
+
|
38
|
+
*Example Workflow*:
|
39
|
+
1. Place cursor where you need code
|
40
|
+
```python
|
41
|
+
def quicksort(arr):
|
42
|
+
if len(arr) <= 1:
|
43
|
+
return arr
|
44
|
+
pivot = arr[len(arr) // 2]
|
45
|
+
# <Cursor here>
|
46
|
+
middle = [x for x in arr if x == pivot]
|
47
|
+
right = [x for x in arr if x > pivot]
|
48
|
+
return quicksort(left) + middle + quicksort(right)
|
49
|
+
```
|
50
|
+
|
51
|
+
2. Use `Ctrl-j` to generate and insert the code (or `Ctrl-l` to trigger → `Ctrl-p` to insert)
|
52
|
+
|
53
|
+
### **Repository-Level Code Completion**
|
54
|
+
|
55
|
+
| Option | Description |
|
56
|
+
|------------|------------------------------------------|
|
57
|
+
| `--repo` | Paths to include as repository context |
|
58
|
+
|
59
|
+
The `--repo` option enhances autocomplete by providing repository-level context to the LLM.
|
60
|
+
|
61
|
+
*Example Workflow*:
|
62
|
+
1. Launch VimLM with repo context: `vimlm main.py --repo utils/*`
|
63
|
+
2. In Insert mode, place cursor where completion is needed
|
64
|
+
3. `Ctrl-l` to generate suggestions informed by repository context
|
65
|
+
4. `Ctrl-p` to accept and insert the code
|
66
|
+
|
67
|
+
## Conversational Assistance
|
30
68
|
|
31
69
|
| Key Binding | Mode | Action |
|
32
70
|
|-------------|---------------|----------------------------------------|
|
@@ -64,12 +102,12 @@ vimlm
|
|
64
102
|
|
65
103
|
`!` prefix to embed inline directives in prompts:
|
66
104
|
|
67
|
-
| Directive | Description
|
68
|
-
|
105
|
+
| Directive | Description |
|
106
|
+
|------------------|--------------------------------------------|
|
69
107
|
| `!include PATH` | Add file/directory/shell output to context |
|
70
|
-
| `!deploy DEST` | Save code blocks to directory
|
71
|
-
| `!continue N` | Continue stopped response
|
72
|
-
| `!followup` | Continue conversation
|
108
|
+
| `!deploy DEST` | Save code blocks to directory |
|
109
|
+
| `!continue N` | Continue stopped response |
|
110
|
+
| `!followup` | Continue conversation |
|
73
111
|
|
74
112
|
### 1. **Context Layering**
|
75
113
|
```text
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.2
|
2
2
|
Name: vimlm
|
3
|
-
Version: 0.
|
3
|
+
Version: 0.1.1
|
4
4
|
Summary: VimLM - LLM-powered Vim assistant
|
5
5
|
Home-page: https://github.com/JosefAlbers/vimlm
|
6
6
|
Author: Josef Albers
|
@@ -8,8 +8,8 @@ Author-email: albersj66@gmail.com
|
|
8
8
|
Requires-Python: >=3.12.8
|
9
9
|
Description-Content-Type: text/markdown
|
10
10
|
License-File: LICENSE
|
11
|
-
Requires-Dist: nanollama>=0.0.
|
12
|
-
Requires-Dist: mlx_lm_utils>=0.0.
|
11
|
+
Requires-Dist: nanollama>=0.0.6
|
12
|
+
Requires-Dist: mlx_lm_utils>=0.0.4
|
13
13
|
Requires-Dist: watchfiles==1.0.4
|
14
14
|
Dynamic: author
|
15
15
|
Dynamic: author-email
|
@@ -20,7 +20,6 @@ Dynamic: requires-dist
|
|
20
20
|
Dynamic: requires-python
|
21
21
|
Dynamic: summary
|
22
22
|
|
23
|
-
|
24
23
|
# VimLM - AI-Powered Coding Assistant for Vim
|
25
24
|
|
26
25
|

|
@@ -48,7 +47,46 @@ pip install vimlm
|
|
48
47
|
vimlm
|
49
48
|
```
|
50
49
|
|
51
|
-
##
|
50
|
+
## Smart Autocomplete
|
51
|
+
|
52
|
+
### **Basic Usage**
|
53
|
+
|
54
|
+
| Key Binding | Mode | Action |
|
55
|
+
|-------------|---------|-----------------------------------------|
|
56
|
+
| `Ctrl-l` | Insert | Generate code suggestion |
|
57
|
+
| `Ctrl-p` | Insert | Insert generated code |
|
58
|
+
| `Ctrl-j` | Insert | Generate and insert code |
|
59
|
+
|
60
|
+
*Example Workflow*:
|
61
|
+
1. Place cursor where you need code
|
62
|
+
```python
|
63
|
+
def quicksort(arr):
|
64
|
+
if len(arr) <= 1:
|
65
|
+
return arr
|
66
|
+
pivot = arr[len(arr) // 2]
|
67
|
+
# <Cursor here>
|
68
|
+
middle = [x for x in arr if x == pivot]
|
69
|
+
right = [x for x in arr if x > pivot]
|
70
|
+
return quicksort(left) + middle + quicksort(right)
|
71
|
+
```
|
72
|
+
|
73
|
+
2. Use `Ctrl-j` to generate and insert the code (or `Ctrl-l` to trigger → `Ctrl-p` to insert)
|
74
|
+
|
75
|
+
### **Repository-Level Code Completion**
|
76
|
+
|
77
|
+
| Option | Description |
|
78
|
+
|------------|------------------------------------------|
|
79
|
+
| `--repo` | Paths to include as repository context |
|
80
|
+
|
81
|
+
The `--repo` option enhances autocomplete by providing repository-level context to the LLM.
|
82
|
+
|
83
|
+
*Example Workflow*:
|
84
|
+
1. Launch VimLM with repo context: `vimlm main.py --repo utils/*`
|
85
|
+
2. In Insert mode, place cursor where completion is needed
|
86
|
+
3. `Ctrl-l` to generate suggestions informed by repository context
|
87
|
+
4. `Ctrl-p` to accept and insert the code
|
88
|
+
|
89
|
+
## Conversational Assistance
|
52
90
|
|
53
91
|
| Key Binding | Mode | Action |
|
54
92
|
|-------------|---------------|----------------------------------------|
|
@@ -86,12 +124,12 @@ vimlm
|
|
86
124
|
|
87
125
|
`!` prefix to embed inline directives in prompts:
|
88
126
|
|
89
|
-
| Directive | Description
|
90
|
-
|
127
|
+
| Directive | Description |
|
128
|
+
|------------------|--------------------------------------------|
|
91
129
|
| `!include PATH` | Add file/directory/shell output to context |
|
92
|
-
| `!deploy DEST` | Save code blocks to directory
|
93
|
-
| `!continue N` | Continue stopped response
|
94
|
-
| `!followup` | Continue conversation
|
130
|
+
| `!deploy DEST` | Save code blocks to directory |
|
131
|
+
| `!continue N` | Continue stopped response |
|
132
|
+
| `!followup` | Continue conversation |
|
95
133
|
|
96
134
|
### 1. **Context Layering**
|
97
135
|
```text
|
@@ -12,10 +12,13 @@
|
|
12
12
|
# See the License for the specific language governing permissions and
|
13
13
|
# limitations under the License.
|
14
14
|
|
15
|
+
import nanollama
|
16
|
+
import mlx_lm_utils
|
15
17
|
import asyncio
|
16
18
|
import subprocess
|
17
19
|
import json
|
18
20
|
import os
|
21
|
+
import glob
|
19
22
|
from watchfiles import awatch
|
20
23
|
import shutil
|
21
24
|
from datetime import datetime
|
@@ -26,21 +29,27 @@ from pathlib import Path
|
|
26
29
|
from string import Template
|
27
30
|
import re
|
28
31
|
|
32
|
+
import sys
|
33
|
+
import tty
|
34
|
+
import termios
|
35
|
+
|
29
36
|
DEFAULTS = dict(
|
30
|
-
LLM_MODEL =
|
37
|
+
LLM_MODEL = "mlx-community/Qwen2.5-Coder-3B-Instruct-4bit", # None | "mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit" | "mlx-community/deepseek-r1-distill-qwen-1.5b" | "mlx-community/phi-4-4bit" (8.25gb) | "mlx-community/Qwen2.5-Coder-14B-Instruct-4bit" (8.31gb) | "mlx-community/Qwen2.5-Coder-3B-Instruct-4bit" (1.74gb)
|
38
|
+
FIM_MODEL = "mlx-community/Qwen2.5-Coder-0.5B-4bit", # None | "mlx-community/Qwen2.5-Coder-32B-4bit" | "mlx-community/Qwen2.5-Coder-0.5B-4bit" (278mb)
|
31
39
|
NUM_TOKEN = 2000,
|
32
40
|
USE_LEADER = False,
|
33
41
|
KEY_MAP = {},
|
34
42
|
DO_RESET = True,
|
35
43
|
SHOW_USER = False,
|
36
44
|
SEP_CMD = '!',
|
37
|
-
|
45
|
+
THINK = ('<think>', '</think>'),
|
46
|
+
VERSION = '0.1.0',
|
38
47
|
DEBUG = False,
|
39
48
|
)
|
40
49
|
|
41
50
|
DATE_FORM = "%Y_%m_%d_%H_%M_%S"
|
42
|
-
VIMLM_DIR = os.path.expanduser("
|
43
|
-
WATCH_DIR = os.path.expanduser("
|
51
|
+
VIMLM_DIR = os.path.expanduser("~/.vimlm")
|
52
|
+
WATCH_DIR = os.path.expanduser("~/.vimlm/watch_dir")
|
44
53
|
CFG_FILE = 'cfg.json'
|
45
54
|
LOG_FILE = "log.json"
|
46
55
|
LTM_FILE = "cache.json"
|
@@ -51,35 +60,35 @@ LOG_PATH = os.path.join(VIMLM_DIR, LOG_FILE)
|
|
51
60
|
LTM_PATH = os.path.join(VIMLM_DIR, LTM_FILE)
|
52
61
|
OUT_PATH = os.path.join(WATCH_DIR, OUT_FILE)
|
53
62
|
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
if
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
63
|
+
def reset_dir(dir_path):
|
64
|
+
if os.path.exists(dir_path):
|
65
|
+
shutil.rmtree(dir_path)
|
66
|
+
os.makedirs(dir_path)
|
67
|
+
|
68
|
+
def initialize():
|
69
|
+
def is_incompatible(config):
|
70
|
+
v_str = config.get('VERSION', '0.0.0')
|
71
|
+
for min_v, usr_v in zip(DEFAULTS['VERSION'].split('.'), v_str.split('.')):
|
72
|
+
if int(min_v) < int(usr_v):
|
73
|
+
return False
|
74
|
+
elif int(min_v) > int(usr_v):
|
75
|
+
return True
|
76
|
+
return False
|
77
|
+
try:
|
78
|
+
with open(CFG_PATH, "r") as f:
|
79
|
+
config = json.load(f)
|
80
|
+
if is_incompatible(config):
|
81
|
+
raise ValueError('Incompatible version')
|
82
|
+
except Exception as e:
|
83
|
+
print('Initializing config')
|
84
|
+
reset_dir(VIMLM_DIR)
|
85
|
+
config = DEFAULTS
|
86
|
+
with open(CFG_PATH, 'w') as f:
|
87
|
+
json.dump(DEFAULTS, f, indent=2)
|
88
|
+
for k, v in DEFAULTS.items():
|
89
|
+
globals()[k] = config.get(k, v)
|
90
|
+
|
91
|
+
initialize()
|
83
92
|
|
84
93
|
def toout(s, key=None, mode=None):
|
85
94
|
key = '' if key is None else ':'+key
|
@@ -89,7 +98,7 @@ def toout(s, key=None, mode=None):
|
|
89
98
|
tolog(s, key='tovim'+key+':'+mode)
|
90
99
|
|
91
100
|
def tolog(log, key='debug'):
|
92
|
-
if not DEBUG and
|
101
|
+
if not DEBUG and 'debug' in key:
|
93
102
|
return
|
94
103
|
try:
|
95
104
|
with open(LOG_PATH, "r", encoding="utf-8") as log_f:
|
@@ -112,18 +121,8 @@ def print_log():
|
|
112
121
|
print(log["log"])
|
113
122
|
print('\033[0m')
|
114
123
|
|
115
|
-
toout('Loading LLM...')
|
116
|
-
if LLM_MODEL is None:
|
117
|
-
from nanollama import Chat
|
118
|
-
chat = Chat(model_path='uncn_llama_32_3b_it')
|
119
|
-
toout(f'LLM is ready')
|
120
|
-
else:
|
121
|
-
from mlx_lm_utils import Chat
|
122
|
-
chat = Chat(model_path=LLM_MODEL)
|
123
|
-
toout(f'{model_path.split('/')[-1]} is ready')
|
124
|
-
|
125
124
|
def deploy(dest=None, src=None, reformat=True):
|
126
|
-
prompt_deploy = 'Reformat the
|
125
|
+
prompt_deploy = 'Reformat the response 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.'
|
127
126
|
tolog(f'deploy {dest=} {src=} {reformat=}')
|
128
127
|
if src:
|
129
128
|
chat.reset()
|
@@ -281,13 +280,10 @@ def ingest(src, max_len=NUM_TOKEN):
|
|
281
280
|
for i, s in enumerate(list_str):
|
282
281
|
chat.reset()
|
283
282
|
toout(f'\n\nIngesting {k_base} {i+1}/{len(list_str)}...\n\n', mode='a')
|
284
|
-
tolog(f'{s=}') # DEBUG
|
285
283
|
newsum = chat(format_ingest.format(volat=volat, incoming=s.rstrip()), max_new=max_new_sum, verbose=False, stream=OUT_PATH)['text'].rstrip()
|
286
|
-
tolog(f'{k} {i+1}/{len(list_str)}: {newsum=}') # DEBUG
|
287
284
|
accum += newsum + ' ...\n'
|
288
285
|
volat = format_volat.format(k=k, newsum=newsum)
|
289
286
|
toout(f'\n\nIngesting {k_base}...\n\n', mode='a')
|
290
|
-
tolog(f'{accum=}') # DEBUG
|
291
287
|
if chat.get_ntok(accum) <= max_new_accum:
|
292
288
|
chat_summary = accum.strip()
|
293
289
|
else:
|
@@ -302,6 +298,15 @@ def ingest(src, max_len=NUM_TOKEN):
|
|
302
298
|
return result
|
303
299
|
|
304
300
|
def process_command(data):
|
301
|
+
if 'fim' in data:
|
302
|
+
toout('Autocompleting...')
|
303
|
+
response = fim.fim(prefix=data['context'], suffix=data['yank'], current_path=data['tree'])
|
304
|
+
toout(response['autocomplete'], 'fim')
|
305
|
+
tolog(response)
|
306
|
+
data['user_prompt'] = ''
|
307
|
+
return data
|
308
|
+
for i in IN_FILES:
|
309
|
+
data[i] = data[i].strip()
|
305
310
|
if len(data['user']) == 0:
|
306
311
|
response = chat.resume(max_new=NUM_TOKEN, verbose=False, stream=OUT_PATH)
|
307
312
|
toout(response['text'], mode='a')
|
@@ -321,7 +326,7 @@ def process_command(data):
|
|
321
326
|
arg = cmd.removeprefix('continue').strip('(').strip(')').strip().strip('"').strip("'").strip()
|
322
327
|
data['max_new'] = NUM_TOKEN if len(arg) == 0 else int(arg)
|
323
328
|
response = chat.resume(max_new=data['max_new'], verbose=False, stream=OUT_PATH)
|
324
|
-
toout(response['text']
|
329
|
+
toout(response['text'])
|
325
330
|
tolog(response)
|
326
331
|
do_reset = False
|
327
332
|
break
|
@@ -398,18 +403,19 @@ def process_command(data):
|
|
398
403
|
async def monitor_directory():
|
399
404
|
async for changes in awatch(WATCH_DIR):
|
400
405
|
found_files = {os.path.basename(f) for _, f in changes}
|
401
|
-
tolog(f'{found_files=}') # DEBUG
|
402
406
|
if IN_FILES[-1] in found_files and set(IN_FILES).issubset(set(os.listdir(WATCH_DIR))):
|
403
|
-
tolog(f'listdir()={os.listdir(WATCH_DIR)}') # DEBUG
|
404
407
|
data = {}
|
405
408
|
for file in IN_FILES:
|
406
409
|
path = os.path.join(WATCH_DIR, file)
|
407
410
|
with open(path, 'r', encoding='utf-8') as f:
|
408
|
-
data[file] = f.read()
|
411
|
+
data[file] = f.read()
|
409
412
|
os.remove(os.path.join(WATCH_DIR, file))
|
410
413
|
if 'followup' in os.listdir(WATCH_DIR):
|
411
414
|
os.remove(os.path.join(WATCH_DIR, 'followup'))
|
412
415
|
data['followup'] = True
|
416
|
+
if 'fim' in os.listdir(WATCH_DIR):
|
417
|
+
os.remove(os.path.join(WATCH_DIR, 'fim'))
|
418
|
+
data['fim'] = True
|
413
419
|
if 'quit' in os.listdir(WATCH_DIR):
|
414
420
|
os.remove(os.path.join(WATCH_DIR, 'quit'))
|
415
421
|
data['quit'] = True
|
@@ -420,6 +426,8 @@ async def process_files(data):
|
|
420
426
|
str_template = '{include}'
|
421
427
|
data = process_command(data)
|
422
428
|
if len(data['user_prompt']) == 0:
|
429
|
+
if 'wip' in os.listdir(WATCH_DIR):
|
430
|
+
os.remove(os.path.join(WATCH_DIR, 'wip'))
|
423
431
|
return
|
424
432
|
if len(data['file']) > 0:
|
425
433
|
str_template += '**{file}**\n'
|
@@ -434,8 +442,6 @@ async def process_files(data):
|
|
434
442
|
else:
|
435
443
|
str_template += "`{yank}` "
|
436
444
|
str_template += '{user_prompt}'
|
437
|
-
tolog(f'process_files o {data=}') # DEBUG
|
438
|
-
tolog(f'process_files {str_template=}') # DEBUG
|
439
445
|
prompt = str_template.format(**data)
|
440
446
|
tolog(prompt, 'tollm')
|
441
447
|
toout('')
|
@@ -451,6 +457,8 @@ async def process_files(data):
|
|
451
457
|
f.write(response['text'])
|
452
458
|
if 'deploy_dest' in data:
|
453
459
|
deploy(dest=data['deploy_dest'], reformat=False)
|
460
|
+
if 'wip' in os.listdir(WATCH_DIR):
|
461
|
+
os.remove(os.path.join(WATCH_DIR, 'wip'))
|
454
462
|
|
455
463
|
KEYL = KEY_MAP.get('l', 'l')
|
456
464
|
KEYJ = KEY_MAP.get('j', 'j')
|
@@ -639,10 +647,11 @@ function! PasteIntoLastVisualSelection(...)
|
|
639
647
|
endfunction
|
640
648
|
|
641
649
|
function! VimLM(...) range
|
642
|
-
let
|
643
|
-
while filereadable(
|
650
|
+
let wip_file = s:watched_dir . '/wip'
|
651
|
+
while filereadable(wip_file)
|
644
652
|
sleep 100m
|
645
653
|
endwhile
|
654
|
+
call writefile([], wip_file, 'w')
|
646
655
|
let user_input = join(a:000, ' ')
|
647
656
|
if empty(user_input)
|
648
657
|
echo "Usage: :VimLM <prompt> [!command1] [!command2] ..."
|
@@ -656,12 +665,73 @@ function! VimLM(...) range
|
|
656
665
|
let user_file = s:watched_dir . '/user'
|
657
666
|
call writefile([user_input], user_file, 'w')
|
658
667
|
let current_file = expand('%:p')
|
668
|
+
let tree_file = s:watched_dir . '/tree'
|
669
|
+
call writefile([current_file], tree_file, 'w')
|
670
|
+
call ScrollToTop()
|
671
|
+
endfunction
|
672
|
+
|
673
|
+
function! SplitAtCursorInInsert()
|
674
|
+
let pos = getcurpos()
|
675
|
+
let line_num = pos[1]
|
676
|
+
let col = pos[2]
|
677
|
+
let lines = getline(1, '$')
|
678
|
+
let current_line = lines[line_num - 1]
|
679
|
+
let prefix_lines = lines[0:line_num - 2]
|
680
|
+
let prefix_part = strpart(current_line, 0, col - 1)
|
681
|
+
if !empty(prefix_part) || col > 1
|
682
|
+
call add(prefix_lines, prefix_part)
|
683
|
+
endif
|
684
|
+
let suffix_lines = []
|
685
|
+
let suffix_part = strpart(current_line, col - 1)
|
686
|
+
if !empty(suffix_part)
|
687
|
+
call add(suffix_lines, suffix_part)
|
688
|
+
endif
|
689
|
+
if line_num < len(lines)
|
690
|
+
call extend(suffix_lines, lines[line_num:])
|
691
|
+
endif
|
692
|
+
call writefile(prefix_lines, s:watched_dir . '/context', 'b')
|
693
|
+
call writefile(suffix_lines, s:watched_dir . '/yank', 'b')
|
694
|
+
call writefile([], s:watched_dir . '/fim', 'w')
|
695
|
+
call writefile([], s:watched_dir . '/user', 'w')
|
696
|
+
let current_file = expand('%:p')
|
697
|
+
let tree_file = s:watched_dir . '/tree'
|
659
698
|
call writefile([current_file], tree_file, 'w')
|
660
699
|
call ScrollToTop()
|
661
700
|
endfunction
|
662
701
|
|
702
|
+
function! InsertResponse()
|
703
|
+
let response_path = s:watched_dir . '/response.md'
|
704
|
+
if !filereadable(response_path)
|
705
|
+
echoerr "Response file not found: " . response_path
|
706
|
+
return
|
707
|
+
endif
|
708
|
+
let content = readfile(response_path)
|
709
|
+
let text = join(content, "\n")
|
710
|
+
call setreg('z', text)
|
711
|
+
let col = col('.')
|
712
|
+
let line = getline('.')
|
713
|
+
if col == len(line) + 1
|
714
|
+
normal! "zgp
|
715
|
+
else
|
716
|
+
normal! "zgP
|
717
|
+
endif
|
718
|
+
endfunction
|
719
|
+
|
720
|
+
function! TabInInsert()
|
721
|
+
let wip_file = s:watched_dir . '/wip'
|
722
|
+
call writefile([], wip_file, 'w')
|
723
|
+
call SplitAtCursorInInsert()
|
724
|
+
while filereadable(wip_file)
|
725
|
+
sleep 10m
|
726
|
+
endwhile
|
727
|
+
call InsertResponse()
|
728
|
+
endfunction
|
729
|
+
|
663
730
|
command! ToggleVimLM call ToggleVimLM()
|
664
731
|
command! -range -nargs=+ VimLM call VimLM(<f-args>)
|
732
|
+
inoremap <silent> $mapl <C-\><C-o>:call SplitAtCursorInInsert()<CR>
|
733
|
+
inoremap <silent> $mapp <C-\><C-o>:call InsertResponse()<CR><Right>
|
734
|
+
inoremap <silent> $mapj <C-\><C-o>:call TabInInsert()<CR><Right>
|
665
735
|
nnoremap $mapp :call PasteIntoLastVisualSelection()<CR>
|
666
736
|
vnoremap $mapp <Cmd>:call PasteIntoLastVisualSelection()<CR>
|
667
737
|
vnoremap $mapl <Cmd>:call VisualPrompt()<CR>
|
@@ -670,19 +740,13 @@ nnoremap $mapj :call FollowUpPrompt()<CR>
|
|
670
740
|
call Monitor()
|
671
741
|
""").safe_substitute(dict(WATCH_DIR=WATCH_DIR, mapl=mapl, mapj=mapj, mapp=mapp))
|
672
742
|
|
673
|
-
async def main():
|
674
|
-
parser = argparse.ArgumentParser(description="VimLM - LLM-powered Vim assistant")
|
675
|
-
parser.add_argument('--test', action='store_true')
|
676
|
-
parser.add_argument("vim_args", nargs=argparse.REMAINDER, help="Vim arguments")
|
677
|
-
args = parser.parse_args()
|
678
|
-
if args.test:
|
679
|
-
return
|
743
|
+
async def main(args):
|
680
744
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.vim', delete=False) as f:
|
681
745
|
f.write(VIMLMSCRIPT)
|
682
746
|
vim_script = f.name
|
683
747
|
vim_command = ["vim", "-c", f"source {vim_script}"]
|
684
|
-
if args.
|
685
|
-
vim_command.extend(args.
|
748
|
+
if args.args_vim:
|
749
|
+
vim_command.extend(args.args_vim)
|
686
750
|
else:
|
687
751
|
vim_command.append('.tmp')
|
688
752
|
try:
|
@@ -697,8 +761,166 @@ async def main():
|
|
697
761
|
pass
|
698
762
|
os.remove(vim_script)
|
699
763
|
|
764
|
+
def get_common_dir_and_children(file_paths):
|
765
|
+
dirs = [os.path.dirname(path) for path in file_paths]
|
766
|
+
dir_parts = [path.split(os.sep) for path in dirs]
|
767
|
+
common_parts = []
|
768
|
+
for parts in zip(*dir_parts):
|
769
|
+
if all(part == parts[0] for part in parts):
|
770
|
+
common_parts.append(parts[0])
|
771
|
+
else:
|
772
|
+
break
|
773
|
+
parent_path = os.sep.join(common_parts)
|
774
|
+
child_paths = [os.path.relpath(path, parent_path) for path in file_paths]
|
775
|
+
repo_name = os.path.basename(os.path.dirname(parent_path))
|
776
|
+
return repo_name, parent_path, child_paths
|
777
|
+
|
778
|
+
def get_key():
|
779
|
+
fd = sys.stdin.fileno()
|
780
|
+
old_settings = termios.tcgetattr(fd)
|
781
|
+
try:
|
782
|
+
tty.setraw(sys.stdin.fileno())
|
783
|
+
ch = sys.stdin.read(1)
|
784
|
+
if ch == '\x1b':
|
785
|
+
ch = sys.stdin.read(2)
|
786
|
+
if ch == '[A':
|
787
|
+
return 'up'
|
788
|
+
elif ch == '[B':
|
789
|
+
return 'down'
|
790
|
+
elif ch == 'j':
|
791
|
+
return 'down'
|
792
|
+
elif ch == 'k':
|
793
|
+
return 'up'
|
794
|
+
elif ch in [' ', 'x']:
|
795
|
+
return 'space'
|
796
|
+
elif ch == '\r':
|
797
|
+
return 'enter'
|
798
|
+
elif ch == 'q':
|
799
|
+
return 'quit'
|
800
|
+
finally:
|
801
|
+
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
|
802
|
+
return None
|
803
|
+
|
804
|
+
def select_files_interactive(file_paths):
|
805
|
+
selected = [False] * len(file_paths)
|
806
|
+
current_row = 0
|
807
|
+
visible_start = 0
|
808
|
+
visible_end = 0
|
809
|
+
max_visible = 10
|
810
|
+
def display():
|
811
|
+
nonlocal visible_start, visible_end
|
812
|
+
visible_start = max(0, current_row - max_visible + 2)
|
813
|
+
visible_end = min(len(file_paths), visible_start + max_visible)
|
814
|
+
sys.stdout.write(f"\x1b[{max_visible + 2}A")
|
815
|
+
for i in range(visible_start, visible_end):
|
816
|
+
prefix = "> " if i == current_row else " "
|
817
|
+
check = "[X]" if selected[i] else "[ ]"
|
818
|
+
filename = os.path.basename(file_paths[i])[:40]
|
819
|
+
sys.stdout.write(f"\x1b[K{prefix}{check} {filename}\n")
|
820
|
+
scroll_indicator = f" [{visible_start+1}-{visible_end} of {len(file_paths)}] "
|
821
|
+
sys.stdout.write(f"\x1b[K{scroll_indicator}\nSpace:Toggle Enter:Confirm Arrows:Navigate\n")
|
822
|
+
sys.stdout.flush()
|
823
|
+
sys.stdout.write("\n" * (max_visible + 2))
|
824
|
+
display()
|
825
|
+
while True:
|
826
|
+
key = get_key()
|
827
|
+
if key == 'up' and current_row > 0:
|
828
|
+
current_row -= 1
|
829
|
+
if current_row < visible_start:
|
830
|
+
visible_start = max(0, visible_start - 1)
|
831
|
+
visible_end = visible_start + max_visible
|
832
|
+
display()
|
833
|
+
elif key == 'down' and current_row < len(file_paths) - 1:
|
834
|
+
current_row += 1
|
835
|
+
if current_row >= visible_end:
|
836
|
+
visible_start = min(len(file_paths) - max_visible, visible_start + 1)
|
837
|
+
visible_end = visible_start + max_visible
|
838
|
+
display()
|
839
|
+
elif key == 'space':
|
840
|
+
selected[current_row] = not selected[current_row]
|
841
|
+
display()
|
842
|
+
elif key == 'enter':
|
843
|
+
sys.stdout.write(f"\x1b[{max_visible + 2}B")
|
844
|
+
sys.stdout.write("\x1b[J")
|
845
|
+
break
|
846
|
+
elif key == 'quit':
|
847
|
+
# selected = []
|
848
|
+
# break
|
849
|
+
return None
|
850
|
+
return [file_paths[i] for i in range(len(file_paths)) if selected[i]]
|
851
|
+
|
852
|
+
def get_repo(args_repo, args_vim):
|
853
|
+
if not args_repo:
|
854
|
+
return None
|
855
|
+
vim_files = []
|
856
|
+
for arg in args_vim:
|
857
|
+
if not arg.startswith('-'):
|
858
|
+
if os.path.exists(arg):
|
859
|
+
vim_files.append(os.path.abspath(arg))
|
860
|
+
repo_paths = []
|
861
|
+
for pattern in args_repo:
|
862
|
+
expanded_paths = glob.glob(pattern)
|
863
|
+
if expanded_paths:
|
864
|
+
for path in expanded_paths:
|
865
|
+
if not os.path.isfile(path) or path in vim_files+repo_paths or os.path.basename(path).startswith('.') or is_binary(path):
|
866
|
+
continue
|
867
|
+
repo_paths.append(os.path.abspath(path))
|
868
|
+
if len(repo_paths) > 9:
|
869
|
+
try:
|
870
|
+
sys.stdout.write("\n")
|
871
|
+
selected_paths = select_files_interactive(repo_paths)
|
872
|
+
if not selected_paths:
|
873
|
+
return None
|
874
|
+
sys.stdout.write("\x1b[2A")
|
875
|
+
sys.stdout.write("\x1b[J")
|
876
|
+
repo_paths = selected_paths
|
877
|
+
except:
|
878
|
+
pass
|
879
|
+
repo_files = []
|
880
|
+
rest_files = []
|
881
|
+
for path in repo_paths:
|
882
|
+
if path in vim_files:
|
883
|
+
rest_files.append(os.path.abspath(path))
|
884
|
+
else:
|
885
|
+
repo_files.append(os.path.abspath(path))
|
886
|
+
repo_name, repo_path, child_paths = get_common_dir_and_children(repo_files+rest_files)
|
887
|
+
repo_names, rest_names = child_paths[:len(repo_files)], child_paths[len(repo_files):]
|
888
|
+
list_content = [f'<|repo_name|>{repo_name}\n']
|
889
|
+
list_mtime = []
|
890
|
+
for p, n in zip(repo_files, repo_names):
|
891
|
+
try:
|
892
|
+
with open(p, 'r') as f:
|
893
|
+
list_content.append(f'<|file_sep|>{n}\n{f.read()}\n')
|
894
|
+
list_mtime.append(int(os.path.getmtime(p)))
|
895
|
+
except Exception as e:
|
896
|
+
tolog(f'Skipped {p} d/t {e}', 'debug:get_repo()')
|
897
|
+
return dict(repo_files=repo_files, rest_files=rest_files, rest_names=rest_names, vim_files=vim_files, list_mtime=list_mtime, list_content=list_content, repo_path=repo_path)
|
898
|
+
|
700
899
|
def run():
|
701
|
-
|
900
|
+
parser = argparse.ArgumentParser(description="VimLM - LLM-powered Vim assistant")
|
901
|
+
parser.add_argument('--test', action='store_true', help="Run in test mode")
|
902
|
+
parser.add_argument('args_vim', nargs='*', help="Vim arguments")
|
903
|
+
parser.add_argument('--repo', nargs='*', help="Paths to directories or files (e.g., assets/*, path/to/file)")
|
904
|
+
args = parser.parse_args()
|
905
|
+
dict_repo = get_repo(args.repo, args.args_vim)
|
906
|
+
tolog(dict_repo, 'debug:get_repo()')
|
907
|
+
if args.test:
|
908
|
+
return
|
909
|
+
reset_dir(WATCH_DIR)
|
910
|
+
toout('Loading LLM...')
|
911
|
+
if LLM_MODEL is None:
|
912
|
+
globals()['chat'] = nanollama.Chat(model_path='uncn_llama_32_3b_it')
|
913
|
+
toout(f'LLM is ready')
|
914
|
+
else:
|
915
|
+
globals()['chat'] = mlx_lm_utils.Chat(model_path=LLM_MODEL, think=THINK)
|
916
|
+
toout(f'{LLM_MODEL.split('/')[-1]} is ready')
|
917
|
+
if FIM_MODEL and FIM_MODEL != LLM_MODEL:
|
918
|
+
globals()['fim'] = mlx_lm_utils.Chat(model_path=FIM_MODEL, cache_dir=VIMLM_DIR, dict_repo=dict_repo)
|
919
|
+
toout(f'\n\n{FIM_MODEL.split("/")[-1]} is ready', mode='a')
|
920
|
+
else:
|
921
|
+
globals()['fim'] = chat
|
922
|
+
chat.set_cache_repo(dict_repo, cache_dir=VIMLM_DIR)
|
923
|
+
asyncio.run(main(args))
|
702
924
|
|
703
925
|
if __name__ == '__main__':
|
704
926
|
run()
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|