vimlm 0.0.6__py3-none-any.whl → 0.0.8__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.
@@ -0,0 +1,169 @@
1
+ Metadata-Version: 2.2
2
+ Name: vimlm
3
+ Version: 0.0.8
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 - AI-Powered Coding Assistant for Vim
25
+
26
+ ![VimLM Demo](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/captioned_vimlm.gif)
27
+
28
+ VimLM brings the power of AI directly into your Vim workflow. Maintain focus with keyboard-driven interactions while leveraging AI for code generation, refactoring, and documentation.
29
+
30
+ Get started quickly with the [tutorial](tutorial.md).
31
+
32
+ ## Features
33
+ - **Native Vim Integration** - Split-window responses & intuitive keybindings
34
+ - **Offline First** - 100% local execution with MLX-compatible models
35
+ - **Contextual Awareness** - Integrates seamlessly with your codebase and external resources
36
+ - **Conversational Workflow** - Iterate on responses with follow-up queries
37
+ - **Project Scaffolding** - Generate and deploy code blocks to directories
38
+ - **Extensible** - Create custom LLM workflows with command chains
39
+
40
+ ## Requirements
41
+ - Apple Silicon (M-series)
42
+ - Python 3.12.8
43
+ - Vim 9.1
44
+
45
+ ## Quick Start
46
+ ```bash
47
+ pip install vimlm
48
+ vimlm
49
+ ```
50
+
51
+ ## Basic Usage
52
+
53
+ | Key Binding | Mode | Action |
54
+ |-------------|---------------|----------------------------------------|
55
+ | `Ctrl-l` | Normal/Visual | Prompt LLM |
56
+ | `Ctrl-j` | Normal | Continue conversation |
57
+ | `Ctrl-p` | Normal/Visual | Import generated code |
58
+ | `Esc` | Prompt | Cancel input |
59
+
60
+ ### 1. **Contextual Prompting**
61
+ `Ctrl-l` to prompt LLM with context:
62
+ - Normal mode: Current file + line
63
+ - Visual mode: Current file + selected block
64
+
65
+ *Example Prompt*: `Create a Chrome extension`
66
+
67
+ ### 2. **Conversational Refinement**
68
+ `Ctrl-j` to continue current thread.
69
+
70
+ *Example Prompt*: `Use manifest V3 instead`
71
+
72
+ ### 3. **Code Substitution**
73
+ `Ctrl-p` to insert generated code block
74
+ - In Normal mode: Into last visual selection
75
+ - In Visual mode: Into current visual selection
76
+
77
+ *Example Workflow*:
78
+ 1. Select a block of code in Visual mode
79
+ 2. Prompt with `Ctrl-l`: `Use regex to remove html tags from item.content`
80
+ 3. Press `Ctrl-p` to replace selection with generated code
81
+
82
+ ## Inline Directives
83
+ ```text
84
+ :VimLM [PROMPT] [!command1] [!command2]...
85
+ ```
86
+
87
+ `!` prefix to embed inline directives in prompts:
88
+
89
+ | Directive | Description |
90
+ |------------------|------------------------------------------|
91
+ | `!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 |
95
+
96
+ ### 1. **Context Layering**
97
+ ```text
98
+ !include [PATH] # Add files/folders to context
99
+ ```
100
+ - **`!include`** (no path): Current folder
101
+ - **`!include ~/projects/utils.py`**: Specific file
102
+ - **`!include ~/docs/api-specs/`**: Entire folder
103
+ - **`!include $(...)`**: Shell command output
104
+
105
+ *Example*: `Summarize recent changes !include $(git log --oneline -n 50)`
106
+
107
+ ### 2. **Code Deployment**
108
+ ```text
109
+ !deploy [DEST_DIR] # Extract code blocks to directory
110
+ ```
111
+ - **`!deploy`** (no path): Current directory
112
+ - **`!deploy ./src`**: Specific directory
113
+
114
+ *Example:* `Create REST API endpoint !deploy ./api`
115
+
116
+ ### 3. **Extending Response**
117
+ ```text
118
+ !continue [MAX_TOKENS] # Continue stopped response
119
+ ```
120
+ - **`!continue`**: Default 2000 tokens
121
+ - **`!continue 3000`**: Custom token limit
122
+
123
+ *Example:* `tl;dr !include large-file.txt !continue 5000`
124
+
125
+ ## Command-Line Mode
126
+ ```vim
127
+ :VimLM prompt [!command1] [!command2]...
128
+ ```
129
+
130
+ Simplify complex tasks by chaining multiple commands together into a single, reusable Vim command.
131
+
132
+ *Examples*:
133
+ ```vim
134
+ " Debug CI failures using error logs
135
+ :VimLM Fix Dockerfile !include .gitlab-ci.yml !include $(tail -n 20 ci.log)
136
+
137
+ " Generate unit tests for selected functions and save to test/
138
+ :VimLM Write pytest tests for this !include ./src !deploy ./test
139
+
140
+ " Add docstrings to all Python functions in file
141
+ :VimLM Add Google-style docstrings !include % !continue 4000
142
+ ```
143
+
144
+ ## Configuration
145
+
146
+ ### 1. **Model Settings**
147
+ Edit `~/vimlm/cfg.json`:
148
+ ```json
149
+ {
150
+ "LLM_MODEL": "mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit",
151
+ "NUM_TOKEN": 32768
152
+ }
153
+ ```
154
+
155
+ ### 2. **Key Customization**
156
+ ```json
157
+ {
158
+ "USE_LEADER": true,
159
+ "KEY_MAP": {
160
+ "l": "]",
161
+ "j": "[",
162
+ "p": "p"
163
+ }
164
+ }
165
+ ```
166
+
167
+ ## License
168
+
169
+ Apache 2.0 - See [LICENSE](LICENSE) for details.
@@ -0,0 +1,7 @@
1
+ vimlm.py,sha256=Y_WXPK_ta1vKTpbLo4PvO2F4E_puXmo7IFMlyqKELrM,25181
2
+ vimlm-0.0.8.dist-info/LICENSE,sha256=f1xgK8fAXg_intwnbc9nLkHf7ODPLtgpHs7DetQHOro,11343
3
+ vimlm-0.0.8.dist-info/METADATA,sha256=M67hbla7u7UaTt-mILUia0tPEmByk1znzavFI5Bf_VA,4942
4
+ vimlm-0.0.8.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
5
+ vimlm-0.0.8.dist-info/entry_points.txt,sha256=mU5V4MYsuIzCc6YB-Ro-6USSHWN5vHw8UDnTEoq0isw,36
6
+ vimlm-0.0.8.dist-info/top_level.txt,sha256=I8GjqoiP--scYsO3AfLhha-6Ax9ci3IvbWvVbPv8g94,6
7
+ vimlm-0.0.8.dist-info/RECORD,,
vimlm.py CHANGED
@@ -18,7 +18,7 @@ import json
18
18
  import os
19
19
  from watchfiles import awatch
20
20
  import shutil
21
- import time
21
+ from datetime import datetime
22
22
  from itertools import accumulate
23
23
  import argparse
24
24
  import tempfile
@@ -26,13 +26,19 @@ from pathlib import Path
26
26
  from string import Template
27
27
  import re
28
28
 
29
- DEBUG = True
30
- LLM_MODEL = None # "mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit"
31
- NUM_TOKEN = 2000
32
- SEP_CMD = '!'
33
- USE_LEADER = False
34
- DO_RESET = True
35
- SHOW_USER = False
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.8',
38
+ DEBUG = False,
39
+ )
40
+
41
+ DATE_FORM = "%Y_%m_%d_%H_%M_%S"
36
42
  VIMLM_DIR = os.path.expanduser("~/vimlm")
37
43
  WATCH_DIR = os.path.expanduser("~/vimlm/watch_dir")
38
44
  CFG_FILE = 'cfg.json'
@@ -45,6 +51,36 @@ LOG_PATH = os.path.join(VIMLM_DIR, LOG_FILE)
45
51
  LTM_PATH = os.path.join(VIMLM_DIR, LTM_FILE)
46
52
  OUT_PATH = os.path.join(WATCH_DIR, OUT_FILE)
47
53
 
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)
83
+
48
84
  def toout(s, key=None, mode=None):
49
85
  key = '' if key is None else ':'+key
50
86
  mode = 'w' if mode is None else mode
@@ -60,26 +96,21 @@ def tolog(log, key='debug'):
60
96
  logs = json.load(log_f)
61
97
  except:
62
98
  logs = []
63
- logs.append(dict(key=key, log=log, timestamp=time.ctime()))
99
+ logs.append(dict(key=key, log=log, timestamp=datetime.now().strftime(DATE_FORM)))
64
100
  with open(LOG_PATH, "w", encoding="utf-8") as log_f:
65
101
  json.dump(logs, log_f, indent=2)
66
102
 
67
- if os.path.exists(WATCH_DIR):
68
- shutil.rmtree(WATCH_DIR)
69
- os.makedirs(WATCH_DIR)
70
-
71
- try:
72
- with open(CFG_PATH, "r") as f:
73
- config = json.load(f)
74
- DEBUG = config.get("DEBUG", DEBUG)
75
- LLM_MODEL = config.get("LLM_MODEL", LLM_MODEL)
76
- NUM_TOKEN = config.get("NUM_TOKEN", NUM_TOKEN)
77
- SEP_CMD = config.get("SEP_CMD", SEP_CMD)
78
- USE_LEADER = config.get("USE_LEADER", USE_LEADER)
79
- except Exception as e:
80
- tolog(str(e))
81
- with open(CFG_PATH, 'w') as f:
82
- json.dump(dict(DEBUG=DEBUG, LLM_MODEL=LLM_MODEL, NUM_TOKEN=NUM_TOKEN, SEP_CMD=SEP_CMD, USE_LEADER=USE_LEADER), f, indent=2)
103
+ def print_log():
104
+ with open(LOG_PATH, 'r') as f:
105
+ logs = json.load(f)
106
+ for log in logs:
107
+ print(f'\033[37m{log["key"]} {log["timestamp"]}\033[0m')
108
+ if 'tovim' in log["key"]:
109
+ print('\033[33m')
110
+ elif 'tollm' in log["key"]:
111
+ print('\033[31m')
112
+ print(log["log"])
113
+ print('\033[0m')
83
114
 
84
115
  toout('Loading LLM...')
85
116
  if LLM_MODEL is None:
@@ -99,6 +130,7 @@ def deploy(dest=None, src=None, reformat=True):
99
130
  with open(src, 'r') as f:
100
131
  prompt_deploy = f.read().strip() + '\n\n---\n\n' + prompt_deploy
101
132
  if reformat:
133
+ toout('Deploying...')
102
134
  response = chat(prompt_deploy, max_new=NUM_TOKEN, verbose=False, stream=False)['text']
103
135
  toout(response, 'deploy')
104
136
  lines = response.splitlines()
@@ -326,19 +358,41 @@ def process_command(data):
326
358
  data['include'] = ''
327
359
  for cmd in cmds:
328
360
  if cmd.startswith('include'):
329
- arg = cmd.removeprefix('include').strip('(').strip(')').strip().strip('"').strip("'").strip()
361
+ arg = cmd.removeprefix('include').strip().strip('(').strip(')').strip().strip('"').strip("'").strip()
330
362
  src = data['dir'] if len(arg) == 0 else arg
331
- data['include'] += ingest(src)
363
+ if arg == '%':
364
+ continue
365
+ if src.startswith('`') or src.startswith('$('):
366
+ shell_cmd = src.strip('`') if src.startswith('`') else src.strip('$()')
367
+ shell_cmd = shell_cmd.strip()
368
+ try:
369
+ result = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True)
370
+ if result.returncode == 0:
371
+ data['include'] += f'--- **{shell_cmd}** ---\n```\n{result.stdout.strip()}\n```\n---\n\n'
372
+ else:
373
+ tolog(f'{shell_cmd} failed {result.stderr.strip()}')
374
+ except Exception as e:
375
+ tolog(f'Error executing {shell_cmd}: {e}')
376
+ else:
377
+ data['include'] += ingest(src)
332
378
 
333
379
  for cmd in cmds:
334
380
  if cmd.startswith('deploy'):
335
- arg = cmd.removeprefix('deploy').strip('(').strip(')').strip().strip('"').strip("'").strip()
381
+ arg = cmd.removeprefix('deploy').strip().strip('(').strip(')').strip().strip('"').strip("'").strip()
336
382
  if len(data['user_prompt']) == 0:
337
383
  deploy(dest=arg)
338
384
  data['user_prompt'] = ''
339
385
  return data
340
386
  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."
341
387
  data['deploy_dest'] = arg
388
+ for cmd in cmds:
389
+ if cmd.startswith('write'):
390
+ arg = cmd.removeprefix('write').strip().strip('(').strip(')').strip().strip('"').strip("'").strip()
391
+ if len(arg) == 0:
392
+ arg = 'response'
393
+ pass
394
+ timestamp = datetime.now().strftime(DATE_FORM)
395
+ data['write_dest'] = re.sub(r"[^a-zA-Z0-9_.-]", "", f'{arg}_{timestamp}.md')
342
396
  return data
343
397
 
344
398
  async def monitor_directory():
@@ -391,15 +445,57 @@ async def process_files(data):
391
445
  toout(response['text'])
392
446
  else:
393
447
  toout(response['text'])
394
- tolog(response['benchmark'])
448
+ tolog(response)
449
+ if 'write_dest' in data:
450
+ with open(data['write_dest'], 'w') as f:
451
+ f.write(response['text'])
395
452
  if 'deploy_dest' in data:
396
453
  deploy(dest=data['deploy_dest'], reformat=False)
397
454
 
398
- mapl, mapj, mapp = ('<Leader>l', '<Leader>j', '<Leader>p') if USE_LEADER else ('<C-l>', '<C-j>', '<C-p>')
399
-
455
+ KEYL = KEY_MAP.get('l', 'l')
456
+ KEYJ = KEY_MAP.get('j', 'j')
457
+ KEYP = KEY_MAP.get('p', 'p')
458
+ 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}>')
400
459
  VIMLMSCRIPT = Template(r"""
401
460
  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']
402
461
  let s:watched_dir = expand('$WATCH_DIR')
462
+ let s:vimlm_enabled = 1
463
+
464
+ function! ToggleVimLM()
465
+ if s:vimlm_enabled
466
+ let s:vimlm_enabled = 0
467
+ let response_path = s:watched_dir . '/response.md'
468
+ let bufnum = bufnr(response_path)
469
+ let winid = bufwinnr(bufnum)
470
+ if winid != -1
471
+ execute winid . 'wincmd c'
472
+ endif
473
+ if exists('s:monitor_timer')
474
+ call timer_stop(s:monitor_timer)
475
+ unlet s:monitor_timer
476
+ endif
477
+ echohl WarningMsg | echom "VimLM disabled" | echohl None
478
+ else
479
+ let s:vimlm_enabled = 1
480
+ silent! call Monitor()
481
+ echohl WarningMsg | echom "VimLM enabled" | echohl None
482
+ endif
483
+ endfunction
484
+
485
+ function! CheckForUpdates(timer)
486
+ if !s:vimlm_enabled
487
+ return
488
+ endif
489
+ let bufnum = bufnr(s:watched_dir . '/response.md')
490
+ let winid = bufwinnr(bufnum)
491
+ if winid == -1
492
+ call timer_stop(s:monitor_timer)
493
+ unlet s:monitor_timer
494
+ call Monitor()
495
+ else
496
+ silent! checktime
497
+ endif
498
+ endfunction
403
499
 
404
500
  function! Monitor()
405
501
  if exists('s:monitor_timer')
@@ -422,13 +518,16 @@ function! Monitor()
422
518
  let s:monitor_timer = timer_start(100, 'CheckForUpdates', {'repeat': -1})
423
519
  endfunction
424
520
 
425
- function! CheckForUpdates(timer)
521
+ function! ScrollToTop()
426
522
  let bufnum = bufnr(s:watched_dir . '/response.md')
427
- if bufnum == -1
428
- call timer_stop(s:monitor_timer)
429
- return
523
+ if bufnum != -1
524
+ let winid = bufwinnr(bufnum)
525
+ if winid > 0
526
+ execute winid . "wincmd w"
527
+ normal! gg
528
+ wincmd p
529
+ endif
430
530
  endif
431
- silent! checktime
432
531
  endfunction
433
532
 
434
533
  function! s:CustomInput(prompt) abort
@@ -452,14 +551,13 @@ function! SaveUserInput(prompt)
452
551
  let current_file = expand('%:p')
453
552
  let tree_file = s:watched_dir . '/tree'
454
553
  call writefile([current_file], tree_file, 'w')
554
+ call ScrollToTop()
455
555
  endfunction
456
556
 
457
557
  function! VisualPrompt()
458
558
  silent! execute "normal! \<ESC>"
459
559
  silent execute "'<,'>w! " . s:watched_dir . "/yank"
460
560
  silent execute "w! " . s:watched_dir . "/context"
461
- " silent! execute "normal! `<V`>"
462
- " silent! execute "normal! \<ESC>"
463
561
  call SaveUserInput('VimLM: ')
464
562
  endfunction
465
563
 
@@ -467,7 +565,6 @@ function! NormalPrompt()
467
565
  silent! execute "normal! V\<ESC>"
468
566
  silent execute "'<,'>w! " . s:watched_dir . "/yank"
469
567
  silent execute "w! " . s:watched_dir . "/context"
470
- " silent! execute "normal! \<ESC>"
471
568
  call SaveUserInput('VimLM: ')
472
569
  endfunction
473
570
 
@@ -515,10 +612,14 @@ function! ExtractAllCodeBlocks()
515
612
  return len(code_blocks)
516
613
  endfunction
517
614
 
518
- function! PasteIntoLastVisualSelection()
615
+ function! PasteIntoLastVisualSelection(...)
519
616
  let num_blocks = ExtractAllCodeBlocks()
520
- echo "Extracted " . num_blocks . " blocks into registers @a-@" . s:register_names[num_blocks - 1] . ". Enter register name: "
521
- let register_name = nr2char(getchar())
617
+ if a:0 > 0
618
+ let register_name = a:1
619
+ else
620
+ echo "Extracted " . num_blocks . " blocks into registers @a-@" . s:register_names[num_blocks - 1] . ". Enter register name: "
621
+ let register_name = nr2char(getchar())
622
+ endif
522
623
  if register_name !~ '^[a-z]$'
523
624
  echoerr "Invalid register name. Please enter a single lowercase letter (e.g., a, b, c)."
524
625
  return
@@ -537,6 +638,30 @@ function! PasteIntoLastVisualSelection()
537
638
  endif
538
639
  endfunction
539
640
 
641
+ function! VimLM(...) range
642
+ let tree_file = s:watched_dir . '/tree'
643
+ while filereadable(tree_file)
644
+ sleep 100m
645
+ endwhile
646
+ let user_input = join(a:000, ' ')
647
+ if empty(user_input)
648
+ echo "Usage: :VimLM <prompt> [!command1] [!command2] ..."
649
+ return
650
+ endif
651
+ if line("'<") == line("'>")
652
+ silent! execute "normal! V\<ESC>"
653
+ endif
654
+ silent execute "'<,'>w! " . s:watched_dir . "/yank"
655
+ silent execute "w! " . s:watched_dir . "/context"
656
+ let user_file = s:watched_dir . '/user'
657
+ call writefile([user_input], user_file, 'w')
658
+ let current_file = expand('%:p')
659
+ call writefile([current_file], tree_file, 'w')
660
+ call ScrollToTop()
661
+ endfunction
662
+
663
+ command! ToggleVimLM call ToggleVimLM()
664
+ command! -range -nargs=+ VimLM call VimLM(<f-args>)
540
665
  nnoremap $mapp :call PasteIntoLastVisualSelection()<CR>
541
666
  vnoremap $mapp <Cmd>:call PasteIntoLastVisualSelection()<CR>
542
667
  vnoremap $mapl <Cmd>:call VisualPrompt()<CR>
@@ -551,7 +676,6 @@ async def main():
551
676
  parser.add_argument("vim_args", nargs=argparse.REMAINDER, help="Vim arguments")
552
677
  args = parser.parse_args()
553
678
  if args.test:
554
- test()
555
679
  return
556
680
  with tempfile.NamedTemporaryFile(mode='w', suffix='.vim', delete=False) as f:
557
681
  f.write(VIMLMSCRIPT)
@@ -1,158 +0,0 @@
1
- Metadata-Version: 2.2
2
- Name: vimlm
3
- Version: 0.0.6
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
- ![vimlm](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/captioned_vimlm.gif)
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
- Install:
50
-
51
- ```zsh
52
- pip install vimlm
53
- ```
54
-
55
- Launch:
56
-
57
- ```zsh
58
- vimlm
59
- ```
60
-
61
- or
62
-
63
- ```zsh
64
- vimlm path/to/your_file
65
- ```
66
-
67
- This launches Vim with the LLM in a split window, ready to assist you.
68
-
69
- ## Basic Usage
70
-
71
- 1. **From Normal Mode**:
72
- - `Ctrl-l`: Adds current line + file to context
73
- - Example prompt: "Regex for removing html tags from item.content"
74
-
75
- 2. **From Visual Mode**:
76
- - Select code → `Ctrl-l`: Adds selected block + current file to context
77
- - Example prompt: "Convert this to async/await syntax"
78
-
79
- 3. **Inline Commands**:
80
-
81
- !include: Adds specified outside files/folders to context:
82
- - `!include` (no path): Current folder
83
- - `!include ~/scrap/jph00/hypermedia-applications.summ.md`: Specific folder
84
- - `!include ~/wtm/utils.py`: Specific file
85
- - Example prompt: "AJAX-ify this app @ ~/scrap/jph00/hypermedia-applications.summ.md"
86
-
87
- !deploy: Extract code blocks to files in user specified dir (current dir if none specified).
88
-
89
- !continue: Lets the LLM resume the generation from where it had halted due to length limits.
90
-
91
- !followup: Continue the thread (equivalent to `Ctrl-j`
92
-
93
- 4. **Follow-Up**: After initial response:
94
- - `Ctrl-j`: Continue thread
95
- - Example follow-up: "In Manifest V3"
96
-
97
- 4. **Code Extraction: Press `Ctrl-p` to choose a code block from the response and insert them into:
98
- - The last selected visual block (in Normal mode)
99
- - The current selection (in Visual mode)
100
- - Example workflow:
101
- 1. Select a block of code in Visual mode.
102
- 2. Prompt the LLM with `Ctrl-l` (e.g., "Convert this to async/await syntax").
103
- 3. Once the response is generated, press `Ctrl-p` to replace the selected block with the extracted code.
104
-
105
- ### Key Bindings
106
-
107
- | Binding | Mode | Action |
108
- |------------|---------------|----------------------------------------|
109
- | `Ctrl-l` | Normal/Visual | Send current file + selection to LLM |
110
- | `Ctrl-j` | Normal | Continue conversation |
111
- | `Ctrl-p` | Normal/Visual | Replace the selection with generated code |
112
- | `Esc` | Prompt | Cancel input |
113
-
114
- ## Advanced Configuration
115
-
116
- VimLM uses a JSON config file with the following configurable parameters:
117
-
118
- ```json
119
- {
120
- "DEBUG": true,
121
- "LLM_MODEL": null,
122
- "NUM_TOKEN": 2000,
123
- "SEP_CMD": "!",
124
- "USE_LEADER": false
125
- }
126
- ```
127
-
128
- ### Custom Model Setup
129
-
130
- 1. **Browse models**: [MLX Community Models on Hugging Face](https://huggingface.co/mlx-community)
131
-
132
- 2. **Edit config file**:
133
-
134
- ```json
135
- {
136
- "LLM_MODEL": "mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit"
137
- }
138
- ```
139
- 3. **Save to**:
140
-
141
- ```
142
- ~/vimlm/config.json
143
- ```
144
-
145
- 4. **Restart VimLM**
146
-
147
-
148
- ### Custom Keybinding
149
-
150
- If you prefer using `<Leader>` in place of `<Ctrl>` for the ViMLM key bindings:
151
-
152
- ```json
153
- {
154
- "USER_LEADER": true
155
- }
156
- ```
157
-
158
-
@@ -1,7 +0,0 @@
1
- vimlm.py,sha256=_2EKVmfvM-Ikf48XzaxvC0rcLCxiHO4f6_fCslAqyCo,21181
2
- vimlm-0.0.6.dist-info/LICENSE,sha256=f1xgK8fAXg_intwnbc9nLkHf7ODPLtgpHs7DetQHOro,11343
3
- vimlm-0.0.6.dist-info/METADATA,sha256=INHaQDN1Id04li0LNWmlkVCIEmnKi9ABpbhAYFGokYE,4233
4
- vimlm-0.0.6.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
5
- vimlm-0.0.6.dist-info/entry_points.txt,sha256=mU5V4MYsuIzCc6YB-Ro-6USSHWN5vHw8UDnTEoq0isw,36
6
- vimlm-0.0.6.dist-info/top_level.txt,sha256=I8GjqoiP--scYsO3AfLhha-6Ax9ci3IvbWvVbPv8g94,6
7
- vimlm-0.0.6.dist-info/RECORD,,
File without changes
File without changes