[
  {
    "path": "LICENSE",
    "content": "\n                                 Apache License\n                           Version 2.0, January 2004\n                        http://www.apache.org/licenses/\n\n   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION\n\n   1. Definitions.\n\n      \"License\" shall mean the terms and conditions for use, reproduction,\n      and distribution as defined by Sections 1 through 9 of this document.\n\n      \"Licensor\" shall mean the copyright owner or entity authorized by\n      the copyright owner that is granting the License.\n\n      \"Legal Entity\" shall mean the union of the acting entity and all\n      other entities that control, are controlled by, or are under common\n      control with that entity. For the purposes of this definition,\n      \"control\" means (i) the power, direct or indirect, to cause the\n      direction or management of such entity, whether by contract or\n      otherwise, or (ii) ownership of fifty percent (50%) or more of the\n      outstanding shares, or (iii) beneficial ownership of such entity.\n\n      \"You\" (or \"Your\") shall mean an individual or Legal Entity\n      exercising permissions granted by this License.\n\n      \"Source\" form shall mean the preferred form for making modifications,\n      including but not limited to software source code, documentation\n      source, and configuration files.\n\n      \"Object\" form shall mean any form resulting from mechanical\n      transformation or translation of a Source form, including but\n      not limited to compiled object code, generated documentation,\n      and conversions to other media types.\n\n      \"Work\" shall mean the work of authorship, whether in Source or\n      Object form, made available under the License, as indicated by a\n      copyright notice that is included in or attached to the work\n      (an example is provided in the Appendix below).\n\n      \"Derivative Works\" shall mean any work, whether in Source or Object\n      form, that is based on (or derived from) the Work and for which the\n      editorial revisions, annotations, elaborations, or other modifications\n      represent, as a whole, an original work of authorship. For the purposes\n      of this License, Derivative Works shall not include works that remain\n      separable from, or merely link (or bind by name) to the interfaces of,\n      the Work and Derivative Works thereof.\n\n      \"Contribution\" shall mean any work of authorship, including\n      the original version of the Work and any modifications or additions\n      to that Work or Derivative Works thereof, that is intentionally\n      submitted to Licensor for inclusion in the Work by the copyright owner\n      or by an individual or Legal Entity authorized to submit on behalf of\n      the copyright owner. For the purposes of this definition, \"submitted\"\n      means any form of electronic, verbal, or written communication sent\n      to the Licensor or its representatives, including but not limited to\n      communication on electronic mailing lists, source code control systems,\n      and issue tracking systems that are managed by, or on behalf of, the\n      Licensor for the purpose of discussing and improving the Work, but\n      excluding communication that is conspicuously marked or otherwise\n      designated in writing by the copyright owner as \"Not a Contribution.\"\n\n      \"Contributor\" shall mean Licensor and any individual or Legal Entity\n      on behalf of whom a Contribution has been received by Licensor and\n      subsequently incorporated within the Work.\n\n   2. Grant of Copyright License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      copyright license to reproduce, prepare Derivative Works of,\n      publicly display, publicly perform, sublicense, and distribute the\n      Work and such Derivative Works in Source or Object form.\n\n   3. Grant of Patent License. Subject to the terms and conditions of\n      this License, each Contributor hereby grants to You a perpetual,\n      worldwide, non-exclusive, no-charge, royalty-free, irrevocable\n      (except as stated in this section) patent license to make, have made,\n      use, offer to sell, sell, import, and otherwise transfer the Work,\n      where such license applies only to those patent claims licensable\n      by such Contributor that are necessarily infringed by their\n      Contribution(s) alone or by combination of their Contribution(s)\n      with the Work to which such Contribution(s) was submitted. If You\n      institute patent litigation against any entity (including a\n      cross-claim or counterclaim in a lawsuit) alleging that the Work\n      or a Contribution incorporated within the Work constitutes direct\n      or contributory patent infringement, then any patent licenses\n      granted to You under this License for that Work shall terminate\n      as of the date such litigation is filed.\n\n   4. Redistribution. You may reproduce and distribute copies of the\n      Work or Derivative Works thereof in any medium, with or without\n      modifications, and in Source or Object form, provided that You\n      meet the following conditions:\n\n      (a) You must give any other recipients of the Work or\n          Derivative Works a copy of this License; and\n\n      (b) You must cause any modified files to carry prominent notices\n          stating that You changed the files; and\n\n      (c) You must retain, in the Source form of any Derivative Works\n          that You distribute, all copyright, patent, trademark, and\n          attribution notices from the Source form of the Work,\n          excluding those notices that do not pertain to any part of\n          the Derivative Works; and\n\n      (d) If the Work includes a \"NOTICE\" text file as part of its\n          distribution, then any Derivative Works that You distribute must\n          include a readable copy of the attribution notices contained\n          within such NOTICE file, excluding those notices that do not\n          pertain to any part of the Derivative Works, in at least one\n          of the following places: within a NOTICE text file distributed\n          as part of the Derivative Works; within the Source form or\n          documentation, if provided along with the Derivative Works; or,\n          within a display generated by the Derivative Works, if and\n          wherever such third-party notices normally appear. The contents\n          of the NOTICE file are for informational purposes only and\n          do not modify the License. You may add Your own attribution\n          notices within Derivative Works that You distribute, alongside\n          or as an addendum to the NOTICE text from the Work, provided\n          that such additional attribution notices cannot be construed\n          as modifying the License.\n\n      You may add Your own copyright statement to Your modifications and\n      may provide additional or different license terms and conditions\n      for use, reproduction, or distribution of Your modifications, or\n      for any such Derivative Works as a whole, provided Your use,\n      reproduction, and distribution of the Work otherwise complies with\n      the conditions stated in this License.\n\n   5. Submission of Contributions. Unless You explicitly state otherwise,\n      any Contribution intentionally submitted for inclusion in the Work\n      by You to the Licensor shall be under the terms and conditions of\n      this License, without any additional terms or conditions.\n      Notwithstanding the above, nothing herein shall supersede or modify\n      the terms of any separate license agreement you may have executed\n      with Licensor regarding such Contributions.\n\n   6. Trademarks. This License does not grant permission to use the trade\n      names, trademarks, service marks, or product names of the Licensor,\n      except as required for reasonable and customary use in describing the\n      origin of the Work and reproducing the content of the NOTICE file.\n\n   7. Disclaimer of Warranty. Unless required by applicable law or\n      agreed to in writing, Licensor provides the Work (and each\n      Contributor provides its Contributions) on an \"AS IS\" BASIS,\n      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or\n      implied, including, without limitation, any warranties or conditions\n      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A\n      PARTICULAR PURPOSE. You are solely responsible for determining the\n      appropriateness of using or redistributing the Work and assume any\n      risks associated with Your exercise of permissions under this License.\n\n   8. Limitation of Liability. In no event and under no legal theory,\n      whether in tort (including negligence), contract, or otherwise,\n      unless required by applicable law (such as deliberate and grossly\n      negligent acts) or agreed to in writing, shall any Contributor be\n      liable to You for damages, including any direct, indirect, special,\n      incidental, or consequential damages of any character arising as a\n      result of this License or out of the use or inability to use the\n      Work (including but not limited to damages for loss of goodwill,\n      work stoppage, computer failure or malfunction, or any and all\n      other commercial damages or losses), even if such Contributor\n      has been advised of the possibility of such damages.\n\n   9. Accepting Warranty or Additional Liability. While redistributing\n      the Work or Derivative Works thereof, You may choose to offer,\n      and charge a fee for, acceptance of support, warranty, indemnity,\n      or other liability obligations and/or rights consistent with this\n      License. However, in accepting such obligations, You may act only\n      on Your own behalf and on Your sole responsibility, not on behalf\n      of any other Contributor, and only if You agree to indemnify,\n      defend, and hold each Contributor harmless for any liability\n      incurred by, or claims asserted against, such Contributor by reason\n      of your accepting any such warranty or additional liability.\n\n   END OF TERMS AND CONDITIONS\n\n   APPENDIX: How to apply the Apache License to your work.\n\n      To apply the Apache License to your work, attach the following\n      boilerplate notice, with the fields enclosed by brackets \"[]\"\n      replaced with your own identifying information. (Don't include\n      the brackets!)  The text should be enclosed in the appropriate\n      comment syntax for the file format. We also recommend that a\n      file or class name and description of purpose be included on the\n      same \"printed page\" as the copyright notice for easier\n      identification within third-party archives.\n\n   Copyright 2025 Josef Albers\n\n   Licensed under the Apache License, Version 2.0 (the \"License\");\n   you may not use this file except in compliance with the License.\n   You may obtain a copy of the License at\n\n       http://www.apache.org/licenses/LICENSE-2.0\n\n   Unless required by applicable law or agreed to in writing, software\n   distributed under the License is distributed on an \"AS IS\" BASIS,\n   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n   See the License for the specific language governing permissions and\n   limitations under the License.\n"
  },
  {
    "path": "README.md",
    "content": "# VimLM - AI-Powered Coding Assistant for Vim/NeoVim\n\n![VimLM Demo](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/captioned_vimlm.gif)\n\nVimLM 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.\n\nGet started quickly with the [tutorial](tutorial.md).\n\n## Features\n- **Native Vim Integration** - Split-window responses & intuitive keybindings\n- **Offline First** - 100% local execution with MLX-compatible models\n- **Contextual Awareness** - Integrates seamlessly with your codebase and external resources\n- **Conversational Workflow** - Iterate on responses with follow-up queries\n- **Project Scaffolding** - Generate and deploy code blocks to directories\n- **Extensible** - Create custom LLM workflows with command chains\n\n## Requirements\n- Apple Silicon (M-series)\n- Python v3.12.8\n- Vim v9.1 or NeoVim v0.10.4\n\n## Quick Start\n```bash\npip install vimlm\nvimlm\n```\n\n## Smart Autocomplete  \n\n### **Basic Usage**\n\n| Key Binding | Mode    | Action                                  |  \n|-------------|---------|-----------------------------------------|  \n| `Ctrl-l`    | Insert  | Generate code suggestion                |  \n| `Ctrl-p`    | Insert  | Insert generated code                   |  \n| `Ctrl-j`    | Insert  | Generate and insert code                |\n\n*Example Workflow*:  \n1. Place cursor where you need code  \n```python\ndef quicksort(arr):\n    if len(arr) <= 1:\n        return arr\n    pivot = arr[len(arr) // 2]\n    # <Cursor here>\n    middle = [x for x in arr if x == pivot]\n    right = [x for x in arr if x > pivot]\n    return quicksort(left) + middle + quicksort(right)\n```\n\n2. Use `Ctrl-j` to autocomplete\n\n### **Repository-Level Code Completion**\n\n| Option     | Description                              |\n|------------|------------------------------------------|\n| `--repo`   | Paths to include as repository context   |\n\nThe `--repo` option enhances autocomplete by providing repository-level context to the LLM.\n\n*Example Workflow*:\n1. Launch VimLM with repo context: `vimlm main.py --repo utils/*`\n2. In Insert mode, place cursor where completion is needed\n3. `Ctrl-l` to generate suggestions informed by repository context\n4. `Ctrl-p` to accept and insert the code\n\n## Conversational Assistance\n\n| Key Binding | Mode          | Action                                 |\n|-------------|---------------|----------------------------------------|\n| `Ctrl-l`    | Normal/Visual | Prompt LLM                             |\n| `Ctrl-j`    | Normal        | Continue conversation                  |\n| `Ctrl-p`    | Normal/Visual | Import generated code                  |\n| `Esc`       | Prompt        | Cancel input                           |\n\n### 1. **Contextual Prompting**\n`Ctrl-l` to prompt LLM with context:\n- Normal mode: Current file + line\n- Visual mode: Current file + selected block\n\n*Example Prompt*: `Create a Chrome extension`\n\n### 2. **Conversational Refinement**\n`Ctrl-j` to continue current thread.\n\n*Example Prompt*: `Use manifest V3 instead`\n\n### 3. **Code Substitution**\n`Ctrl-p` to insert generated code block\n- In Normal mode: Into last visual selection\n- In Visual mode: Into current visual selection \n\n*Example Workflow*:  \n1. Select a block of code in Visual mode  \n2. Prompt with `Ctrl-l`: `Use regex to remove html tags from item.content`  \n3. Press `Ctrl-p` to replace selection with generated code  \n\n## Inline Directives\n```text\n:VimLM [PROMPT] [!command1] [!command2]...\n```\n\n`!` prefix to embed inline directives in prompts:\n\n| Directive        | Description                                |\n|------------------|--------------------------------------------|\n| `!include PATH`  | Add file/directory/shell output to context |\n| `!deploy DEST`   | Save code blocks to directory              |\n| `!continue N`    | Continue stopped response                  |\n| `!followup`      | Continue conversation                      |\n\n### 1. **Context Layering**\n```text\n!include [PATH]  # Add files/folders to context\n```\n- **`!include`** (no path): Current folder  \n- **`!include ~/projects/utils.py`**: Specific file  \n- **`!include ~/docs/api-specs/`**: Entire folder  \n- **`!include $(...)`**: Shell command output\n\n*Example*: `Summarize recent changes !include $(git log --oneline -n 50)`\n\n### 2. **Code Deployment**\n```text\n!deploy [DEST_DIR]  # Extract code blocks to directory\n```\n- **`!deploy`** (no path): Current directory  \n- **`!deploy ./src`**: Specific directory  \n\n*Example:* `Create REST API endpoint !deploy ./api`\n\n### 3. **Extending Response**\n```text\n!continue [MAX_TOKENS]  # Continue stopped response\n```\n- **`!continue`**: Default 2000 tokens  \n- **`!continue 3000`**: Custom token limit  \n\n*Example:* `tl;dr !include large-file.txt !continue 5000`\n\n## Command-Line Mode\n```vim\n:VimLM prompt [!command1] [!command2]...\n```\n\nSimplify complex tasks by chaining multiple commands together into a single, reusable Vim command.\n\n*Examples*:\n```vim\n\" Debug CI failures using error logs\n:VimLM Fix Dockerfile !include .gitlab-ci.yml !include $(tail -n 20 ci.log)\n\n\" Generate unit tests for selected functions and save to test/\n:VimLM Write pytest tests for this !include ./src !deploy ./test\n\n\" Add docstrings to all Python functions in file\n:VimLM Add Google-style docstrings !include % !continue 4000\n```\n\n## Configuration\n\n### 1. **Model Settings**\nEdit `~/vimlm/cfg.json`:\n```json\n{\n  \"LLM_MODEL\": \"mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit\",\n  \"NUM_TOKEN\": 32768\n}\n```\n\n### 2. **Key Customization**\n```json\n{\n  \"USE_LEADER\": true,\n  \"KEY_MAP\": {\n    \"l\": \"]\",\n    \"j\": \"[\",\n    \"p\": \"p\" \n  }\n}\n```\n\n## License\n\nApache 2.0 - See [LICENSE](LICENSE) for details.\n"
  },
  {
    "path": "requirements.txt",
    "content": "nanollama>=0.0.6\nmlx_lm_utils>=0.0.4\nwatchfiles==1.0.4\n"
  },
  {
    "path": "setup.py",
    "content": "from setuptools import setup, find_packages\n\nwith open(\"requirements.txt\") as f:\n    requirements = [l.strip() for l in f.readlines()]\n\nsetup(\n    name=\"vimlm\",\n    version=\"0.1.2\",\n    author=\"Josef Albers\",\n    author_email=\"albersj66@gmail.com\",\n    readme='README.md',\n    description=\"VimLM - LLM-powered Vim assistant\",\n    long_description=open('README.md', encoding='utf-8').read(),\n    long_description_content_type=\"text/markdown\",\n    url=\"https://github.com/JosefAlbers/vimlm\",\n    # packages=find_packages(),\n    py_modules=['vimlm'],\n    python_requires=\">=3.12.8\",\n    install_requires=requirements,\n    entry_points={\n        \"console_scripts\": [\n            \"vimlm=vimlm:run\",\n        ],\n    },\n)\n\n"
  },
  {
    "path": "tutorial.md",
    "content": "# VimLM: Bringing AI Assistance to Vim\n\nAt their core, LLMs generate chunks of text - code snippets, explanations, refactorings - that developers need to evaluate and integrate into their projects. \n\nVim, with its efficient text manipulation and navigation capabilities, provides the perfect environment to harness the power of LLMs. Its modal design transforms editing into a fluid, keyboard-driven dialogue: yank fragments into registers for later use, leap between files with marks, or rewrite blocks with precision—all while maintaining unbroken focus. \n\nVimLM aims to seamlessly integrate LLMs into this workflow.\n\n## Getting Started\n\n### Installation\nInstall VimLM with a simple pip command:\n```\n$ pip install vimlm\n```\n\n### Launch\nStart VimLM from your terminal:\n```\n$ vimlm\n```\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0000.png)\n\nYou'll see a split interface with your editing pane on the left and the LLM response window on the right. The right window is where the AI assistant's outputs will appear.\n\n## Basic Workflow\n\n### Prompt the AI\nTo ask the LLM a question, press `Ctrl-l`\n\nThe command-line area will show \"VimLM:\" ready for your input. Type your request and press Enter.\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0010.png)\n\nFor example, ask for help creating a Chrome extension:\n```\nCreate a Chrome Extension for copying selected content from webpages\n```\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0020.png)\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0030.png)\n\nThe response is streamed asynchronously to the split window, freeing you to continue editing in the other window.\n\n**TIP**: To focus only on the generated content, use `Ctrl-w w w o` to close the empty window and maximize the response window:\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0033.png)\n\n### Follow-up Questions\nWhen you need to refine or adjust the AI's response, press `Ctrl-j` to make a follow-up request. The previous context is maintained.\n\nFor example, if you notice the generated code uses an outdated manifest version:\n```\nUse manifest V3 instead\n```\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0040.png)\n\n### Deploy Generated Code\nTo extract code blocks from the response and save them as separate files, use the `!deploy` command in a follow-up prompt (`Ctrl-j`):\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0050.png)\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0060.png)\n\n### Apply Suggestions to Your Code\nOpen the file you want to edit: `:e popup.js` (or `vimlm popup.js` in terminal)\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0070.png)\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0071.png)\n\nMake a selection and press `Ctrl-l` and ask a specific question about the selected code:\n```\nVimLM: how to get rid of html tags from item.content\n```\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0100.png)\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0110.png)\n\nWhen you see a solution you like, press `Ctrl-p` to apply the suggested code fix directly to your selection.\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0120.png)\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0130.png)\n\n**TIP**: `gg=G` auto-indents the entire file:\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0140.png)\n\n**TIP**: If the suggested code change doesn't match your original selection, press `gv` to return to your previous selection, then use `o` to switch between the start and end points to adjust as needed.\n\n### Adding Context\nVimLM defaults to layered context - your active selection and the entire current file are automatically included alongside prompts. But as Vim's creator Bram Moolenaar noted, *\"a file seldom comes alone\"*([source](https://www.moolenaar.net/habits.html)). You can use `!include` to add more context to the query:\n```\nAJAX-ify this app !include ~/scrap/hypermedia-applications.summ.md\n```\n\nIt can be used to automate tedious parts of development, such as reviewing changes for a commit message:\n```\nGit commit message for the code changes !include $(git diff popup.js)\n```\n\n![](https://raw.githubusercontent.com/JosefAlbers/VimLM/main/assets/0150.png)\n\nOr to generate changelogs after a version update:\n```\nSummarize the changes !include $(git log --oneline -n 50)\n```\n\nYou can also pipe and filter to focus on specific patterns, just as you would in a terminal:\n```\nDiagnose server errors !include $(grep -r \"500 Internal Error\" ./fuzz | head -n 5)\n```\n\n### Ex Commands\nFor frequently used LLM workflows, VimLM provides the `:VimLM` command, allowing you to create and store reusable prompting patterns. A few examples:\n```\n\" Debug CI failures using error logs\n:VimLM Fix Dockerfile !include .gitlab-ci.yml !include $(tail -n 20 ci.log)\n\n\" Generate unit tests for selected functions and save to test/\n:VimLM Write pytest tests for this !include ./src !deploy ./test\n\n\" Add docstrings to all Python functions in file\n:VimLM Add Google-style docstrings !include % !continue 4000\n```\n\n### Changing the LLM Model\nBy default, VimLM uses an uncensored Llama 3.2 3B model with token limit of 2000. You can switch to any MLX-compatible model:\n```json\n{\n  \"LLM_MODEL\": \"mlx-community/DeepSeek-R1-Distill-Qwen-7B-4bit\",\n  \"NUM_TOKEN\": 32768\n}\n```\n\nSave to `~/vimlm/cfg.json` and restart VimLM.\n\n## Conclusion\n\nVim's efficiency and LLM capabilities are a perfect match. VimLM bridges this gap, giving you AI assistance without leaving your favorite editor. Whether you're writing code, fixing bugs, or exploring new frameworks, VimLM helps you stay in flow while leveraging AI power.\n"
  },
  {
    "path": "vimlm.py",
    "content": "# Copyright 2025 Josef Albers\n# \n# Licensed under the Apache License, Version 2.0 (the \"License\");\n# you may not use this file except in compliance with the License.\n# You may obtain a copy of the License at\n# \n#     http://www.apache.org/licenses/LICENSE-2.0\n# \n# Unless required by applicable law or agreed to in writing, software\n# distributed under the License is distributed on an \"AS IS\" BASIS,\n# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n# See the License for the specific language governing permissions and\n# limitations under the License.\n\nimport nanollama\nimport mlx_lm_utils\nimport asyncio\nimport subprocess\nimport json\nimport os\nimport glob\nfrom watchfiles import awatch\nimport shutil\nfrom datetime import datetime\nfrom itertools import accumulate\nimport argparse\nimport tempfile\nfrom pathlib import Path\nfrom string import Template\nimport re\nimport sys\nimport tty\nimport termios\n\nDEFAULTS = dict(\n    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) | \"mlx-community/phi-4-4bit\" (8.25gb)\n    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)\n    NUM_TOKEN = 2000,\n    USE_LEADER = False,\n    KEY_MAP = {},\n    DO_RESET = True,\n    SHOW_USER = False, \n    SEP_CMD = '!',\n    THINK = ('<think>', '</think>'),\n    VERSION = '0.1.2',\n    DEBUG = False,\n)\n\nDATE_FORM = \"%Y_%m_%d_%H_%M_%S\"\nVIMLM_DIR = os.path.expanduser(\"~/.vimlm\")\nWATCH_DIR = os.path.expanduser(\"~/.vimlm/watch_dir\")\nCFG_FILE = 'cfg.json'\nLOG_FILE = \"log.json\"\nLTM_FILE = \"cache.json\"\nOUT_FILE = \"response.md\"\nIN_FILES = [\"context\", \"yank\", \"user\", \"tree\"]\nCFG_PATH = os.path.join(VIMLM_DIR, CFG_FILE)\nLOG_PATH = os.path.join(VIMLM_DIR, LOG_FILE)\nLTM_PATH = os.path.join(VIMLM_DIR, LTM_FILE)\nOUT_PATH = os.path.join(WATCH_DIR, OUT_FILE) \n\ndef reset_dir(dir_path):\n    if os.path.exists(dir_path):\n        shutil.rmtree(dir_path)\n    os.makedirs(dir_path)\n\ndef initialize():\n    def is_incompatible(config):\n        v_str = config.get('VERSION', '0.0.0')\n        for min_v, usr_v in zip(DEFAULTS['VERSION'].split('.'), v_str.split('.')):\n            if int(min_v) < int(usr_v):\n                return False\n            elif int(min_v) > int(usr_v): \n                return True\n        return False\n    try:\n        with open(CFG_PATH, \"r\") as f:\n            config = json.load(f)\n        if is_incompatible(config):\n            raise ValueError('Incompatible version')\n    except Exception as e:\n        print('Initializing config')\n        reset_dir(VIMLM_DIR)\n        config = DEFAULTS\n        with open(CFG_PATH, 'w') as f:\n            json.dump(DEFAULTS, f, indent=2)\n    for k, v in DEFAULTS.items():\n        globals()[k] = config.get(k, v)\n\ninitialize()\n\ndef toout(s, key=None, mode=None):\n    key = '' if key is None else ':'+key\n    mode = 'w' if mode is None else mode\n    with open(OUT_PATH, mode, encoding='utf-8') as f:\n        f.write(s)\n    tolog(s, key='tovim'+key+':'+mode)\n\ndef tolog(log, key='debug'):\n    if not DEBUG and 'debug' in key:\n        return\n    try:\n        with open(LOG_PATH, \"r\", encoding=\"utf-8\") as log_f:\n            logs = json.load(log_f)\n    except:\n        logs = []\n    logs.append(dict(key=key, log=log, timestamp=datetime.now().strftime(DATE_FORM)))\n    with open(LOG_PATH, \"w\", encoding=\"utf-8\") as log_f:\n        json.dump(logs, log_f, indent=2)\n\ndef print_log():\n    with open(LOG_PATH, 'r') as f:\n        logs = json.load(f)\n    for log in logs:\n        print(f'\\033[37m{log[\"key\"]} {log[\"timestamp\"]}\\033[0m')\n        if 'tovim' in log[\"key\"]:\n            print('\\033[33m')\n        elif 'tollm' in log[\"key\"]:\n            print('\\033[31m')\n        print(log[\"log\"])\n        print('\\033[0m')\n\ndef deploy(dest=None, src=None, reformat=True):\n    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.'\n    tolog(f'deploy {dest=} {src=} {reformat=}')\n    if src:\n        chat.reset()\n        with open(src, 'r') as f:\n            prompt_deploy = f.read().strip() + '\\n\\n---\\n\\n' + prompt_deploy\n    if reformat:\n        toout('Deploying...')\n        response = chat(prompt_deploy, max_new=NUM_TOKEN, verbose=False, stream=False)['text']\n        toout(response, 'deploy')\n        lines = response.splitlines()\n    else:\n        with open(OUT_PATH, 'r') as f:\n            lines = f.readlines()\n    dest = get_path(dest)\n    os.makedirs(dest, exist_ok=True)\n    current_filename = None\n    code_block = []\n    in_code_block = False\n    for line in lines:\n        line = line.rstrip()\n        if line.startswith(\"```\"):\n            if in_code_block and current_filename:\n                with open(os.path.join(dest, os.path.basename(current_filename)), \"w\", encoding=\"utf-8\") as code_file:\n                    code_file.write(\"\\n\".join(code_block) + \"\\n\")\n                code_block = []\n            in_code_block = not in_code_block\n        elif in_code_block:\n            code_block.append(line)\n        else:\n            match = re.match(r\"^\\*\\*(.+?)\\*\\*$\", line)\n            if match:\n                current_filename = re.sub(r\"[^a-zA-Z0-9_.-]\", \"\", match.group(1))\n\ndef is_binary(file_path):\n    try:\n        with open(file_path, 'rb') as f:\n            chunk = f.read(1024)\n            chunk.decode('utf-8') \n        return False \n    except UnicodeDecodeError:\n        return True\n    except Exception as e:\n        return f\"Error: {e}\"\n\ndef split_str(doc, max_len=2000, get_len=len):\n    chunks, current_chunk, current_len = [], [], 0\n    lines = doc.splitlines(keepends=True)\n    atomic_chunks, temp = [], []\n    for line in lines:\n        if line.strip():\n            temp.append(line)\n        else:\n            if temp:\n                atomic_chunks.append(\"\".join(temp))\n                temp = []\n            atomic_chunks.append(line) \n    if temp:\n        atomic_chunks.append(\"\".join(temp))\n    for chunk in atomic_chunks:\n        if current_len + get_len(chunk) > max_len and current_chunk:\n            chunks.append(\"\".join(current_chunk))\n            current_chunk, current_len = [], 0\n        current_chunk.append(chunk)\n        current_len += get_len(chunk)\n    if current_chunk:\n        if current_len < max_len / 2 and len(chunks) > 0:\n            chunks[-1] += \"\".join(current_chunk)\n        else:\n            chunks.append(\"\".join(current_chunk))\n    return chunks\n\ndef retrieve(src_path, max_len=2000, get_len=len):\n    src_path = get_path(src_path)\n    result = {}\n    if not os.path.exists(src_path):\n        tolog(f\"The path {src_path} does not exist.\", 'retrieve')\n        return result\n    if os.path.isfile(src_path):\n        try:\n            with open(src_path, 'r', encoding='utf-8', errors='ignore') as f:\n                content = f.read()\n            result = {src_path:dict(timestamp=os.path.getmtime(src_path), list_str=split_str(content, max_len=max_len, get_len=get_len))}\n        except Exception as e:\n            tolog(f'Failed to retrieve({filename}) due to {e}')\n    else:\n        for filename in os.listdir(src_path):\n            try:\n                file_path = os.path.join(src_path, filename)\n                if filename.startswith('.') or is_binary(file_path):\n                    continue\n                if os.path.isfile(file_path):\n                    with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:\n                        content = f.read()\n                    result[file_path] = dict(timestamp=os.path.getmtime(file_path), list_str=split_str(content, max_len=max_len, get_len=get_len))\n            except Exception as e:\n                tolog(f'Failed to retrieve({filename}) due to {e}')\n                continue\n    return result\n\ndef get_path(s):\n    if not s:\n        s = '.'\n    s = s.strip()\n    s = os.path.expanduser(s)\n    s = os.path.abspath(s)\n    return s\n\ndef ingest(src, max_len=NUM_TOKEN):\n    def load_cache(cache_path=LTM_PATH):\n        if os.path.exists(cache_path):\n            with open(cache_path, 'r', encoding='utf-8') as f:\n                return json.load(f)\n        return {}\n    def dump_cache(new_data, cache_path=LTM_PATH):\n        current_data = load_cache(cache_path)\n        for k, v in new_data.items():\n            if k not in current_data or v['timestamp'] > current_data[k]['timestamp']:\n                current_data[k] = v\n        with open(cache_path, 'w', encoding='utf-8') as f:\n            json.dump(current_data, f, indent=2)\n    src = get_path(src)\n    tolog(f'ingest {src=}')\n    result = ''\n    src_base = os.path.basename(src)\n    if os.path.isdir(src):\n        listdir = [i for i in os.listdir(src) if not i.startswith('.') and '.' in i]\n        result = '\\n- '.join([f'--- {src_base} ---', *listdir]) + '\\n\\n'\n    elif os.path.isfile(src):\n        result = ''\n    else:\n        tolog(f'Failed to ingest({src})')\n        return ''\n    dict_doc = retrieve(src, max_len=max_len, get_len=chat.get_ntok)\n    toout(f'Ingesting {src}...')\n    format_ingest = '{volat}{incoming}\\n\\n---\\n\\nPlease provide a succint bullet point summary for above:' \n    format_volat = 'Here is a summary of part 1 of **{k}**:\\n\\n---\\n\\n{newsum}\\n\\n---\\n\\nHere is the next part:\\n\\n---\\n\\n' \n    dict_sum = {}\n    cache = load_cache()\n    max_new_accum = int(max_len/len(dict_doc)) if len(dict_doc) > 0 else max_len\n    for k, v in dict_doc.items():\n        list_str = v['list_str']\n        v_stamp = v['timestamp']\n        if len(list_str) == 0:\n            continue\n        if len(list_str) == 1 and chat.get_ntok(list_str[0]) <= max_new_accum:\n            chat_summary = list_str[0]\n        else:\n            k_base = os.path.basename(k)\n            if v_stamp == cache.get(k, {}).get('timestamp'):\n                dict_sum[k] = cache[k]\n                continue\n            max_new_sum = int(max_len/len(list_str))\n            volat = f'**{k}**:\\n'\n            accum = ''\n            for i, s in enumerate(list_str):\n                chat.reset()\n                toout(f'\\n\\nIngesting {k_base} {i+1}/{len(list_str)}...\\n\\n', mode='a')\n                newsum = chat(format_ingest.format(volat=volat, incoming=s.rstrip()), max_new=max_new_sum, verbose=False, stream=OUT_PATH)['text'].rstrip()\n                accum += newsum + ' ...\\n'\n                volat = format_volat.format(k=k, newsum=newsum)\n            toout(f'\\n\\nIngesting {k_base}...\\n\\n', mode='a')\n            if chat.get_ntok(accum) <= max_new_accum:\n                chat_summary = accum.strip()\n            else:\n                chat.reset()\n                chat_summary = chat(format_ingest.format(volat=f'**{k}**:\\n', incoming=accum), max_new=max_new_accum, verbose=False, stream=OUT_PATH)['text'].strip()\n        dict_sum[k] = dict(timestamp=v_stamp, summary=chat_summary, ntok=chat.get_ntok(chat_summary))\n    dump_cache(dict_sum)\n    for k, v in dict_sum.items():\n        result += f'--- **{os.path.basename(k)}** ---\\n{v[\"summary\"].strip()}\\n\\n'\n    result += '---\\n\\n'\n    toout(result, 'ingest')\n    return result\n\ndef process_command(data):\n    if 'fim' in data:\n        toout('Autocompleting...')\n        response = fim.fim(prefix=data['context'], suffix=data['yank'], current_path=data['tree'])\n        toout(response['autocomplete'], 'fim')\n        tolog(response)\n        data['user_prompt'] = ''\n        return data\n    for i in IN_FILES:\n        data[i] = data[i].strip()\n    if len(data['user']) == 0:\n        response = chat.resume(max_new=NUM_TOKEN, verbose=False, stream=OUT_PATH)\n        toout(response['text'], mode='a')\n        tolog(response)\n        data['user_prompt'] = ''\n        return data\n    if SEP_CMD in data['user']:\n        data['user_prompt'], *cmds = (x.strip() for x in data['user'].split(SEP_CMD))\n    else:\n        data['user_prompt'] = data['user'].strip()\n        cmds = []\n    tolog(f'process_command i {cmds=} {data=}')\n\n    do_reset = False if 'followup' in data else DO_RESET \n    for cmd in cmds:\n        if cmd.startswith('continue'):\n            arg = cmd.removeprefix('continue').strip('(').strip(')').strip().strip('\"').strip(\"'\").strip()\n            data['max_new'] = NUM_TOKEN if len(arg) == 0 else int(arg)\n            response = chat.resume(max_new=data['max_new'], verbose=False, stream=OUT_PATH)\n            toout(response['text'])\n            tolog(response)\n            do_reset = False\n            break\n\n        if cmd.startswith('reset'):\n            do_reset = True\n            break\n        if cmd.startswith('followup'):\n            do_reset = False\n            break\n    if do_reset:\n        chat.reset()\n\n    full_path = data['tree']\n    data['dir'] = os.path.dirname(full_path)\n    data['file'] = os.path.basename(full_path)\n    data['ext'] = os.path.splitext(full_path)[1][1:]\n    if chat.stop:\n        data['file'] = ''\n        data['context'] = ''\n    if data['tree'] == OUT_PATH:\n        data['dir'] = os.getcwd()\n        data['file'] = ''\n        data['context'] = ''\n        data['ext'] = ''\n    if data['file'] == '.tmp':\n        data['file'] = ''\n        data['ext'] = ''\n\n    if len(cmds) == 1 and len(cmds[0]) == 0:\n        data['include'] = ingest(data['dir'])\n        return data\n\n    data['include'] = ''\n    for cmd in cmds:\n        if cmd.startswith('include'):\n            arg = cmd.removeprefix('include').strip().strip('(').strip(')').strip().strip('\"').strip(\"'\").strip()\n            src = data['dir'] if len(arg) == 0 else arg\n            if arg == '%':\n                continue\n            if src.startswith('`') or src.startswith('$('):\n                shell_cmd = src.strip('`') if src.startswith('`') else src.strip('$()')\n                shell_cmd = shell_cmd.strip()\n                try:\n                    result = subprocess.run(shell_cmd, shell=True, capture_output=True, text=True)\n                    if result.returncode == 0:\n                        data['include'] += f'--- **{shell_cmd}** ---\\n```\\n{result.stdout.strip()}\\n```\\n---\\n\\n'\n                    else:\n                        tolog(f'{shell_cmd} failed {result.stderr.strip()}')\n                except Exception as e:\n                    tolog(f'Error executing {shell_cmd}: {e}')\n            else:\n                data['include'] += ingest(src)\n\n    for cmd in cmds:\n        if cmd.startswith('deploy'):\n            arg = cmd.removeprefix('deploy').strip().strip('(').strip(')').strip().strip('\"').strip(\"'\").strip()\n            if len(data['user_prompt']) == 0:\n                deploy(dest=arg)\n                data['user_prompt'] = ''\n                return data\n            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.\"\n            data['deploy_dest'] = arg\n    for cmd in cmds:\n        if cmd.startswith('write'):\n            arg = cmd.removeprefix('write').strip().strip('(').strip(')').strip().strip('\"').strip(\"'\").strip()\n            if len(arg) == 0:\n                arg = 'response'\n                pass\n            timestamp = datetime.now().strftime(DATE_FORM)\n            data['write_dest'] = re.sub(r\"[^a-zA-Z0-9_.-]\", \"\", f'{arg}_{timestamp}.md')\n    return data\n    \nasync def monitor_directory():\n    async for changes in awatch(WATCH_DIR):\n        found_files = {os.path.basename(f) for _, f in changes}\n        if IN_FILES[-1] in found_files and set(IN_FILES).issubset(set(os.listdir(WATCH_DIR))):\n            data = {}\n            for file in IN_FILES:\n                path = os.path.join(WATCH_DIR, file)\n                with open(path, 'r', encoding='utf-8') as f:\n                    data[file] = f.read()\n                os.remove(os.path.join(WATCH_DIR, file))\n            if 'followup' in os.listdir(WATCH_DIR):\n                os.remove(os.path.join(WATCH_DIR, 'followup'))\n                data['followup'] = True\n            if 'fim' in os.listdir(WATCH_DIR):\n                os.remove(os.path.join(WATCH_DIR, 'fim'))\n                data['fim'] = True\n            if 'quit' in os.listdir(WATCH_DIR):\n                os.remove(os.path.join(WATCH_DIR, 'quit'))\n                data['quit'] = True\n            await process_files(data)\n\nasync def process_files(data):\n    tolog(f'process_files i {data=}')\n    str_template = '{include}'\n    data = process_command(data)\n    if len(data['user_prompt']) == 0:\n        if 'wip' in os.listdir(WATCH_DIR):\n            os.remove(os.path.join(WATCH_DIR, 'wip'))\n        return    \n    if len(data['file']) > 0:\n        str_template += '**{file}**\\n'\n    if len(data['context']) > 0 and data['yank'] != data['context']:\n        str_template += '```{ext}\\n{context}\\n```\\n\\n'\n    if len(data['yank']) > 0:\n        if '\\n' in data['yank']:\n            str_template += \"```{ext}\\n{yank}\\n```\\n\\n\"\n        else:\n            if data['user'] == 0:\n                str_template += \"{yank}\"\n            else:\n                str_template += \"`{yank}` \"\n    str_template += '{user_prompt}'\n    prompt = str_template.format(**data)\n    tolog(prompt, 'tollm')\n    toout('')\n    max_new = data['max_new'] if 'max_new' in data else max(10, NUM_TOKEN - chat.get_ntok(prompt))\n    response = chat(prompt, max_new=max_new, verbose=False, stream=OUT_PATH)\n    if SHOW_USER:\n        toout(response['text'])\n    else:\n        toout(response['text'])\n    tolog(response)\n    if 'write_dest' in data:\n        with open(data['write_dest'], 'w') as f:\n            f.write(response['text'])\n    if 'deploy_dest' in data:\n        deploy(dest=data['deploy_dest'], reformat=False)\n    if 'wip' in os.listdir(WATCH_DIR):\n        os.remove(os.path.join(WATCH_DIR, 'wip'))\n\nKEYL = KEY_MAP.get('l', 'l')\nKEYJ = KEY_MAP.get('j', 'j')\nKEYP = KEY_MAP.get('p', 'p')\nmapl, mapj, mapp = (f'<Leader>{KEYL}', f'<Leader>{KEYJ}', f'<Leader>{KEYP}') if USE_LEADER else (f'<C-{KEYL}>', f'<C-{KEYJ}>', f'<C-{KEYP}>')\nVIMLMSCRIPT = Template(r\"\"\"\nlet s:register_names = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u'] \nlet s:watched_dir = expand('$WATCH_DIR')\nlet s:vimlm_enabled = 1\n\nfunction! ToggleVimLM()\n    if s:vimlm_enabled\n        let s:vimlm_enabled = 0\n        let response_path = s:watched_dir . '/response.md'\n        let bufnum = bufnr(response_path)\n        let winid = bufwinnr(bufnum)\n        if winid != -1\n            execute winid . 'wincmd c'\n        endif\n        if exists('s:monitor_timer')\n            call timer_stop(s:monitor_timer)\n            unlet s:monitor_timer\n        endif\n        echohl WarningMsg | echom \"VimLM disabled\" | echohl None\n    else\n        let s:vimlm_enabled = 1\n        silent! call Monitor()\n        echohl WarningMsg | echom \"VimLM enabled\" | echohl None  \n    endif\nendfunction\n\nfunction! CheckForUpdates(timer)\n    if !s:vimlm_enabled\n        return\n    endif\n    let bufnum = bufnr(s:watched_dir . '/response.md')\n    let winid = bufwinnr(bufnum)\n    if winid == -1\n        call timer_stop(s:monitor_timer)\n        unlet s:monitor_timer\n        call Monitor()\n    else\n        silent! checktime\n    endif\nendfunction\n\nfunction! Monitor()\n    if exists('s:monitor_timer')\n        call timer_stop(s:monitor_timer)\n        unlet s:monitor_timer\n    endif\n    let response_path = s:watched_dir . '/response.md'\n    let bufnum = bufnr(response_path)\n    if bufnum != -1\n        execute 'bwipeout ' . bufnum\n    endif\n    let response_path = s:watched_dir . '/response.md'\n    rightbelow vsplit | execute 'view ' . response_path\n    setlocal autoread\n    setlocal readonly\n    setlocal nobuflisted\n    filetype detect\n    syntax on\n    wincmd h\n    let s:monitor_timer = timer_start(100, 'CheckForUpdates', {'repeat': -1})\nendfunction\n\nfunction! ScrollToTop()\n    let bufnum = bufnr(s:watched_dir . '/response.md')\n    if bufnum != -1\n        let winid = bufwinnr(bufnum)\n        if winid > 0\n            execute winid . \"wincmd w\"\n            normal! gg\n            wincmd p\n        endif\n    endif\nendfunction\n\nfunction! s:CustomInput(prompt) abort\n    call inputsave()\n    let input = input(a:prompt)\n    call inputrestore()\n    if empty(input)\n        return v:null\n    endif\n    return input\nendfunction\n\nfunction! SaveUserInput(prompt)\n    let user_input = s:CustomInput(a:prompt)\n    if user_input is v:null\n        echo \"Input aborted\"\n        return\n    endif\n    let user_file = s:watched_dir . '/user'\n    call writefile([user_input], user_file)\n    let current_file = expand('%:p')\n    let tree_file = s:watched_dir . '/tree'\n    call writefile([current_file], tree_file)\n    call ScrollToTop()\nendfunction\n\nfunction! VisualPrompt()\n    silent! execute \"normal! \\<ESC>\"\n    silent execute \"'<,'>w! \" . s:watched_dir . \"/yank\"\n    silent execute \"w! \" . s:watched_dir . \"/context\"\n    call SaveUserInput('VimLM: ')\nendfunction\n\nfunction! NormalPrompt()\n    silent! execute \"normal! V\\<ESC>\"\n    silent execute \"'<,'>w! \" . s:watched_dir . \"/yank\"\n    silent execute \"w! \" . s:watched_dir . \"/context\"\n    call SaveUserInput('VimLM: ')\nendfunction\n\nfunction! FollowUpPrompt()\n    call writefile([], s:watched_dir . '/yank')\n    call writefile([], s:watched_dir . '/context')\n    call writefile([], s:watched_dir . '/followup')\n    call SaveUserInput('... ')\nendfunction\n\nfunction! ExtractAllCodeBlocks()\n    let filepath = s:watched_dir . '/response.md'\n    if !filereadable(filepath)\n        echoerr \"File not found: \" . filepath\n        return\n    endif\n    let lines = readfile(filepath)\n    let in_code_block = 0\n    let code_blocks = []\n    let current_block = []\n    for line in lines\n        if line =~ '^```'\n            if in_code_block\n                call add(code_blocks, current_block)\n                let current_block = []\n                let in_code_block = 0\n            else\n                let in_code_block = 1\n            endif\n        elseif in_code_block\n            call add(current_block, line)\n        endif\n    endfor\n    if in_code_block\n        call add(code_blocks, current_block)\n    endif\n    for idx in range(len(code_blocks))\n        if idx >= len(s:register_names)\n            break\n        endif\n        let code_block_text = join(code_blocks[idx], \"\\n\")\n        let register_name = s:register_names[idx]\n        call setreg(register_name, code_block_text, 'v') \n    endfor\n    return len(code_blocks)\nendfunction\n\nfunction! PasteIntoLastVisualSelection(...)\n    let num_blocks = ExtractAllCodeBlocks()\n    if a:0 > 0\n        let register_name = a:1\n    else\n        echo \"Extracted \" . num_blocks . \" blocks into registers @a-@\" . s:register_names[num_blocks - 1] . \". Enter register name: \"\n        let register_name = nr2char(getchar())\n    endif\n    if register_name !~ '^[a-z]$'\n        echoerr \"Invalid register name. Please enter a single lowercase letter (e.g., a, b, c).\"\n        return\n    endif\n    let register_content = getreg(register_name)\n    if register_content == ''\n        echoerr \"Register @\" . register_name . \" is empty.\"\n        return\n    endif\n    let current_mode = mode()\n    if current_mode == 'v' || current_mode == 'V' || current_mode == '\u0016'\n        execute 'normal! \"' . register_name . 'p'\n    else\n        normal! gv\n        execute 'normal! \"' . register_name . 'p'\n    endif\nendfunction\n\nfunction! VimLM(...) range\n    let wip_file = s:watched_dir . '/wip'\n    while filereadable(wip_file)\n        sleep 100m\n    endwhile\n    call writefile([], wip_file)\n    let user_input = join(a:000, ' ')\n    if empty(user_input)\n        echo \"Usage: :VimLM <prompt> [!command1] [!command2] ...\"\n        return\n    endif\n    if line(\"'<\") == line(\"'>\")\n        silent! execute \"normal! V\\<ESC>\"\n    endif\n    silent execute \"'<,'>w! \" . s:watched_dir . \"/yank\"\n    silent execute \"w! \" . s:watched_dir . \"/context\"\n    let user_file = s:watched_dir . '/user'\n    call writefile([user_input], user_file)\n    let current_file = expand('%:p')\n    let tree_file = s:watched_dir . '/tree'\n    call writefile([current_file], tree_file)\n    call ScrollToTop()\nendfunction\n\nfunction! SplitAtCursorInInsert()\n    let pos = getcurpos()\n    let line_num = pos[1]\n    let col = pos[2]\n    let lines = getline(1, '$')\n    let current_line = lines[line_num - 1]\n    let prefix_lines = lines[0:line_num - 2]\n    let prefix_part = strpart(current_line, 0, col - 1)\n    if !empty(prefix_part) || col > 1\n        call add(prefix_lines, prefix_part)\n    endif\n    let suffix_lines = []\n    let suffix_part = strpart(current_line, col - 1)\n    if empty(suffix_part)\n        call add(suffix_lines, \"\")\n    else\n        call add(suffix_lines, suffix_part)\n    endif\n    if line_num < len(lines)\n        call extend(suffix_lines, lines[line_num:])\n    endif\n    call writefile(prefix_lines, s:watched_dir . '/context', 'b')\n    call writefile(suffix_lines, s:watched_dir . '/yank', 'b')\n    call writefile([], s:watched_dir . '/fim')\n    call writefile([], s:watched_dir . '/user')\n    let current_file = expand('%:p')\n    let tree_file = s:watched_dir . '/tree'\n    call writefile([current_file], tree_file)\n    call ScrollToTop()\nendfunction\n\nfunction! InsertResponse()\n    let response_path = s:watched_dir . '/response.md'\n    if !filereadable(response_path)\n        echoerr \"Response file not found: \" . response_path\n        return\n    endif\n    let content = readfile(response_path, 'b')\n    let text = join(content, \"\\n\")\n    let saved_z = getreg('z')\n    let saved_ztype = getregtype('z')\n    call setreg('z', text)\n    let col = col('.')\n    let line = getline('.')\n    if col == len(line) + 1\n        normal! \"zgp\n    else\n        normal! \"zgP\n    endif\n    call setreg('z', saved_z, saved_ztype)\nendfunction\n\nfunction! TabInInsert()\n    let wip_file = s:watched_dir . '/wip'\n    call writefile([], wip_file)\n    call SplitAtCursorInInsert()\n    while filereadable(wip_file)\n        sleep 10m\n    endwhile\n    call InsertResponse()\nendfunction\n\ncommand! ToggleVimLM call ToggleVimLM()\ncommand! -range -nargs=+ VimLM call VimLM(<f-args>)\ninoremap <silent> $mapl <C-\\><C-o>:call SplitAtCursorInInsert()<CR>\ninoremap <silent> $mapp <C-\\><C-o>:call InsertResponse()<CR><Right>\ninoremap <silent> $mapj <C-\\><C-o>:call TabInInsert()<CR><Right>\nnnoremap $mapp :call PasteIntoLastVisualSelection()<CR>\nvnoremap $mapp <Cmd>:call PasteIntoLastVisualSelection()<CR>\nvnoremap $mapl <Cmd>:call VisualPrompt()<CR>\nnnoremap $mapl :call NormalPrompt()<CR>\nnnoremap $mapj :call FollowUpPrompt()<CR>\ncall Monitor()\n\"\"\").safe_substitute(dict(WATCH_DIR=WATCH_DIR, mapl=mapl, mapj=mapj, mapp=mapp))\n\nasync def main(args):\n    with tempfile.NamedTemporaryFile(mode='w', suffix='.vim', delete=False) as f:\n        f.write(VIMLMSCRIPT)\n        vim_script = f.name\n    vim_command = [\"vim\", \"-c\", f\"source {vim_script}\"]\n    if args.args_vim:\n        vim_command.extend(args.args_vim)\n    else:\n        vim_command.append('.tmp')\n    try:\n        monitor_task = asyncio.create_task(monitor_directory())\n        vim_process = await asyncio.create_subprocess_exec(*vim_command)\n        await vim_process.wait()\n    finally:\n        monitor_task.cancel()\n        try:\n            await monitor_task\n        except asyncio.CancelledError:\n            pass\n        os.remove(vim_script)\n\ndef get_common_dir_and_children(file_paths):\n    dirs = [os.path.dirname(path) for path in file_paths]\n    dir_parts = [path.split(os.sep) for path in dirs]\n    common_parts = []\n    for parts in zip(*dir_parts):\n        if all(part == parts[0] for part in parts):\n            common_parts.append(parts[0])\n        else:\n            break\n    parent_path = os.sep.join(common_parts)\n    child_paths = [os.path.relpath(path, parent_path) for path in file_paths]\n    repo_name = os.path.basename(os.path.dirname(parent_path))\n    return repo_name, parent_path, child_paths\n\ndef get_key():\n    fd = sys.stdin.fileno()\n    old_settings = termios.tcgetattr(fd)\n    try:\n        tty.setraw(sys.stdin.fileno())\n        ch = sys.stdin.read(1)\n        if ch == '\\x1b': \n            ch = sys.stdin.read(2)\n            if ch == '[A':\n                return 'up'\n            elif ch == '[B':\n                return 'down'\n        elif ch == 'j':\n            return 'down'\n        elif ch == 'k':\n            return 'up'\n        elif ch in [' ', 'x']:\n            return 'space'\n        elif ch == '\\r':\n            return 'enter'\n        elif ch == 'q':\n            return 'quit'\n    finally:\n        termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)\n    return None\n\ndef select_files_interactive(file_paths):\n    selected = [False] * len(file_paths)\n    current_row = 0\n    visible_start = 0\n    visible_end = 0\n    max_visible = 10\n    def display():\n        nonlocal visible_start, visible_end\n        visible_start = max(0, current_row - max_visible + 2)\n        visible_end = min(len(file_paths), visible_start + max_visible)\n        sys.stdout.write(f\"\\x1b[{max_visible + 2}A\") \n        for i in range(visible_start, visible_end):\n            prefix = \"> \" if i == current_row else \"  \"\n            check = \"[X]\" if selected[i] else \"[ ]\"\n            filename = os.path.basename(file_paths[i])[:40] \n            sys.stdout.write(f\"\\x1b[K{prefix}{check} {filename}\\n\") \n        scroll_indicator = f\" [{visible_start+1}-{visible_end} of {len(file_paths)}] \"\n        sys.stdout.write(f\"\\x1b[K{scroll_indicator}\\nSpace:Toggle Enter:Confirm Arrows:Navigate\\n\")\n        sys.stdout.flush()\n    sys.stdout.write(\"\\n\" * (max_visible + 2))\n    display()\n    while True:\n        key = get_key()\n        if key == 'up' and current_row > 0:\n            current_row -= 1\n            if current_row < visible_start:\n                visible_start = max(0, visible_start - 1)\n                visible_end = visible_start + max_visible\n            display()\n        elif key == 'down' and current_row < len(file_paths) - 1:\n            current_row += 1\n            if current_row >= visible_end:\n                visible_start = min(len(file_paths) - max_visible, visible_start + 1)\n                visible_end = visible_start + max_visible\n            display()\n        elif key == 'space':\n            selected[current_row] = not selected[current_row]\n            display()\n        elif key == 'enter':\n            sys.stdout.write(f\"\\x1b[{max_visible + 2}B\")\n            sys.stdout.write(\"\\x1b[J\") \n            break\n        elif key == 'quit':\n            # selected = []\n            # break\n            return None\n    return [file_paths[i] for i in range(len(file_paths)) if selected[i]]\n\ndef get_repo(args_repo, args_vim):\n    if not args_repo:\n        return None\n    vim_files = []\n    for arg in args_vim:\n        if not arg.startswith('-'):\n            if os.path.exists(arg):\n                vim_files.append(os.path.abspath(arg))\n    repo_paths = []\n    for pattern in args_repo:\n        expanded_paths = glob.glob(pattern)\n        if expanded_paths:\n            for path in expanded_paths:\n                if not os.path.isfile(path) or path in vim_files+repo_paths or os.path.basename(path).startswith('.') or is_binary(path):\n                    continue\n                repo_paths.append(os.path.abspath(path))\n    if len(repo_paths) > 9:\n        try:\n            sys.stdout.write(\"\\n\") \n            selected_paths = select_files_interactive(repo_paths)\n            if not selected_paths:\n                return None\n            sys.stdout.write(\"\\x1b[2A\")  \n            sys.stdout.write(\"\\x1b[J\") \n            repo_paths = selected_paths\n        except:\n            pass\n    repo_files = []\n    rest_files = []\n    for path in repo_paths:\n        if path in vim_files:\n            rest_files.append(os.path.abspath(path))\n        else:\n            repo_files.append(os.path.abspath(path))\n    repo_name, repo_path, child_paths = get_common_dir_and_children(repo_files+rest_files)\n    repo_names, rest_names = child_paths[:len(repo_files)], child_paths[len(repo_files):]\n    list_content = [f'<|repo_name|>{repo_name}\\n']\n    list_mtime = []\n    for p, n in zip(repo_files, repo_names):\n        try:\n            with open(p, 'r') as f:\n                list_content.append(f'<|file_sep|>{n}\\n{f.read()}\\n')\n            list_mtime.append(int(os.path.getmtime(p)))\n        except Exception as e:\n            tolog(f'Skipped {p} d/t {e}', 'debug:get_repo()')\n    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)\n\ndef run():\n    parser = argparse.ArgumentParser(description=\"VimLM - LLM-powered Vim assistant\")\n    parser.add_argument('--test', action='store_true', help=\"Run in test mode\")\n    parser.add_argument('args_vim', nargs='*', help=\"Vim arguments\")\n    parser.add_argument('--repo', nargs='*', help=\"Paths to directories or files (e.g., assets/*, path/to/file)\")\n    args = parser.parse_args()\n    dict_repo = get_repo(args.repo, args.args_vim)\n    tolog(dict_repo, 'debug:get_repo()')\n    if args.test:\n        return\n    reset_dir(WATCH_DIR)\n    toout('Loading LLM...')\n    if LLM_MODEL is None:\n        globals()['chat'] = nanollama.Chat(model_path='uncn_llama_32_3b_it')\n        toout(f'LLM is ready')\n    else:\n        globals()['chat'] = mlx_lm_utils.Chat(model_path=LLM_MODEL, think=THINK)\n        toout(f'{LLM_MODEL.split('/')[-1]} is ready')\n    if FIM_MODEL and FIM_MODEL != LLM_MODEL:\n        globals()['fim'] = mlx_lm_utils.Chat(model_path=FIM_MODEL, cache_dir=VIMLM_DIR, dict_repo=dict_repo)\n        toout(f'\\n\\n{FIM_MODEL.split(\"/\")[-1]} is ready', mode='a')\n    else:\n        globals()['fim'] = chat\n        chat.set_cache_repo(dict_repo, cache_dir=VIMLM_DIR)\n    asyncio.run(main(args))\n\nif __name__ == '__main__':\n    run()\n"
  }
]