vimlm 0.0.9__py3-none-any.whl → 0.1.1__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: vimlm
3
- Version: 0.0.9
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.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,46 @@ 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
+ | `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
@@ -0,0 +1,7 @@
1
+ vimlm.py,sha256=C8b9W3HthJhSF5tU5YNK7E26R-7OZJwsezOINSzV_28,33956
2
+ vimlm-0.1.1.dist-info/LICENSE,sha256=f1xgK8fAXg_intwnbc9nLkHf7ODPLtgpHs7DetQHOro,11343
3
+ vimlm-0.1.1.dist-info/METADATA,sha256=jjtEcKF0tKJseHGLEQ83AOuz1UC72QG4JVTE3vUaGYM,6359
4
+ vimlm-0.1.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
5
+ vimlm-0.1.1.dist-info/entry_points.txt,sha256=mU5V4MYsuIzCc6YB-Ro-6USSHWN5vHw8UDnTEoq0isw,36
6
+ vimlm-0.1.1.dist-info/top_level.txt,sha256=I8GjqoiP--scYsO3AfLhha-6Ax9ci3IvbWvVbPv8g94,6
7
+ vimlm-0.1.1.dist-info/RECORD,,
vimlm.py CHANGED
@@ -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 = None, # "mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit"
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
- VERSION = '0.0.9',
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("~/vimlm")
43
- WATCH_DIR = os.path.expanduser("~/vimlm/watch_dir")
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 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)
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 key == 'debug':
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 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.'
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'], mode='a')
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().strip()
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 tree_file = s:watched_dir . '/tree'
643
- while filereadable(tree_file)
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.vim_args:
685
- vim_command.extend(args.vim_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
- asyncio.run(main())
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()
@@ -1,7 +0,0 @@
1
- vimlm.py,sha256=n0rCiqHJQ1ouKZD-0C9rcE6rAC4d1bVriKTx5303Dmo,25180
2
- vimlm-0.0.9.dist-info/LICENSE,sha256=f1xgK8fAXg_intwnbc9nLkHf7ODPLtgpHs7DetQHOro,11343
3
- vimlm-0.0.9.dist-info/METADATA,sha256=xhXHiDlYY44vN12JgbsZHcgRJDy01-hDOCcm05ZQHng,4942
4
- vimlm-0.0.9.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
5
- vimlm-0.0.9.dist-info/entry_points.txt,sha256=mU5V4MYsuIzCc6YB-Ro-6USSHWN5vHw8UDnTEoq0isw,36
6
- vimlm-0.0.9.dist-info/top_level.txt,sha256=I8GjqoiP--scYsO3AfLhha-6Ax9ci3IvbWvVbPv8g94,6
7
- vimlm-0.0.9.dist-info/RECORD,,
File without changes
File without changes