vimlm 0.0.9__tar.gz → 0.1.0__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: vimlm
3
- Version: 0.0.9
3
+ Version: 0.1.0
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.5
12
- Requires-Dist: mlx_lm_utils>=0.0.3
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
  ![VimLM Demo](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/captioned_vimlm.gif)
@@ -48,7 +47,45 @@ pip install vimlm
48
47
  vimlm
49
48
  ```
50
49
 
51
- ## Basic Usage
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
+
59
+ *Example Workflow*:
60
+ 1. Place cursor where you need code
61
+ ```python
62
+ def quicksort(arr):
63
+ if len(arr) <= 1:
64
+ return arr
65
+ pivot = arr[len(arr) // 2]
66
+ # <Cursor here>
67
+ middle = [x for x in arr if x == pivot]
68
+ right = [x for x in arr if x > pivot]
69
+ return quicksort(left) + middle + quicksort(right)
70
+ ```
71
+
72
+ 2. `Ctrl-l` to trigger → `Ctrl-p` to insert
73
+
74
+ ### **Repository-Level Code Completion**
75
+
76
+ | Option | Description |
77
+ |------------|------------------------------------------|
78
+ | `--repo` | Paths to include as repository context |
79
+
80
+ The `--repo` option enhances autocomplete by providing repository-level context to the LLM.
81
+
82
+ *Example Workflow*:
83
+ 1. Launch VimLM with repo context: `vimlm main.py --repo utils/*`
84
+ 2. In Insert mode, place cursor where completion is needed
85
+ 3. `Ctrl-l` to generate suggestions informed by repository context
86
+ 4. `Ctrl-p` to accept and insert the code
87
+
88
+ ## Conversational Assistance
52
89
 
53
90
  | Key Binding | Mode | Action |
54
91
  |-------------|---------------|----------------------------------------|
@@ -86,12 +123,12 @@ vimlm
86
123
 
87
124
  `!` prefix to embed inline directives in prompts:
88
125
 
89
- | Directive | Description |
90
- |------------------|------------------------------------------|
126
+ | Directive | Description |
127
+ |------------------|--------------------------------------------|
91
128
  | `!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 |
129
+ | `!deploy DEST` | Save code blocks to directory |
130
+ | `!continue N` | Continue stopped response |
131
+ | `!followup` | Continue conversation |
95
132
 
96
133
  ### 1. **Context Layering**
97
134
  ```text
@@ -141,6 +178,29 @@ Simplify complex tasks by chaining multiple commands together into a single, reu
141
178
  :VimLM Add Google-style docstrings !include % !continue 4000
142
179
  ```
143
180
 
181
+ ## Smart Autocomplete
182
+ **Fill code gaps intelligently**
183
+
184
+ | Key Binding | Mode | Action |
185
+ |-------------|---------|-----------------------------------------|
186
+ | `Ctrl-l` | Insert | Generate code suggestion |
187
+ | `Ctrl-p` | Insert | Insert generated code |
188
+
189
+ *Example Workflow*:
190
+ 1. Place cursor where you need code
191
+ ```python
192
+ def quicksort(arr):
193
+ if len(arr) <= 1:
194
+ return arr
195
+ pivot = arr[len(arr) // 2]
196
+ # <Cursor here>
197
+ middle = [x for x in arr if x == pivot]
198
+ right = [x for x in arr if x > pivot]
199
+ return quicksort(left) + middle + quicksort(right)
200
+ ```
201
+
202
+ 2. `Ctrl-l` to trigger → `Ctrl-p` to insert
203
+
144
204
  ## Configuration
145
205
 
146
206
  ### 1. **Model Settings**
@@ -1,4 +1,3 @@
1
-
2
1
  # VimLM - AI-Powered Coding Assistant for Vim
3
2
 
4
3
  ![VimLM Demo](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/captioned_vimlm.gif)
@@ -26,7 +25,45 @@ pip install vimlm
26
25
  vimlm
27
26
  ```
28
27
 
29
- ## Basic Usage
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
+
37
+ *Example Workflow*:
38
+ 1. Place cursor where you need code
39
+ ```python
40
+ def quicksort(arr):
41
+ if len(arr) <= 1:
42
+ return arr
43
+ pivot = arr[len(arr) // 2]
44
+ # <Cursor here>
45
+ middle = [x for x in arr if x == pivot]
46
+ right = [x for x in arr if x > pivot]
47
+ return quicksort(left) + middle + quicksort(right)
48
+ ```
49
+
50
+ 2. `Ctrl-l` to trigger → `Ctrl-p` to insert
51
+
52
+ ### **Repository-Level Code Completion**
53
+
54
+ | Option | Description |
55
+ |------------|------------------------------------------|
56
+ | `--repo` | Paths to include as repository context |
57
+
58
+ The `--repo` option enhances autocomplete by providing repository-level context to the LLM.
59
+
60
+ *Example Workflow*:
61
+ 1. Launch VimLM with repo context: `vimlm main.py --repo utils/*`
62
+ 2. In Insert mode, place cursor where completion is needed
63
+ 3. `Ctrl-l` to generate suggestions informed by repository context
64
+ 4. `Ctrl-p` to accept and insert the code
65
+
66
+ ## Conversational Assistance
30
67
 
31
68
  | Key Binding | Mode | Action |
32
69
  |-------------|---------------|----------------------------------------|
@@ -64,12 +101,12 @@ vimlm
64
101
 
65
102
  `!` prefix to embed inline directives in prompts:
66
103
 
67
- | Directive | Description |
68
- |------------------|------------------------------------------|
104
+ | Directive | Description |
105
+ |------------------|--------------------------------------------|
69
106
  | `!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 |
107
+ | `!deploy DEST` | Save code blocks to directory |
108
+ | `!continue N` | Continue stopped response |
109
+ | `!followup` | Continue conversation |
73
110
 
74
111
  ### 1. **Context Layering**
75
112
  ```text
@@ -119,6 +156,29 @@ Simplify complex tasks by chaining multiple commands together into a single, reu
119
156
  :VimLM Add Google-style docstrings !include % !continue 4000
120
157
  ```
121
158
 
159
+ ## Smart Autocomplete
160
+ **Fill code gaps intelligently**
161
+
162
+ | Key Binding | Mode | Action |
163
+ |-------------|---------|-----------------------------------------|
164
+ | `Ctrl-l` | Insert | Generate code suggestion |
165
+ | `Ctrl-p` | Insert | Insert generated code |
166
+
167
+ *Example Workflow*:
168
+ 1. Place cursor where you need code
169
+ ```python
170
+ def quicksort(arr):
171
+ if len(arr) <= 1:
172
+ return arr
173
+ pivot = arr[len(arr) // 2]
174
+ # <Cursor here>
175
+ middle = [x for x in arr if x == pivot]
176
+ right = [x for x in arr if x > pivot]
177
+ return quicksort(left) + middle + quicksort(right)
178
+ ```
179
+
180
+ 2. `Ctrl-l` to trigger → `Ctrl-p` to insert
181
+
122
182
  ## Configuration
123
183
 
124
184
  ### 1. **Model Settings**
@@ -5,7 +5,7 @@ with open("requirements.txt") as f:
5
5
 
6
6
  setup(
7
7
  name="vimlm",
8
- version="0.0.9",
8
+ version="0.1.0",
9
9
  author="Josef Albers",
10
10
  author_email="albersj66@gmail.com",
11
11
  readme='README.md',
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: vimlm
3
- Version: 0.0.9
3
+ Version: 0.1.0
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.5
12
- Requires-Dist: mlx_lm_utils>=0.0.3
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
  ![VimLM Demo](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/captioned_vimlm.gif)
@@ -48,7 +47,45 @@ pip install vimlm
48
47
  vimlm
49
48
  ```
50
49
 
51
- ## Basic Usage
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
+
59
+ *Example Workflow*:
60
+ 1. Place cursor where you need code
61
+ ```python
62
+ def quicksort(arr):
63
+ if len(arr) <= 1:
64
+ return arr
65
+ pivot = arr[len(arr) // 2]
66
+ # <Cursor here>
67
+ middle = [x for x in arr if x == pivot]
68
+ right = [x for x in arr if x > pivot]
69
+ return quicksort(left) + middle + quicksort(right)
70
+ ```
71
+
72
+ 2. `Ctrl-l` to trigger → `Ctrl-p` to insert
73
+
74
+ ### **Repository-Level Code Completion**
75
+
76
+ | Option | Description |
77
+ |------------|------------------------------------------|
78
+ | `--repo` | Paths to include as repository context |
79
+
80
+ The `--repo` option enhances autocomplete by providing repository-level context to the LLM.
81
+
82
+ *Example Workflow*:
83
+ 1. Launch VimLM with repo context: `vimlm main.py --repo utils/*`
84
+ 2. In Insert mode, place cursor where completion is needed
85
+ 3. `Ctrl-l` to generate suggestions informed by repository context
86
+ 4. `Ctrl-p` to accept and insert the code
87
+
88
+ ## Conversational Assistance
52
89
 
53
90
  | Key Binding | Mode | Action |
54
91
  |-------------|---------------|----------------------------------------|
@@ -86,12 +123,12 @@ vimlm
86
123
 
87
124
  `!` prefix to embed inline directives in prompts:
88
125
 
89
- | Directive | Description |
90
- |------------------|------------------------------------------|
126
+ | Directive | Description |
127
+ |------------------|--------------------------------------------|
91
128
  | `!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 |
129
+ | `!deploy DEST` | Save code blocks to directory |
130
+ | `!continue N` | Continue stopped response |
131
+ | `!followup` | Continue conversation |
95
132
 
96
133
  ### 1. **Context Layering**
97
134
  ```text
@@ -141,6 +178,29 @@ Simplify complex tasks by chaining multiple commands together into a single, reu
141
178
  :VimLM Add Google-style docstrings !include % !continue 4000
142
179
  ```
143
180
 
181
+ ## Smart Autocomplete
182
+ **Fill code gaps intelligently**
183
+
184
+ | Key Binding | Mode | Action |
185
+ |-------------|---------|-----------------------------------------|
186
+ | `Ctrl-l` | Insert | Generate code suggestion |
187
+ | `Ctrl-p` | Insert | Insert generated code |
188
+
189
+ *Example Workflow*:
190
+ 1. Place cursor where you need code
191
+ ```python
192
+ def quicksort(arr):
193
+ if len(arr) <= 1:
194
+ return arr
195
+ pivot = arr[len(arr) // 2]
196
+ # <Cursor here>
197
+ middle = [x for x in arr if x == pivot]
198
+ right = [x for x in arr if x > pivot]
199
+ return quicksort(left) + middle + quicksort(right)
200
+ ```
201
+
202
+ 2. `Ctrl-l` to trigger → `Ctrl-p` to insert
203
+
144
204
  ## Configuration
145
205
 
146
206
  ### 1. **Model Settings**
@@ -0,0 +1,3 @@
1
+ nanollama>=0.0.6
2
+ mlx_lm_utils>=0.0.4
3
+ watchfiles==1.0.4
@@ -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
@@ -27,20 +30,22 @@ from string import Template
27
30
  import re
28
31
 
29
32
  DEFAULTS = dict(
30
- LLM_MODEL = None, # "mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit"
33
+ 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)
34
+ 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
35
  NUM_TOKEN = 2000,
32
36
  USE_LEADER = False,
33
37
  KEY_MAP = {},
34
38
  DO_RESET = True,
35
39
  SHOW_USER = False,
36
40
  SEP_CMD = '!',
37
- VERSION = '0.0.9',
38
- DEBUG = False,
41
+ THINK = ('<think>', '</think>'),
42
+ VERSION = '0.1.0',
43
+ DEBUG = True,
39
44
  )
40
45
 
41
46
  DATE_FORM = "%Y_%m_%d_%H_%M_%S"
42
- VIMLM_DIR = os.path.expanduser("~/vimlm")
43
- WATCH_DIR = os.path.expanduser("~/vimlm/watch_dir")
47
+ VIMLM_DIR = os.path.expanduser("~/.vimlm")
48
+ WATCH_DIR = os.path.expanduser("~/.vimlm/watch_dir")
44
49
  CFG_FILE = 'cfg.json'
45
50
  LOG_FILE = "log.json"
46
51
  LTM_FILE = "cache.json"
@@ -51,35 +56,35 @@ LOG_PATH = os.path.join(VIMLM_DIR, LOG_FILE)
51
56
  LTM_PATH = os.path.join(VIMLM_DIR, LTM_FILE)
52
57
  OUT_PATH = os.path.join(WATCH_DIR, OUT_FILE)
53
58
 
54
- def is_old(config):
55
- v_str = config.get('VERSION', 0)
56
- for min_v, usr_v in zip(DEFAULTS['VERSION'].split('.'), v_str.split('.')):
57
- if int(min_v) < int(usr_v):
58
- return False
59
- elif int(min_v) > int(usr_v):
60
- return True
61
- return False
62
-
63
- if os.path.exists(WATCH_DIR):
64
- shutil.rmtree(WATCH_DIR)
65
- os.makedirs(WATCH_DIR)
66
-
67
- try:
68
- with open(CFG_PATH, "r") as f:
69
- config = json.load(f)
70
- if is_old(config):
71
- for p in [CFG_PATH, LOG_PATH, LTM_PATH]:
72
- if os.path.isfile(p):
73
- os.remove(p)
74
- raise ValueError(f'Updating config')
75
- except Exception as e:
76
- print(e)
77
- config = DEFAULTS
78
- with open(CFG_PATH, 'w') as f:
79
- json.dump(DEFAULTS, f, indent=2)
80
-
81
- for k, v in DEFAULTS.items():
82
- globals()[k] = config.get(k, v)
59
+ def reset_dir(dir_path):
60
+ if os.path.exists(dir_path):
61
+ shutil.rmtree(dir_path)
62
+ os.makedirs(dir_path)
63
+
64
+ def initialize():
65
+ def is_incompatible(config):
66
+ v_str = config.get('VERSION', '0.0.0')
67
+ for min_v, usr_v in zip(DEFAULTS['VERSION'].split('.'), v_str.split('.')):
68
+ if int(min_v) < int(usr_v):
69
+ return False
70
+ elif int(min_v) > int(usr_v):
71
+ return True
72
+ return False
73
+ try:
74
+ with open(CFG_PATH, "r") as f:
75
+ config = json.load(f)
76
+ if is_incompatible(config):
77
+ raise ValueError('Incompatible version')
78
+ except Exception as e:
79
+ print('Initializing config')
80
+ reset_dir(VIMLM_DIR)
81
+ config = DEFAULTS
82
+ with open(CFG_PATH, 'w') as f:
83
+ json.dump(DEFAULTS, f, indent=2)
84
+ for k, v in DEFAULTS.items():
85
+ globals()[k] = config.get(k, v)
86
+
87
+ initialize()
83
88
 
84
89
  def toout(s, key=None, mode=None):
85
90
  key = '' if key is None else ':'+key
@@ -112,18 +117,8 @@ def print_log():
112
117
  print(log["log"])
113
118
  print('\033[0m')
114
119
 
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
120
  def deploy(dest=None, src=None, reformat=True):
126
- 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.'
121
+ 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
122
  tolog(f'deploy {dest=} {src=} {reformat=}')
128
123
  if src:
129
124
  chat.reset()
@@ -281,13 +276,10 @@ def ingest(src, max_len=NUM_TOKEN):
281
276
  for i, s in enumerate(list_str):
282
277
  chat.reset()
283
278
  toout(f'\n\nIngesting {k_base} {i+1}/{len(list_str)}...\n\n', mode='a')
284
- tolog(f'{s=}') # DEBUG
285
279
  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
280
  accum += newsum + ' ...\n'
288
281
  volat = format_volat.format(k=k, newsum=newsum)
289
282
  toout(f'\n\nIngesting {k_base}...\n\n', mode='a')
290
- tolog(f'{accum=}') # DEBUG
291
283
  if chat.get_ntok(accum) <= max_new_accum:
292
284
  chat_summary = accum.strip()
293
285
  else:
@@ -302,6 +294,15 @@ def ingest(src, max_len=NUM_TOKEN):
302
294
  return result
303
295
 
304
296
  def process_command(data):
297
+ if 'fim' in data:
298
+ toout('Autocompleting...')
299
+ response = fim.fim(prefix=data['context'], suffix=data['yank'], current_path=data['tree'])
300
+ toout(response['autocomplete'], 'fim')
301
+ tolog(response)
302
+ data['user_prompt'] = ''
303
+ return data
304
+ for i in IN_FILES:
305
+ data[i] = data[i].strip()
305
306
  if len(data['user']) == 0:
306
307
  response = chat.resume(max_new=NUM_TOKEN, verbose=False, stream=OUT_PATH)
307
308
  toout(response['text'], mode='a')
@@ -321,7 +322,7 @@ def process_command(data):
321
322
  arg = cmd.removeprefix('continue').strip('(').strip(')').strip().strip('"').strip("'").strip()
322
323
  data['max_new'] = NUM_TOKEN if len(arg) == 0 else int(arg)
323
324
  response = chat.resume(max_new=data['max_new'], verbose=False, stream=OUT_PATH)
324
- toout(response['text'], mode='a')
325
+ toout(response['text'])
325
326
  tolog(response)
326
327
  do_reset = False
327
328
  break
@@ -398,18 +399,19 @@ def process_command(data):
398
399
  async def monitor_directory():
399
400
  async for changes in awatch(WATCH_DIR):
400
401
  found_files = {os.path.basename(f) for _, f in changes}
401
- tolog(f'{found_files=}') # DEBUG
402
402
  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
403
  data = {}
405
404
  for file in IN_FILES:
406
405
  path = os.path.join(WATCH_DIR, file)
407
406
  with open(path, 'r', encoding='utf-8') as f:
408
- data[file] = f.read().strip()
407
+ data[file] = f.read()
409
408
  os.remove(os.path.join(WATCH_DIR, file))
410
409
  if 'followup' in os.listdir(WATCH_DIR):
411
410
  os.remove(os.path.join(WATCH_DIR, 'followup'))
412
411
  data['followup'] = True
412
+ if 'fim' in os.listdir(WATCH_DIR):
413
+ os.remove(os.path.join(WATCH_DIR, 'fim'))
414
+ data['fim'] = True
413
415
  if 'quit' in os.listdir(WATCH_DIR):
414
416
  os.remove(os.path.join(WATCH_DIR, 'quit'))
415
417
  data['quit'] = True
@@ -420,6 +422,8 @@ async def process_files(data):
420
422
  str_template = '{include}'
421
423
  data = process_command(data)
422
424
  if len(data['user_prompt']) == 0:
425
+ if 'wip' in os.listdir(WATCH_DIR):
426
+ os.remove(os.path.join(WATCH_DIR, 'wip'))
423
427
  return
424
428
  if len(data['file']) > 0:
425
429
  str_template += '**{file}**\n'
@@ -434,8 +438,6 @@ async def process_files(data):
434
438
  else:
435
439
  str_template += "`{yank}` "
436
440
  str_template += '{user_prompt}'
437
- tolog(f'process_files o {data=}') # DEBUG
438
- tolog(f'process_files {str_template=}') # DEBUG
439
441
  prompt = str_template.format(**data)
440
442
  tolog(prompt, 'tollm')
441
443
  toout('')
@@ -451,6 +453,8 @@ async def process_files(data):
451
453
  f.write(response['text'])
452
454
  if 'deploy_dest' in data:
453
455
  deploy(dest=data['deploy_dest'], reformat=False)
456
+ if 'wip' in os.listdir(WATCH_DIR):
457
+ os.remove(os.path.join(WATCH_DIR, 'wip'))
454
458
 
455
459
  KEYL = KEY_MAP.get('l', 'l')
456
460
  KEYJ = KEY_MAP.get('j', 'j')
@@ -639,10 +643,11 @@ function! PasteIntoLastVisualSelection(...)
639
643
  endfunction
640
644
 
641
645
  function! VimLM(...) range
642
- let tree_file = s:watched_dir . '/tree'
643
- while filereadable(tree_file)
646
+ let wip_file = s:watched_dir . '/wip'
647
+ while filereadable(wip_file)
644
648
  sleep 100m
645
649
  endwhile
650
+ call writefile([], wip_file, 'w')
646
651
  let user_input = join(a:000, ' ')
647
652
  if empty(user_input)
648
653
  echo "Usage: :VimLM <prompt> [!command1] [!command2] ..."
@@ -656,12 +661,61 @@ function! VimLM(...) range
656
661
  let user_file = s:watched_dir . '/user'
657
662
  call writefile([user_input], user_file, 'w')
658
663
  let current_file = expand('%:p')
664
+ let tree_file = s:watched_dir . '/tree'
659
665
  call writefile([current_file], tree_file, 'w')
660
666
  call ScrollToTop()
661
667
  endfunction
662
668
 
669
+ function! SplitAtCursorInInsert()
670
+ let pos = getcurpos()
671
+ let line_num = pos[1]
672
+ let col = pos[2]
673
+ let lines = getline(1, '$')
674
+ let current_line = lines[line_num - 1]
675
+ let prefix_lines = lines[0:line_num - 2]
676
+ let prefix_part = strpart(current_line, 0, col - 1)
677
+ if !empty(prefix_part) || col > 1
678
+ call add(prefix_lines, prefix_part)
679
+ endif
680
+ let suffix_lines = []
681
+ let suffix_part = strpart(current_line, col - 1)
682
+ if !empty(suffix_part)
683
+ call add(suffix_lines, suffix_part)
684
+ endif
685
+ if line_num < len(lines)
686
+ call extend(suffix_lines, lines[line_num:])
687
+ endif
688
+ call writefile(prefix_lines, s:watched_dir . '/context', 'b')
689
+ call writefile(suffix_lines, s:watched_dir . '/yank', 'b')
690
+ call writefile([], s:watched_dir . '/fim', 'w')
691
+ call writefile([], s:watched_dir . '/user', 'w')
692
+ let current_file = expand('%:p')
693
+ let tree_file = s:watched_dir . '/tree'
694
+ call writefile([current_file], tree_file, 'w')
695
+ endfunction
696
+
697
+ function! InsertResponse()
698
+ let response_path = s:watched_dir . '/response.md'
699
+ if !filereadable(response_path)
700
+ echoerr "Response file not found: " . response_path
701
+ return
702
+ endif
703
+ let content = readfile(response_path)
704
+ let text = join(content, "\n")
705
+ call setreg('z', text)
706
+ let col = col('.')
707
+ let line = getline('.')
708
+ if col == len(line) + 1
709
+ normal! "zgpa
710
+ else
711
+ normal! "zgPa
712
+ endif
713
+ endfunction
714
+
663
715
  command! ToggleVimLM call ToggleVimLM()
664
716
  command! -range -nargs=+ VimLM call VimLM(<f-args>)
717
+ inoremap <silent> <C-l> <C-\><C-o>:call SplitAtCursorInInsert()<CR>
718
+ inoremap <silent> <C-p> <C-\><C-o>:call InsertResponse()<CR>
665
719
  nnoremap $mapp :call PasteIntoLastVisualSelection()<CR>
666
720
  vnoremap $mapp <Cmd>:call PasteIntoLastVisualSelection()<CR>
667
721
  vnoremap $mapl <Cmd>:call VisualPrompt()<CR>
@@ -670,19 +724,13 @@ nnoremap $mapj :call FollowUpPrompt()<CR>
670
724
  call Monitor()
671
725
  """).safe_substitute(dict(WATCH_DIR=WATCH_DIR, mapl=mapl, mapj=mapj, mapp=mapp))
672
726
 
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
727
+ async def main(args):
680
728
  with tempfile.NamedTemporaryFile(mode='w', suffix='.vim', delete=False) as f:
681
729
  f.write(VIMLMSCRIPT)
682
730
  vim_script = f.name
683
731
  vim_command = ["vim", "-c", f"source {vim_script}"]
684
- if args.vim_args:
685
- vim_command.extend(args.vim_args)
732
+ if args.args_vim:
733
+ vim_command.extend(args.args_vim)
686
734
  else:
687
735
  vim_command.append('.tmp')
688
736
  try:
@@ -697,8 +745,78 @@ async def main():
697
745
  pass
698
746
  os.remove(vim_script)
699
747
 
748
+ def get_common_dir_and_children(file_paths):
749
+ dirs = [os.path.dirname(path) for path in file_paths]
750
+ dir_parts = [path.split(os.sep) for path in dirs]
751
+ common_parts = []
752
+ for parts in zip(*dir_parts):
753
+ if all(part == parts[0] for part in parts):
754
+ common_parts.append(parts[0])
755
+ else:
756
+ break
757
+ parent_path = os.sep.join(common_parts)
758
+ child_paths = [os.path.relpath(path, parent_path) for path in file_paths]
759
+ repo_name = os.path.basename(os.path.dirname(parent_path))
760
+ return repo_name, parent_path, child_paths
761
+
762
+ def get_repo(args_repo, args_vim):
763
+ if not args_repo:
764
+ return None
765
+ vim_files = []
766
+ for arg in args_vim:
767
+ if not arg.startswith('-'):
768
+ if os.path.exists(arg):
769
+ vim_files.append(os.path.abspath(arg))
770
+ repo_files = []
771
+ rest_files = []
772
+ for pattern in args_repo:
773
+ expanded_paths = glob.glob(pattern)
774
+ if expanded_paths:
775
+ for path in expanded_paths:
776
+ if not os.path.isfile(path) or path in vim_files+repo_files or os.path.basename(path).startswith('.') or is_binary(path):
777
+ continue
778
+ if path in vim_files:
779
+ rest_files.append(os.path.abspath(path))
780
+ else:
781
+ repo_files.append(os.path.abspath(path))
782
+ repo_name, repo_path, child_paths = get_common_dir_and_children(repo_files+rest_files)
783
+ repo_names, rest_names = child_paths[:len(repo_files)], child_paths[len(repo_files):]
784
+ list_content = [f'<|repo_name|>{repo_name}\n']
785
+ list_mtime = []
786
+ for p, n in zip(repo_files, repo_names):
787
+ try:
788
+ with open(p, 'r') as f:
789
+ list_content.append(f'<|file_sep|>{n}\n{f.read()}\n')
790
+ list_mtime.append(int(os.path.getmtime(p)))
791
+ except Exception as e:
792
+ tolog(f'Skipped {p} d/t {e}', 'get_repo()')
793
+ 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)
794
+
700
795
  def run():
701
- asyncio.run(main())
796
+ parser = argparse.ArgumentParser(description="VimLM - LLM-powered Vim assistant")
797
+ parser.add_argument('--test', action='store_true', help="Run in test mode")
798
+ parser.add_argument('args_vim', nargs='*', help="Vim arguments")
799
+ parser.add_argument('--repo', nargs='*', help="Paths to directories or files (e.g., assets/*, path/to/file)")
800
+ args = parser.parse_args()
801
+ dict_repo = get_repo(args.repo, args.args_vim)
802
+ tolog(dict_repo, 'get_repo()')
803
+ if args.test:
804
+ return
805
+ reset_dir(WATCH_DIR)
806
+ toout('Loading LLM...')
807
+ if LLM_MODEL is None:
808
+ globals()['chat'] = nanollama.Chat(model_path='uncn_llama_32_3b_it')
809
+ toout(f'LLM is ready')
810
+ else:
811
+ globals()['chat'] = mlx_lm_utils.Chat(model_path=LLM_MODEL, think=THINK)
812
+ toout(f'{LLM_MODEL.split('/')[-1]} is ready')
813
+ if FIM_MODEL and FIM_MODEL != LLM_MODEL:
814
+ globals()['fim'] = mlx_lm_utils.Chat(model_path=FIM_MODEL, cache_dir=VIMLM_DIR, dict_repo=dict_repo)
815
+ toout(f'\n\n{FIM_MODEL.split("/")[-1]} is ready', mode='a')
816
+ else:
817
+ globals()['fim'] = chat
818
+ chat.set_cache_repo(dict_repo, cache_dir=VIMLM_DIR)
819
+ asyncio.run(main(args))
702
820
 
703
821
  if __name__ == '__main__':
704
822
  run()
@@ -1,3 +0,0 @@
1
- nanollama>=0.0.5
2
- mlx_lm_utils>=0.0.3
3
- watchfiles==1.0.4
File without changes
File without changes
File without changes