Repository: yetone/avante.nvim Branch: main Commit: 348be57354a8 Files: 215 Total size: 1.5 MB Directory structure: gitextract_wdvhrp8a/ ├── .cargo/ │ └── config.toml ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.yml │ │ ├── config.yml │ │ └── feature_request.yml │ └── workflows/ │ ├── close-stale-issues-and-prs.yaml │ ├── lua.yaml │ ├── pre-commit.yaml │ ├── release.yaml │ └── rust.yaml ├── .gitignore ├── .luacheckrc ├── .pre-commit-config.yaml ├── Build.ps1 ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md ├── README_zh.md ├── autoload/ │ └── avante.vim ├── build.sh ├── crates/ │ ├── avante-html2md/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ ├── avante-repo-map/ │ │ ├── Cargo.toml │ │ ├── queries/ │ │ │ ├── tree-sitter-c-defs.scm │ │ │ ├── tree-sitter-c-sharp-defs.scm │ │ │ ├── tree-sitter-cpp-defs.scm │ │ │ ├── tree-sitter-elixir-defs.scm │ │ │ ├── tree-sitter-go-defs.scm │ │ │ ├── tree-sitter-java-defs.scm │ │ │ ├── tree-sitter-javascript-defs.scm │ │ │ ├── tree-sitter-lua-defs.scm │ │ │ ├── tree-sitter-php-defs.scm │ │ │ ├── tree-sitter-python-defs.scm │ │ │ ├── tree-sitter-ruby-defs.scm │ │ │ ├── tree-sitter-rust-defs.scm │ │ │ ├── tree-sitter-scala-defs.scm │ │ │ ├── tree-sitter-swift-defs.scm │ │ │ ├── tree-sitter-typescript-defs.scm │ │ │ └── tree-sitter-zig-defs.scm │ │ └── src/ │ │ └── lib.rs │ ├── avante-templates/ │ │ ├── Cargo.toml │ │ └── src/ │ │ └── lib.rs │ └── avante-tokenizers/ │ ├── Cargo.toml │ ├── README.md │ └── src/ │ └── lib.rs ├── lua/ │ ├── avante/ │ │ ├── api.lua │ │ ├── auth/ │ │ │ └── pkce.lua │ │ ├── clipboard.lua │ │ ├── config.lua │ │ ├── diff.lua │ │ ├── extensions/ │ │ │ ├── init.lua │ │ │ └── nvim_tree.lua │ │ ├── file_selector.lua │ │ ├── health.lua │ │ ├── highlights.lua │ │ ├── history/ │ │ │ ├── helpers.lua │ │ │ ├── init.lua │ │ │ ├── message.lua │ │ │ └── render.lua │ │ ├── history_selector.lua │ │ ├── html2md.lua │ │ ├── init.lua │ │ ├── libs/ │ │ │ ├── ReAct_parser.lua │ │ │ ├── ReAct_parser2.lua │ │ │ ├── acp_client.lua │ │ │ ├── jsonparser.lua │ │ │ └── xmlparser.lua │ │ ├── llm.lua │ │ ├── llm_tools/ │ │ │ ├── attempt_completion.lua │ │ │ ├── base.lua │ │ │ ├── bash.lua │ │ │ ├── create.lua │ │ │ ├── delete_tool_use_messages.lua │ │ │ ├── dispatch_agent.lua │ │ │ ├── edit_file.lua │ │ │ ├── get_diagnostics.lua │ │ │ ├── glob.lua │ │ │ ├── grep.lua │ │ │ ├── helpers.lua │ │ │ ├── init.lua │ │ │ ├── insert.lua │ │ │ ├── ls.lua │ │ │ ├── read_todos.lua │ │ │ ├── replace_in_file.lua │ │ │ ├── str_replace.lua │ │ │ ├── think.lua │ │ │ ├── undo_edit.lua │ │ │ ├── view.lua │ │ │ ├── write_to_file.lua │ │ │ └── write_todos.lua │ │ ├── model_selector.lua │ │ ├── path.lua │ │ ├── providers/ │ │ │ ├── azure.lua │ │ │ ├── bedrock/ │ │ │ │ └── claude.lua │ │ │ ├── bedrock.lua │ │ │ ├── claude.lua │ │ │ ├── cohere.lua │ │ │ ├── copilot.lua │ │ │ ├── gemini.lua │ │ │ ├── init.lua │ │ │ ├── ollama.lua │ │ │ ├── openai.lua │ │ │ ├── vertex.lua │ │ │ ├── vertex_claude.lua │ │ │ └── watsonx_code_assistant.lua │ │ ├── rag_service.lua │ │ ├── range.lua │ │ ├── repo_map.lua │ │ ├── selection.lua │ │ ├── selection_result.lua │ │ ├── sidebar.lua │ │ ├── suggestion.lua │ │ ├── templates/ │ │ │ ├── _context.avanterules │ │ │ ├── _diagnostics.avanterules │ │ │ ├── _environments.avanterules │ │ │ ├── _gpt4-1-agentic.avanterules │ │ │ ├── _memory.avanterules │ │ │ ├── _project.avanterules │ │ │ ├── _task-guidelines.avanterules │ │ │ ├── _tools-guidelines.avanterules │ │ │ ├── agentic.avanterules │ │ │ ├── base.avanterules │ │ │ ├── editing.avanterules │ │ │ ├── legacy.avanterules │ │ │ └── suggesting.avanterules │ │ ├── tokenizers.lua │ │ ├── types.lua │ │ ├── ui/ │ │ │ ├── acp_confirm_adapter.lua │ │ │ ├── button_group_line.lua │ │ │ ├── confirm.lua │ │ │ ├── input/ │ │ │ │ ├── init.lua │ │ │ │ └── providers/ │ │ │ │ ├── dressing.lua │ │ │ │ ├── native.lua │ │ │ │ └── snacks.lua │ │ │ ├── line.lua │ │ │ ├── prompt_input.lua │ │ │ └── selector/ │ │ │ ├── init.lua │ │ │ └── providers/ │ │ │ ├── fzf_lua.lua │ │ │ ├── mini_pick.lua │ │ │ ├── native.lua │ │ │ ├── snacks.lua │ │ │ └── telescope.lua │ │ └── utils/ │ │ ├── diff2search_replace.lua │ │ ├── environment.lua │ │ ├── file.lua │ │ ├── init.lua │ │ ├── logo.lua │ │ ├── lru_cache.lua │ │ ├── lsp.lua │ │ ├── path.lua │ │ ├── platform.lua │ │ ├── promptLogger.lua │ │ ├── prompts.lua │ │ ├── root.lua │ │ ├── streaming_json_parser.lua │ │ ├── test.lua │ │ └── tokens.lua │ ├── avante_lib.lua │ └── cmp_avante/ │ ├── commands.lua │ ├── mentions.lua │ └── shortcuts.lua ├── luarc.json.template ├── plugin/ │ └── avante.lua ├── py/ │ └── rag-service/ │ ├── Dockerfile │ ├── README.md │ ├── gitconfig │ ├── requirements.txt │ ├── run.sh │ ├── shell.nix │ └── src/ │ ├── libs/ │ │ ├── __init__.py │ │ ├── configs.py │ │ ├── db.py │ │ ├── logger.py │ │ └── utils.py │ ├── main.py │ ├── models/ │ │ ├── __init__.py │ │ ├── indexing_history.py │ │ └── resource.py │ ├── providers/ │ │ ├── __init__.py │ │ ├── dashscope.py │ │ ├── factory.py │ │ ├── ollama.py │ │ ├── openai.py │ │ └── openrouter.py │ └── services/ │ ├── __init__.py │ ├── indexing_history.py │ └── resource.py ├── pyrightconfig.json ├── ruff.toml ├── scripts/ │ ├── lua-typecheck.sh │ ├── run-luatest.sh │ └── setup-deps.sh ├── stylua.toml ├── syntax/ │ └── jinja.vim └── tests/ ├── data/ │ ├── claude_token_error_response.json │ └── claude_token_response.json ├── libs/ │ ├── acp_client_spec.lua │ └── jsonparser_spec.lua ├── llm_spec.lua ├── llm_tools/ │ └── helpers_spec.lua ├── llm_tools_spec.lua ├── providers/ │ ├── bedrock_spec.lua │ ├── claude_spec.lua │ └── watsonx_code_assistant_spec.lua ├── rag_service_spec.lua ├── ui/ │ └── acp_confirm_adapter_spec.lua └── utils/ ├── file_spec.lua ├── fix_diff_spec.lua ├── get_parent_path_spec.lua ├── init_spec.lua ├── join_paths_spec.lua ├── make_relative_path_spec.lua └── streaming_json_parser_spec.lua ================================================ FILE CONTENTS ================================================ ================================================ FILE: .cargo/config.toml ================================================ [target.x86_64-apple-darwin] rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"] [target.aarch64-apple-darwin] rustflags = ["-C", "link-arg=-undefined", "-C", "link-arg=dynamic_lookup"] [target.x86_64-unknown-linux-musl] rustflags = ["-C", "target-feature=-crt-static"] ================================================ FILE: .editorconfig ================================================ ; https://editorconfig.org/ root = true [*] insert_final_newline = true charset = utf-8 trim_trailing_whitespace = true indent_style = space indent_size = 2 tab_width = 8 [{Makefile,**/Makefile}] indent_style = tab indent_size = 8 ================================================ FILE: .gitattributes ================================================ * text=auto eol=lf **/*.lock linguist-generated=true *.avanterules linguist-language=jinja syntax/jinja.vim linguist-vendored ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.yml ================================================ name: 🐛 Bug Report description: Create a bug report to help us improve Avante title: 'bug: ' labels: ['bug'] body: - type: markdown id: issue-already-exists attributes: value: | Please search to see if an issue already exists for the bug you encountered. See [Searching Issues and Pull Requests](https://docs.github.com/en/search-github/searching-on-github/searching-issues-and-pull-requests) for how to use the GitHub search bar and filters. - type: textarea id: describe-the-bug validations: required: true attributes: label: Describe the bug description: Please provide a clear and concise description about the problem you ran into. placeholder: This happened when I ... - type: textarea id: to-reproduce validations: required: false attributes: label: To reproduce description: | Please provide a code sample or a code snippet to reproduce said problem. If you have code snippets, error messages, or a stack trace please also provide them here. **IMPORTANT**: make sure to use [code tags](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/creating-and-highlighting-code-blocks#syntax-highlighting) to correctly format your code. Screenshots are helpful but don't use them for code snippets as they don't allow others to copy-and-paste your code. placeholder: | Give a minimal config to reproduce the issue. - type: textarea id: expected-behavior validations: required: false attributes: label: Expected behavior description: 'A clear and concise description of what you would expect to happen.' - type: textarea id: how-to-install validations: required: true attributes: label: Installation method description: | Please share your installation method with us. value: | Use lazy.nvim: ```lua { "yetone/avante.nvim", event = "VeryLazy", lazy = false, version = false, -- set this if you want to always pull the latest change opts = { -- add any opts here }, -- if you want to build from source then do `make BUILD_FROM_SOURCE=true` build = "make", -- build = "powershell -ExecutionPolicy Bypass -File Build.ps1 -BuildFromSource false" -- for windows dependencies = { "nvim-lua/plenary.nvim", "MunifTanjim/nui.nvim", }, } ``` - type: textarea id: environment-info attributes: label: Environment description: | Please share your environment with us, including your neovim version using `nvim -v` and `uname -a`. placeholder: | neovim version: ... distribution (if any): ... platform: ... validations: required: true - type: textarea attributes: label: Repro description: Minimal `init.lua` to reproduce this issue. Save as `repro.lua` and run with `nvim -u repro.lua` value: | vim.env.LAZY_STDPATH = ".repro" load(vim.fn.system("curl -s https://raw.githubusercontent.com/folke/lazy.nvim/main/bootstrap.lua"))() require("lazy.minit").repro({ spec = { -- add any other plugins here }, }) render: lua validations: required: false ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true version: 2.1 ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.yml ================================================ name: 🚀 Feature Request description: Submit a proposal/request for new Avante feature. title: 'feature: ' labels: ['new-feature', 'enhancement'] body: - type: textarea id: feature-request validations: required: true attributes: label: Feature request description: | A clear and concise description of the feature request. placeholder: | I would like it if... - type: textarea id: motivation validations: required: false attributes: label: Motivation description: | Please outline the motivation for this feature request. Is your feature request related to a problem? e.g., I'm always frustrated when [...]. If this is related to another issue, please link here too. If you have a current workaround, please also provide it here. placeholder: | This feature would solve ... - type: textarea id: other attributes: label: Other description: | Is there any way that you could help, e.g. by submitting a PR? placeholder: | I would love to contribute ... ================================================ FILE: .github/workflows/close-stale-issues-and-prs.yaml ================================================ name: 'Close stale issues and PRs' on: schedule: - cron: '30 1 * * *' permissions: contents: write # only for delete-branch option issues: write pull-requests: write jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v9 with: stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days.' stale-pr-message: 'This PR is stale because it has been open 14 days with no activity. Remove stale label or comment or this will be closed in 10 days.' close-issue-message: 'This issue was closed because it has been stalled for 5 days with no activity.' close-pr-message: 'This PR was closed because it has been stalled for 10 days with no activity.' days-before-issue-stale: 30 days-before-pr-stale: 14 days-before-issue-close: 5 days-before-pr-close: 10 ================================================ FILE: .github/workflows/lua.yaml ================================================ name: Lua CI on: push: branches: - main paths: - "**/*.lua" - .github/workflows/lua.yaml pull_request: branches: - main paths: - "**/*.lua" - .github/workflows/lua.yaml jobs: # reference from: https://github.com/nvim-lua/plenary.nvim/blob/2d9b06177a975543726ce5c73fca176cedbffe9d/.github/workflows/default.yml#L6C3-L43C20 run_tests: name: unit tests runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - os: ubuntu-22.04 rev: v0.10.0 steps: - uses: actions/checkout@v6 - id: todays-date run: echo "date=$(date +%F)" >> "$GITHUB_OUTPUT" - name: Restore cache for today's nightly. id: cache-neovim uses: actions/cache@v4 with: path: _neovim key: ${{ runner.os }}-${{ matrix.rev }}-${{ steps.todays-date.outputs.date }} - name: Download neovim ${{ matrix.rev }} env: GH_TOKEN: ${{ github.token }} NEOVIM_VERSION: ${{ matrix.rev }} if: steps.cache-neovim.outputs.cache-hit != 'true' run: | mkdir -p _neovim gh release download \ --output - \ --pattern nvim-linux64.tar.gz \ --repo neovim/neovim \ "$NEOVIM_VERSION" | tar xvz --strip-components 1 --directory _neovim - name: Prepare run: | sudo apt-get update sudo apt-get install -y ripgrep sudo apt-get install -y silversearcher-ag echo "${PWD}/_neovim/bin" >> "$GITHUB_PATH" echo VIM="${PWD}/_neovim/share/nvim/runtime" >> "$GITHUB_ENV" - name: Run tests run: | nvim --version make luatest typecheck: name: Typecheck runs-on: ubuntu-latest strategy: matrix: nvim_version: [ stable ] luals_version: [ 3.13.6 ] steps: - uses: actions/checkout@v6 - uses: rhysd/action-setup-vim@v1 with: neovim: true version: ${{ matrix.nvim_version }} - name: Typecheck env: VIMRUNTIME: /home/runner/nvim-${{ matrix.nvim_version }}/share/nvim/runtime run: | make lua-typecheck ================================================ FILE: .github/workflows/pre-commit.yaml ================================================ name: pre-commit on: pull_request: types: [labeled, opened, reopened, synchronize] push: branches: [main, test-me-*] jobs: pre-commit: if: "github.event.action != 'labeled' || github.event.label.name == 'pre-commit ci run'" runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: gh pr edit ${{ github.event.number }} --remove-label 'pre-commit ci run' if: github.event.action == 'labeled' && github.event.label.name == 'pre-commit ci run' env: GH_TOKEN: ${{ github.token }} - uses: actions/setup-python@v3 with: python-version: '3.11' - name: Install uv uses: astral-sh/setup-uv@v5 - run: | uv venv source .venv/bin/activate uv pip install -r py/rag-service/requirements.txt - uses: leafo/gh-actions-lua@v11 - uses: leafo/gh-actions-luarocks@v5 - run: luarocks install luacheck - name: Install stylua from crates.io uses: baptiste0928/cargo-install@v3 with: crate: stylua args: --features lua54 - uses: pre-commit/action@v3.0.1 - uses: pre-commit-ci/lite-action@v1.1.0 if: always() ================================================ FILE: .github/workflows/release.yaml ================================================ name: Release on: push: tags: [v\d+\.\d+\.\d+] permissions: contents: write packages: write env: CARGO_TERM_COLOR: always jobs: create-release: permissions: contents: write runs-on: ubuntu-24.04 outputs: release_id: ${{ steps.create-release.outputs.id }} release_upload_url: ${{ steps.create-release.outputs.upload_url }} release_body: "${{ steps.tag.outputs.message }}" steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 - name: Get version id: get_version uses: battila7/get-version-action@d97fbc34ceb64d1f5d95f4dfd6dce33521ccccf5 # ratchet:battila7/get-version-action@v2 - name: Get tag message id: tag run: | git fetch --depth=1 origin +refs/tags/*:refs/tags/* echo "message<> $GITHUB_OUTPUT echo "$(git tag -l --format='%(contents)' ${{ steps.get_version.outputs.version }})" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT - name: Create Release id: create-release uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # ratchet:ncipollo/release-action@v1 with: draft: true name: "avante-libs ${{ steps.get_version.outputs.version }}" tag: ${{ steps.get_version.outputs.version }} body: "${{ steps.tag.outputs.message }}" releases-matrix: needs: [create-release] strategy: fail-fast: false matrix: feature: [lua51, luajit] config: - os: ubuntu-24.04-arm os_name: linux arch: aarch64 rust_target: aarch64-unknown-linux-gnu docker_platform: linux/aarch64 container: quay.io/pypa/manylinux2014_aarch64 - os: ubuntu-latest os_name: linux arch: x86_64 rust_target: x86_64-unknown-linux-gnu docker_platform: linux/amd64 container: quay.io/pypa/manylinux2014_x86_64 # for glibc 2.17 - os: macos-13 os_name: darwin arch: x86_64 rust_target: x86_64-apple-darwin - os: macos-latest os_name: darwin arch: aarch64 rust_target: aarch64-apple-darwin - os: windows-latest os_name: windows arch: x86_64 rust_target: x86_64-pc-windows-msvc - os: windows-latest os_name: windows arch: aarch64 rust_target: aarch64-pc-windows-msvc runs-on: ${{ matrix.config.os }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # ratchet:Swatinem/rust-cache@v2 if: ${{ matrix.config.container == null }} - uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a # ratchet:dtolnay/rust-toolchain@master if: ${{ matrix.config.container == null }} with: targets: ${{ matrix.config.rust_target }} toolchain: "1.85.0" - name: Build all crates if: ${{ matrix.config.container == null }} run: | cargo build --release --features ${{ matrix.feature }} - name: Build all crates with glibc 2.17 # for glibc 2.17 if: ${{ matrix.config.container != null }} run: | # sudo apt-get install -y qemu qemu-user-static docker run \ --rm \ -v $(pwd):/workspace \ -w /workspace \ --platform ${{ matrix.config.docker_platform }} \ ${{ matrix.config.container }} \ bash -c "yum install -y perl-IPC-Cmd openssl-devel && curl --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal && . /root/.cargo/env && cargo build --release --features ${{ matrix.feature }}" - name: Handle binaries if: ${{ matrix.config.os_name != 'windows' }} shell: bash run: | mkdir -p results if [ "${{ matrix.config.os_name }}" == "linux" ]; then EXT="so" else EXT="dylib" fi cp target/release/libavante_templates.$EXT results/avante_templates.$EXT cp target/release/libavante_tokenizers.$EXT results/avante_tokenizers.$EXT cp target/release/libavante_repo_map.$EXT results/avante_repo_map.$EXT cp target/release/libavante_html2md.$EXT results/avante_html2md.$EXT cd results tar zcvf avante_lib-${{ matrix.config.os_name }}-${{ matrix.config.arch }}-${{ matrix.feature }}.tar.gz *.${EXT} - name: Handle binaries (Windows) if: ${{ matrix.config.os_name == 'windows' }} shell: pwsh run: | New-Item -ItemType Directory -Force -Path results Copy-Item -Path "target\release\avante_templates.dll" -Destination "results\avante_templates.dll" Copy-Item -Path "target\release\avante_tokenizers.dll" -Destination "results\avante_tokenizers.dll" Copy-Item -Path "target\release\avante_repo_map.dll" -Destination "results\avante_repo_map.dll" Copy-Item -Path "target\release\avante_html2md.dll" -Destination "results\avante_html2md.dll" Set-Location -Path results $dllFiles = Get-ChildItem -Filter "*.dll" | Select-Object -ExpandProperty Name Compress-Archive -Path $dllFiles -DestinationPath "avante_lib-${{ matrix.config.os_name }}-${{ matrix.config.arch }}-${{ matrix.feature }}.zip" - name: Upload Release Asset uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # ratchet:shogo82148/actions-upload-release-asset@v1 if: ${{ matrix.config.os_name != 'windows' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ASSET_NAME: avante_lib-${{ matrix.config.os_name }}-${{ matrix.config.arch }}-${{ matrix.feature }}.tar.gz with: upload_url: ${{ needs.create-release.outputs.release_upload_url }} asset_path: ./results/avante_lib-${{ matrix.config.os_name }}-${{ matrix.config.arch }}-${{ matrix.feature }}.tar.gz - name: Upload Release Asset (Windows) uses: shogo82148/actions-upload-release-asset@8482bd769644976d847e96fb4b9354228885e7b4 # ratchet:shogo82148/actions-upload-release-asset@v1 if: ${{ matrix.config.os_name == 'windows' }} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ASSET_NAME: avante_lib-${{ matrix.config.os_name }}-${{ matrix.config.arch }}-${{ matrix.feature }}.zip with: upload_url: ${{ needs.create-release.outputs.release_upload_url }} asset_path: ./results/avante_lib-${{ matrix.config.os_name }}-${{ matrix.config.arch }}-${{ matrix.feature }}.zip publish-release: permissions: contents: write runs-on: ubuntu-24.04 needs: [create-release, releases-matrix] steps: - name: publish release id: publish-release uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # ratchet:actions/github-script@v6 env: release_id: ${{ needs.create-release.outputs.release_id }} with: script: | github.rest.repos.updateRelease({ owner: context.repo.owner, repo: context.repo.repo, release_id: process.env.release_id, draft: false, prerelease: false }) ================================================ FILE: .github/workflows/rust.yaml ================================================ name: Rust CI on: push: branches: - main paths: - "crates/**/*" - "Cargo.lock" - "Cargo.toml" pull_request: branches: - main paths: - "crates/**/*" - "Cargo.lock" - "Cargo.toml" jobs: tests: name: Run Rust tests runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4 - uses: Swatinem/rust-cache@82a92a6e8fbeee089604da2575dc567ae9ddeaab # ratchet:Swatinem/rust-cache@v2 - uses: dtolnay/rust-toolchain@7b1c307e0dcbda6122208f10795a713336a9b35a # ratchet:dtolnay/rust-toolchain@master with: toolchain: stable components: clippy, rustfmt - name: Run rust tests run: cargo test --features luajit ================================================ FILE: .gitignore ================================================ *.so # Lua compiled files *.lua~ *.luac .venv __pycache__/ # Neovim plugin specific files plugin/packer_compiled.lua # OS generated files .DS_Store Thumbs.db # Editor/IDE generated files *.swp *.swo *~ .vscode/ .idea/ # Dependency manager generated directories /lua_modules/ /.luarocks/ # Log files *.log # Temporary files tmp/ temp/ # Environment variable files (if you use .env file to store API keys) .env .envrc # If you use any build tools, you might need to ignore build output directories build/ dist/ # If you use any test frameworks, you might need to ignore test coverage reports coverage/ # If you use documentation generation tools, you might need to ignore generated docs doc/ # If you have any personal configuration files, you should ignore them too config.personal.lua target ================================================ FILE: .luacheckrc ================================================ -- Rerun tests only if their modification time changed. cache = true -- Glorious list of warnings: https://luacheck.readthedocs.io/en/stable/warnings.html ignore = { '211', '631', '212', -- Unused argument, In the case of callback function, _arg_name is easier to understand than _, so this option is set to off. '411', -- Redefining a local variable. '412', -- Redefining an argument. '422', -- Shadowing an argument '431', -- Shadowing a variable '122', -- Indirectly setting a readonly global } -- Global objects defined by the C code read_globals = { 'vim', 'Snacks', } ================================================ FILE: .pre-commit-config.yaml ================================================ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-yaml - id: end-of-file-fixer - id: trailing-whitespace - id: check-ast # 检查Python语法错误 - id: debug-statements # 检查是否有debug语句 - id: check-added-large-files - id: check-merge-conflict - repo: https://github.com/JohnnyMorganz/StyLua rev: v2.0.2 hooks: - id: stylua-system # or stylua-system / stylua-github files: \.lua$ - repo: https://github.com/Calinou/pre-commit-luacheck rev: v1.0.0 hooks: - id: luacheck - repo: https://github.com/doublify/pre-commit-rust rev: v1.0 hooks: - id: fmt files: \.rs$ - id: cargo-check args: ['--features', 'luajit'] files: \.rs$ - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.9 hooks: # 运行 Ruff linter - id: ruff args: [--fix] # 运行 Ruff formatter - id: ruff-format - repo: https://github.com/RobertCraigie/pyright-python rev: v1.1.395 hooks: - id: pyright additional_dependencies: - "types-setuptools" - "types-requests" ================================================ FILE: Build.ps1 ================================================ param ( [string]$Version = "luajit", [string]$BuildFromSource = "false" ) $Build = [System.Convert]::ToBoolean($BuildFromSource) $ErrorActionPreference = "Stop" $ProgressPreference = "SilentlyContinue" $BuildDir = "build" $REPO_OWNER = "yetone" $REPO_NAME = "avante.nvim" function Build-FromSource($feature) { if (-not (Test-Path $BuildDir)) { New-Item -ItemType Directory -Path $BuildDir | Out-Null } cargo build --release --features=$feature $SCRIPT_DIR = $PSScriptRoot $targetTokenizerFile = "avante_tokenizers.dll" $targetTemplatesFile = "avante_templates.dll" $targetRepoMapFile = "avante_repo_map.dll" Copy-Item (Join-Path $SCRIPT_DIR "target\release\avante_tokenizers.dll") (Join-Path $BuildDir $targetTokenizerFile) Copy-Item (Join-Path $SCRIPT_DIR "target\release\avante_templates.dll") (Join-Path $BuildDir $targetTemplatesFile) Copy-Item (Join-Path $SCRIPT_DIR "target\release\avante_repo_map.dll") (Join-Path $BuildDir $targetRepoMapFile) Remove-Item -Recurse -Force "target" } function Test-Command($cmdname) { return $null -ne (Get-Command $cmdname -ErrorAction SilentlyContinue) } function Test-GHAuth { try { $null = gh api user return $true } catch { return $false } } function Download-Prebuilt($feature, $tag) { $SCRIPT_DIR = $PSScriptRoot # Set the target directory to clone the artifact $TARGET_DIR = Join-Path $SCRIPT_DIR "build" # Set the platform to Windows $PLATFORM = "windows" $ARCH = "x86_64" if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") { $ARCH = "aarch64" } # Set the Lua version (lua51 or luajit) $LUA_VERSION = if ($feature) { $feature } else { "luajit" } # Set the artifact name pattern $ARTIFACT_NAME_PATTERN = "avante_lib-$PLATFORM-$ARCH-$LUA_VERSION" $TempFile = Get-Item ([System.IO.Path]::GetTempFilename()) | Rename-Item -NewName { $_.Name + ".zip" } -PassThru if ((Test-Command "gh") -and (Test-GHAuth)) { write-host "Using GitHub CLI to download artifacts..." gh release download $latestTag --repo "$REPO_OWNER/$REPO_NAME" --pattern "*$ARTIFACT_NAME_PATTERN*" --output $TempFile --clobber } else { # Get the artifact download URL $RELEASE = Invoke-RestMethod -Uri "https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$tag" $ARTIFACT_URL = $RELEASE.assets | Where-Object { $_.name -like "*$ARTIFACT_NAME_PATTERN*" } | Select-Object -ExpandProperty browser_download_url # Download and extract the artifact Invoke-WebRequest -Uri $ARTIFACT_URL -OutFile $TempFile } # Create target directory if it doesn't exist if (-not (Test-Path $TARGET_DIR)) { New-Item -ItemType Directory -Path $TARGET_DIR | Out-Null } Expand-Archive -Path $TempFile -DestinationPath $TARGET_DIR -Force Remove-Item $TempFile } function Main { Set-Location $PSScriptRoot if ($Build) { Write-Host "Building for $Version..." Build-FromSource $Version } else { $latestTag = git describe --tags --abbrev=0 2>&1 | Where-Object { $_ -ne $null } $builtTag = if (Test-Path "build/.tag") { Get-Content "build/.tag" } else { $null } function Save-Tag($tag) { $tag | Set-Content "build/.tag" } if ($latestTag -eq $builtTag -and $latestTag) { Write-Host "Local build is up to date. No download needed." } elseif ($latestTag -ne $builtTag -and $latestTag) { Write-Host "Downloading prebuilt binaries $latestTag for $Version..." Download-Prebuilt $Version $latestTag Save-Tag $latestTag } else { cargo build --release --features=$Version Get-ChildItem -Path "target/release/avante_*.dll" | ForEach-Object { Copy-Item $_.FullName "build/$($_.Name)" } Save-Tag $latestTag } } Write-Host "Completed!" } # Run the main function Main ================================================ FILE: Cargo.toml ================================================ [workspace] members = ["crates/*"] resolver = "2" [workspace.package] edition = "2021" rust-version = "1.80" license = "Apache-2.0" version = "0.1.0" [workspace.dependencies] avante-tokenizers = { path = "crates/avante-tokenizers" } avante-templates = { path = "crates/avante-templates" } avante-repo-map = { path = "crates/avante-repo-map" } avante-html2md = { path = "crates/avante-html2md" } minijinja = { version = "2.4.0", features = [ "loader", "json", "fuel", "unicode", "speedups", "custom_syntax", "loop_controls", ] } mlua = { version = "0.10.0", features = ["module", "serialize"] } tiktoken-rs = { version = "0.6.0" } tokenizers = { version = "0.20.0", features = [ "esaxx_fast", "http", "unstable_wasm", "onig", ], default-features = false } serde = { version = "1.0.209", features = ["derive"] } [workspace.lints.rust] unsafe_code = "warn" unreachable_pub = "warn" [workspace.lints.clippy] pedantic = { level = "warn", priority = -2 } # Allowed pedantic lints char_lit_as_u8 = "allow" collapsible_else_if = "allow" collapsible_if = "allow" implicit_hasher = "allow" map_unwrap_or = "allow" match_same_arms = "allow" missing_errors_doc = "allow" missing_panics_doc = "allow" module_name_repetitions = "allow" must_use_candidate = "allow" similar_names = "allow" too_many_lines = "allow" too_many_arguments = "allow" # Disallowed restriction lints print_stdout = "warn" print_stderr = "warn" dbg_macro = "warn" empty_drop = "warn" empty_structs_with_brackets = "warn" exit = "warn" get_unwrap = "warn" rc_buffer = "warn" rc_mutex = "warn" rest_pat_in_fully_bound_structs = "warn" ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: Makefile ================================================ UNAME := $(shell uname) ARCH := $(shell uname -m) ifeq ($(UNAME), Linux) OS := linux EXT := so else ifeq ($(UNAME), Darwin) OS := macOS EXT := dylib else $(error Unsupported operating system: $(UNAME)) endif LUA_VERSIONS := luajit lua51 BUILD_DIR := build BUILD_FROM_SOURCE ?= false TARGET_LIBRARY ?= all RAG_SERVICE_VERSION ?= 0.0.11 RAG_SERVICE_IMAGE := quay.io/yetoneful/avante-rag-service:$(RAG_SERVICE_VERSION) all: luajit define make_definitions ifeq ($(BUILD_FROM_SOURCE),true) ifeq ($(TARGET_LIBRARY), all) $1: $(BUILD_DIR)/libAvanteTokenizers-$1.$(EXT) $(BUILD_DIR)/libAvanteTemplates-$1.$(EXT) $(BUILD_DIR)/libAvanteRepoMap-$1.$(EXT) $(BUILD_DIR)/libAvanteHtml2md-$1.$(EXT) else ifeq ($(TARGET_LIBRARY), tokenizers) $1: $(BUILD_DIR)/libAvanteTokenizers-$1.$(EXT) else ifeq ($(TARGET_LIBRARY), templates) $1: $(BUILD_DIR)/libAvanteTemplates-$1.$(EXT) else ifeq ($(TARGET_LIBRARY), repo-map) $1: $(BUILD_DIR)/libAvanteRepoMap-$1.$(EXT) else ifeq ($(TARGET_LIBRARY), html2md) $1: $(BUILD_DIR)/libAvanteHtml2md-$1.$(EXT) else $$(error TARGET_LIBRARY must be one of all, tokenizers, templates, repo-map, html2md) endif else $1: LUA_VERSION=$1 bash ./build.sh endif endef $(foreach lua_version,$(LUA_VERSIONS),$(eval $(call make_definitions,$(lua_version)))) define build_package $1-$2: cargo build --release --features=$1 -p avante-$2 cp target/release/libavante_$(shell echo $2 | tr - _).$(EXT) $(BUILD_DIR)/avante_$(shell echo $2 | tr - _).$(EXT) endef define build_targets $(BUILD_DIR)/libAvanteTokenizers-$1.$(EXT): $(BUILD_DIR) $1-tokenizers $(BUILD_DIR)/libAvanteTemplates-$1.$(EXT): $(BUILD_DIR) $1-templates $(BUILD_DIR)/libAvanteRepoMap-$1.$(EXT): $(BUILD_DIR) $1-repo-map $(BUILD_DIR)/libAvanteHtml2md-$1.$(EXT): $(BUILD_DIR) $1-html2md endef $(foreach lua_version,$(LUA_VERSIONS),$(eval $(call build_package,$(lua_version),tokenizers))) $(foreach lua_version,$(LUA_VERSIONS),$(eval $(call build_package,$(lua_version),templates))) $(foreach lua_version,$(LUA_VERSIONS),$(eval $(call build_package,$(lua_version),repo-map))) $(foreach lua_version,$(LUA_VERSIONS),$(eval $(call build_package,$(lua_version),html2md))) $(foreach lua_version,$(LUA_VERSIONS),$(eval $(call build_targets,$(lua_version)))) $(BUILD_DIR): @mkdir -p $(BUILD_DIR) clean: @rm -rf $(BUILD_DIR) luacheck: @luacheck `find \( -path './target' -prune \) -o -name "*.lua" -print` --codes luastylecheck: @stylua --check lua/ plugin/ tests/ stylefix: @stylua lua/ plugin/ .PHONY: ruststylecheck ruststylecheck: @rustup component add rustfmt 2> /dev/null @cargo fmt --all -- --check .PHONY: rustlint rustlint: @rustup component add clippy 2> /dev/null @cargo clippy -F luajit --all -- -F clippy::dbg-macro -D warnings .PHONY: rusttest rusttest: @cargo test --features luajit .PHONY: luatest luatest: @./scripts/run-luatest.sh .PHONY: lint lint: luacheck luastylecheck ruststylecheck rustlint .PHONY: lua-typecheck lua-typecheck: @./scripts/lua-typecheck.sh .PHONY: build-image build-image: docker build --platform=linux/amd64 -t $(RAG_SERVICE_IMAGE) -f py/rag-service/Dockerfile py/rag-service .PHONY: push-image push-image: build-image docker push $(RAG_SERVICE_IMAGE) ================================================ FILE: README.md ================================================
logo

avante.nvim

Neovim: v0.10+ Lua CI status Rust CI status pre-commit status Discord

**avante.nvim** is a Neovim plugin designed to emulate the behaviour of the [Cursor](https://www.cursor.com) AI IDE. It provides users with AI-driven code suggestions and the ability to apply these recommendations directly to their source files with minimal effort. [查看中文版](README_zh.md) > [!NOTE] > > 🥰 This project is undergoing rapid iterations, and many exciting features will be added successively. Stay tuned! ## Sponsorship ❤️ If you like this project, please consider supporting me on Patreon, as it helps me to continue maintaining and improving it: [Sponsor me](https://patreon.com/yetone) ## Features - **AI-Powered Code Assistance**: Interact with AI to ask questions about your current code file and receive intelligent suggestions for improvement or modification. - **One-Click Application**: Quickly apply the AI's suggested changes to your source code with a single command, streamlining the editing process and saving time. - **Project-Specific Instruction Files**: Customize AI behavior by adding a markdown file (`avante.md` by default) in the project root. This file is automatically referenced during workspace changes. You can also configure a custom file name for tailored project instructions. ## Avante Zen Mode Due to the prevalence of claude code, it is clear that this is an era of Coding Agent CLIs. As a result, there are many arguments like: in the Vibe Coding era, editors are no longer needed; you only need to use the CLI in the terminal. But have people realized that for more than half a century, Terminal-based Editors have solved and standardized the biggest problem with Terminal-based applications — that is, the awkward TUI interactions! No matter how much these Coding Agent CLIs optimize their UI/UX, their UI/UX will always be a subset of Terminal-based Editors (Vim, Emacs)! They cannot achieve Vim’s elegant action + text objects abstraction (imagine how you usually edit large multi-line prompts in an Agent CLI), nor can they leverage thousands of mature Vim/Neovim plugins to help optimize TUI UI/UX—such as easymotions and so on. Moreover, when they want to view or modify code, they often have to jump into other applications which forcibly interrupts the UI/UX experience. Therefore, Avante’s Zen Mode was born! It looks like a Vibe Coding Agent CLI but it is completely Neovim underneath. So you can use your muscle-memory Vim operations and those rich and mature Neovim plugins on it. At the same time, by leveraging [ACP](https://github.com/yetone/avante.nvim#acp-support) it has all capabilities of claude code / gemini-cli / codex! Why not enjoy both? Now all you need to do is alias this command to avante; then every time you simply type avante just like using claude code and enter Avante’s Zen Mode! ```bash alias avante='nvim -c "lua vim.defer_fn(function()require(\"avante.api\").zen_mode()end, 100)"' ``` The effect is as follows: Avante Zen Mode ## Project instructions with avante.md
The `avante.md` file allows you to provide project-specific context and instructions to the ai. this file should be placed in your project root and will be automatically referenced during all interactions with avante. ### Best practices for avante.md to get the most out of your project instruction file, consider following this structure: #### Your role define the ai's persona and expertise level for your project: ```markdown ### your role you are an expert senior software engineer specializing in [technology stack]. you have deep knowledge of [specific frameworks/tools] and understand best practices for [domain/industry]. you write clean, maintainable, and well-documented code. you prioritize code quality, performance, and security in all your recommendations. ``` #### Your mission clearly describe what the ai should focus on and how it should help: ```markdown ### your mission your primary goal is to help build and maintain [project description]. you should: - provide code suggestions that follow our established patterns and conventions - help debug issues by analyzing code and suggesting solutions - assist with refactoring to improve code quality and maintainability - suggest optimizations for performance and scalability - ensure all code follows our security guidelines - help write comprehensive tests for new features ``` #### Additional sections to consider - **project context**: brief description of the project, its goals, and target users - **technology stack**: list of technologies, frameworks, and tools used - **coding standards**: specific conventions, style guides, and patterns to follow - **architecture guidelines**: how components should interact and be organized - **testing requirements**: testing strategies and coverage expectations - **security considerations**: specific security requirements or constraints ### example avante.md ```markdown # project instructions for myapp ## your role you are an expert full-stack developer specializing in react, node.js, and typescript. you understand modern web development practices and have experience with our tech stack. ## your mission help build a scalable e-commerce platform by: - writing type-safe typescript code - following react best practices and hooks patterns - implementing restful apis with proper error handling - ensuring responsive design with tailwind css - writing comprehensive unit and integration tests ## project context myapp is a modern e-commerce platform targeting small businesses. we prioritize performance, accessibility, and user experience. ## technology stack - frontend: react 18, typescript, tailwind css, vite - backend: node.js, express, prisma, postgresql - testing: jest, react testing library, playwright - deployment: docker, aws ## coding standards - use functional components with hooks - prefer composition over inheritance - write self-documenting code with clear variable names - add jsdoc comments for complex functions - follow the existing folder structure and naming conventions ```
## Installation For building binary if you wish to build from source, then `cargo` is required. Otherwise `curl` and `tar` will be used to get prebuilt binary from GitHub.
lazy.nvim (recommended) ```lua { "yetone/avante.nvim", -- if you want to build from source then do `make BUILD_FROM_SOURCE=true` -- ⚠️ must add this setting! ! ! build = vim.fn.has("win32") ~= 0 and "powershell -ExecutionPolicy Bypass -File Build.ps1 -BuildFromSource false" or "make", event = "VeryLazy", version = false, -- Never set this value to "*"! Never! ---@module 'avante' ---@type avante.Config opts = { -- add any opts here -- this file can contain specific instructions for your project instructions_file = "avante.md", -- for example provider = "claude", providers = { claude = { endpoint = "https://api.anthropic.com", model = "claude-sonnet-4-20250514", timeout = 30000, -- Timeout in milliseconds extra_request_body = { temperature = 0.75, max_tokens = 20480, }, }, moonshot = { endpoint = "https://api.moonshot.ai/v1", model = "kimi-k2-0711-preview", timeout = 30000, -- Timeout in milliseconds extra_request_body = { temperature = 0.75, max_tokens = 32768, }, }, }, }, dependencies = { "nvim-lua/plenary.nvim", "MunifTanjim/nui.nvim", --- The below dependencies are optional, "nvim-mini/mini.pick", -- for file_selector provider mini.pick "nvim-telescope/telescope.nvim", -- for file_selector provider telescope "hrsh7th/nvim-cmp", -- autocompletion for avante commands and mentions "ibhagwan/fzf-lua", -- for file_selector provider fzf "stevearc/dressing.nvim", -- for input provider dressing "folke/snacks.nvim", -- for input provider snacks "nvim-tree/nvim-web-devicons", -- or echasnovski/mini.icons "zbirenbaum/copilot.lua", -- for providers='copilot' { -- support for image pasting "HakonHarnes/img-clip.nvim", event = "VeryLazy", opts = { -- recommended settings default = { embed_image_as_base64 = false, prompt_for_file_name = false, drag_and_drop = { insert_mode = true, }, -- required for Windows users use_absolute_path = true, }, }, }, { -- Make sure to set this up properly if you have lazy=true 'MeanderingProgrammer/render-markdown.nvim', opts = { file_types = { "markdown", "Avante" }, }, ft = { "markdown", "Avante" }, }, }, } ```
vim-plug ```vim call plug#begin() " Deps Plug 'nvim-lua/plenary.nvim' Plug 'MunifTanjim/nui.nvim' Plug 'MeanderingProgrammer/render-markdown.nvim' " Optional deps Plug 'hrsh7th/nvim-cmp' Plug 'nvim-tree/nvim-web-devicons' "or Plug 'echasnovski/mini.icons' Plug 'HakonHarnes/img-clip.nvim' Plug 'zbirenbaum/copilot.lua' Plug 'stevearc/dressing.nvim' " for enhanced input UI Plug 'folke/snacks.nvim' " for modern input UI " Yay, pass source=true if you want to build from source Plug 'yetone/avante.nvim', { 'branch': 'main', 'do': 'make' } call plug#end() autocmd! User avante.nvim lua << EOF require('avante').setup({}) EOF ```
mini.deps ```lua local add, later, now = MiniDeps.add, MiniDeps.later, MiniDeps.now add({ source = 'yetone/avante.nvim', monitor = 'main', depends = { 'nvim-lua/plenary.nvim', 'MunifTanjim/nui.nvim', 'echasnovski/mini.icons' }, hooks = { post_checkout = function() vim.cmd('make') end } }) --- optional add({ source = 'hrsh7th/nvim-cmp' }) add({ source = 'zbirenbaum/copilot.lua' }) add({ source = 'HakonHarnes/img-clip.nvim' }) add({ source = 'MeanderingProgrammer/render-markdown.nvim' }) later(function() require('render-markdown').setup({...}) end) later(function() require('img-clip').setup({...}) -- config img-clip require("copilot").setup({...}) -- setup copilot to your liking require("avante").setup({...}) -- config for avante.nvim end) ```
Packer ```vim -- Required plugins use 'nvim-lua/plenary.nvim' use 'MunifTanjim/nui.nvim' use 'MeanderingProgrammer/render-markdown.nvim' -- Optional dependencies use 'hrsh7th/nvim-cmp' use 'nvim-tree/nvim-web-devicons' -- or use 'echasnovski/mini.icons' use 'HakonHarnes/img-clip.nvim' use 'zbirenbaum/copilot.lua' use 'stevearc/dressing.nvim' -- for enhanced input UI use 'folke/snacks.nvim' -- for modern input UI -- Avante.nvim with build process use { 'yetone/avante.nvim', branch = 'main', run = 'make', config = function() require('avante').setup() end } ```
Home Manager ```nix programs.neovim = { plugins = [ { plugin = pkgs.vimPlugins.avante-nvim; type = "lua"; config = '' require("avante_lib").load() require("avante").setup() '' # or builtins.readFile ./plugins/avante.lua; } ]; }; ```
Nixvim ```nix plugins.avante.enable = true; plugins.avante.settings = { # setup options here }; ```
Lua ```lua -- deps: require('cmp').setup ({ -- use recommended settings from above }) require('img-clip').setup ({ -- use recommended settings from above }) require('copilot').setup ({ -- use recommended settings from above }) require('render-markdown').setup ({ -- use recommended settings from above }) require('avante').setup({ -- Example: Using snacks.nvim as input provider input = { provider = "snacks", -- "native" | "dressing" | "snacks" provider_opts = { -- Snacks input configuration title = "Avante Input", icon = " ", placeholder = "Enter your API key...", }, }, -- Your other config here! }) ```
> [!IMPORTANT] > > `avante.nvim` is currently only compatible with Neovim 0.10.1 or later. Please ensure that your Neovim version meets these requirements before proceeding. > [!NOTE] > > When loading the plugin synchronously, we recommend `require`ing it sometime after your colorscheme. > [!NOTE] > > Recommended **Neovim** options: > > ```lua > -- views can only be fully collapsed with the global statusline > vim.opt.laststatus = 3 > ``` > [!TIP] > > Any rendering plugins that support markdown should work with Avante as long as you add the supported filetype `Avante`. See and [this comment](https://github.com/yetone/avante.nvim/issues/175#issuecomment-2313749363) for more information. ### Default setup configuration _See [config.lua#L9](./lua/avante/config.lua) for the full config_
Default configuration ```lua { ---@alias Provider "claude" | "openai" | "azure" | "gemini" | "cohere" | "copilot" | string ---@type Provider provider = "claude", -- The provider used in Aider mode or in the planning phase of Cursor Planning Mode ---@alias Mode "agentic" | "legacy" ---@type Mode mode = "agentic", -- The default mode for interaction. "agentic" uses tools to automatically generate code, "legacy" uses the old planning method to generate code. -- WARNING: Since auto-suggestions are a high-frequency operation and therefore expensive, -- currently designating it as `copilot` provider is dangerous because: https://github.com/yetone/avante.nvim/issues/1048 -- Of course, you can reduce the request frequency by increasing `suggestion.debounce`. auto_suggestions_provider = "claude", providers = { claude = { endpoint = "https://api.anthropic.com", auth_type = "api" -- Set to "max" to sign in with Claude Pro/Max subscription model = "claude-3-5-sonnet-20241022", extra_request_body = { temperature = 0.75, max_tokens = 4096, }, }, }, ---Specify the special dual_boost mode ---1. enabled: Whether to enable dual_boost mode. Default to false. ---2. first_provider: The first provider to generate response. Default to "openai". ---3. second_provider: The second provider to generate response. Default to "claude". ---4. prompt: The prompt to generate response based on the two reference outputs. ---5. timeout: Timeout in milliseconds. Default to 60000. ---How it works: --- When dual_boost is enabled, avante will generate two responses from the first_provider and second_provider respectively. Then use the response from the first_provider as provider1_output and the response from the second_provider as provider2_output. Finally, avante will generate a response based on the prompt and the two reference outputs, with the default Provider as normal. ---Note: This is an experimental feature and may not work as expected. dual_boost = { enabled = false, first_provider = "openai", second_provider = "claude", prompt = "Based on the two reference outputs below, generate a response that incorporates elements from both but reflects your own judgment and unique perspective. Do not provide any explanation, just give the response directly. Reference Output 1: [{{provider1_output}}], Reference Output 2: [{{provider2_output}}]", timeout = 60000, -- Timeout in milliseconds }, behaviour = { auto_suggestions = false, -- Experimental stage auto_set_highlight_group = true, auto_set_keymaps = true, auto_apply_diff_after_generation = false, support_paste_from_clipboard = false, minimize_diff = true, -- Whether to remove unchanged lines when applying a code block enable_token_counting = true, -- Whether to enable token counting. Default to true. auto_add_current_file = true, -- Whether to automatically add the current file when opening a new chat. Default to true. auto_approve_tool_permissions = true, -- Default: auto-approve all tools (no prompts) -- Examples: -- auto_approve_tool_permissions = false, -- Show permission prompts for all tools -- auto_approve_tool_permissions = {"bash", "str_replace"}, -- Auto-approve specific tools only ---@type "popup" | "inline_buttons" confirmation_ui_style = "inline_buttons", --- Whether to automatically open files and navigate to lines when ACP agent makes edits ---@type boolean acp_follow_agent_locations = true, }, prompt_logger = { -- logs prompts to disk (timestamped, for replay/debugging) enabled = true, -- toggle logging entirely log_dir = vim.fn.stdpath("cache") .. "/avante_prompts", -- directory where logs are saved fortune_cookie_on_success = false, -- shows a random fortune after each logged prompt (requires `fortune` installed) next_prompt = { normal = "", -- load the next (newer) prompt log in normal mode insert = "", }, prev_prompt = { normal = "", -- load the previous (older) prompt log in normal mode insert = "", }, }, mappings = { --- @class AvanteConflictMappings diff = { ours = "co", theirs = "ct", all_theirs = "ca", both = "cb", cursor = "cc", next = "]x", prev = "[x", }, suggestion = { accept = "", next = "", prev = "", dismiss = "", }, jump = { next = "]]", prev = "[[", }, submit = { normal = "", insert = "", }, cancel = { normal = { "", "", "q" }, insert = { "" }, }, sidebar = { apply_all = "A", apply_cursor = "a", retry_user_request = "r", edit_user_request = "e", switch_windows = "", reverse_switch_windows = "", remove_file = "d", add_file = "@", close = { "", "q" }, close_from_input = nil, -- e.g., { normal = "", insert = "" } }, }, selection = { enabled = true, hint_display = "delayed", }, windows = { ---@type "right" | "left" | "top" | "bottom" position = "right", -- the position of the sidebar wrap = true, -- similar to vim.o.wrap width = 30, -- default % based on available width sidebar_header = { enabled = true, -- true, false to enable/disable the header align = "center", -- left, center, right for title rounded = true, }, spinner = { editing = { "⡀", "⠄", "⠂", "⠁", "⠈", "⠐", "⠠", "⢀", "⣀", "⢄", "⢂", "⢁", "⢈", "⢐", "⢠", "⣠", "⢤", "⢢", "⢡", "⢨", "⢰", "⣰", "⢴", "⢲", "⢱", "⢸", "⣸", "⢼", "⢺", "⢹", "⣹", "⢽", "⢻", "⣻", "⢿", "⣿" }, generating = { "·", "✢", "✳", "∗", "✻", "✽" }, -- Spinner characters for the 'generating' state thinking = { "🤯", "🙄" }, -- Spinner characters for the 'thinking' state }, input = { prefix = "> ", height = 8, -- Height of the input window in vertical layout }, edit = { border = "rounded", start_insert = true, -- Start insert mode when opening the edit window }, ask = { floating = false, -- Open the 'AvanteAsk' prompt in a floating window start_insert = true, -- Start insert mode when opening the ask window border = "rounded", ---@type "ours" | "theirs" focus_on_apply = "ours", -- which diff to focus after applying }, }, highlights = { ---@type AvanteConflictHighlights diff = { current = "DiffText", incoming = "DiffAdd", }, }, --- @class AvanteConflictUserConfig diff = { autojump = true, ---@type string | fun(): any list_opener = "copen", --- Override the 'timeoutlen' setting while hovering over a diff (see :help timeoutlen). --- Helps to avoid entering operator-pending mode with diff mappings starting with `c`. --- Disable by setting to -1. override_timeoutlen = 500, }, suggestion = { debounce = 600, throttle = 600, }, } ```
## Blink.cmp users For blink cmp users (nvim-cmp alternative) view below instruction for configuration This is achieved by emulating nvim-cmp using blink.compat or you can use [Kaiser-Yang/blink-cmp-avante](https://github.com/Kaiser-Yang/blink-cmp-avante).
Lua ```lua selector = { --- @alias avante.SelectorProvider "native" | "fzf_lua" | "mini_pick" | "snacks" | "telescope" | fun(selector: avante.ui.Selector): nil --- @type avante.SelectorProvider provider = "fzf", -- Options override for custom providers provider_opts = {}, } ``` To create a customized selector provider, you can specify a customized function to launch a picker to select items and pass the selected items to the `on_select` callback. ```lua selector = { ---@param selector avante.ui.Selector provider = function(selector) local items = selector.items ---@type avante.ui.SelectorItem[] local title = selector.title ---@type string local on_select = selector.on_select ---@type fun(selected_item_ids: string[]|nil): nil --- your customized picker logic here end, } ``` ### Input Provider Configuration Avante.nvim supports multiple input providers for user input (like API key entry). You can configure which provider to use:
Native Input Provider (Default) ```lua { input = { provider = "native", -- Uses vim.ui.input provider_opts = {}, } } ```
Dressing.nvim Input Provider For enhanced input UI with better styling and features: ```lua { input = { provider = "dressing", provider_opts = {}, } } ``` You'll need to install dressing.nvim: ```lua -- With lazy.nvim { "stevearc/dressing.nvim" } ```
Snacks.nvim Input Provider (Recommended) For modern, feature-rich input UI: ```lua { input = { provider = "snacks", provider_opts = { -- Additional snacks.input options title = "Avante Input", icon = " ", }, } } ``` You'll need to install snacks.nvim: ```lua -- With lazy.nvim { "folke/snacks.nvim" } ```
Custom Input Provider To create a customized input provider, you can specify a function: ```lua { input = { ---@param input avante.ui.Input provider = function(input) local title = input.title ---@type string local default = input.default ---@type string local conceal = input.conceal ---@type boolean local on_submit = input.on_submit ---@type fun(result: string|nil): nil --- your customized input logic here end, } } ```
Choose a selector other that native, the default as that currently has an issue For lazyvim users copy the full config for blink.cmp from the website or extend the options ```lua compat = { "avante_commands", "avante_mentions", "avante_files", } ``` For other users just add a custom provider ### Available Completion Sources Avante.nvim provides several completion sources that can be integrated with blink.cmp: #### Mentions (`@` trigger) Mentions allow you to quickly reference specific features or add files to the chat context: - `@codebase` - Enable project context and repository mapping - `@diagnostics` - Enable diagnostics information - `@file` - Open file selector to add files to chat context - `@quickfix` - Add files from quickfix list to chat context - `@buffers` - Add open buffers to chat context #### Slash Commands (`/` trigger) Built-in slash commands for common operations: - `/help` - Show help message with available commands - `/init` - Initialize AGENTS.md based on current project - `/clear` - Clear chat history - `/new` - Start a new chat - `/compact` - Compact history messages to save tokens - `/lines - ` - Ask about specific lines - `/commit` - Generate commit message for changes #### Shortcuts (`#` trigger) Shortcuts provide quick access to predefined prompt templates. You can customize these in your config: ```lua { shortcuts = { { name = "refactor", description = "Refactor code with best practices", details = "Automatically refactor code to improve readability, maintainability, and follow best practices while preserving functionality", prompt = "Please refactor this code following best practices, improving readability and maintainability while preserving functionality." }, { name = "test", description = "Generate unit tests", details = "Create comprehensive unit tests covering edge cases, error scenarios, and various input conditions", prompt = "Please generate comprehensive unit tests for this code, covering edge cases and error scenarios." }, -- Add more custom shortcuts... } } ``` When you type `#refactor` in the input, it will automatically be replaced with the corresponding prompt text. ### Configuration Example Here's a complete blink.cmp configuration example with all Avante sources: ```lua default = { ... "avante_commands", "avante_mentions", "avante_shortcuts", "avante_files", } ``` ```lua providers = { avante_commands = { name = "avante_commands", module = "blink.compat.source", score_offset = 90, -- show at a higher priority than lsp opts = {}, }, avante_files = { name = "avante_files", module = "blink.compat.source", score_offset = 100, -- show at a higher priority than lsp opts = {}, }, avante_mentions = { name = "avante_mentions", module = "blink.compat.source", score_offset = 1000, -- show at a higher priority than lsp opts = {}, }, avante_shortcuts = { name = "avante_shortcuts", module = "blink.compat.source", score_offset = 1000, -- show at a higher priority than lsp opts = {}, } ... } ```
## Usage ### Using Claude Pro/Max Subscription To login with your Claude subscription, set the **auth_type** of the Claude provider entry in your config to "max", re-open Neovim then the authentication process will start in your browser. Once logged in and authorized, a code will show that needs to be copied into the prompt in Neovim, which should then give access to use your subscription with Avante. You may need to run `AvanteSwitchProvider claude` to initiate the authentication if you previously had a different provider selected. ```lua -- Providers = { ... claude = { -- ... auth_type = "max", }, ``` ### Basic Functionality Given its early stage, `avante.nvim` currently supports the following basic functionalities: > [!IMPORTANT] > > For most consistency between neovim session, it is recommended to set the environment variables in your shell file. > By default, `Avante` will prompt you at startup to input the API key for the provider you have selected. > > **Scoped API Keys (Recommended for Isolation)** > > Avante now supports scoped API keys, allowing you to isolate API keys specifically for Avante without affecting other applications. Simply prefix any API key with `AVANTE_`: > > ```sh > # Scoped keys (recommended) > export AVANTE_ANTHROPIC_API_KEY=your-claude-api-key > export AVANTE_OPENAI_API_KEY=your-openai-api-key > export AVANTE_AZURE_OPENAI_API_KEY=your-azure-api-key > export AVANTE_GEMINI_API_KEY=your-gemini-api-key > export AVANTE_CO_API_KEY=your-cohere-api-key > export AVANTE_AIHUBMIX_API_KEY=your-aihubmix-api-key > export AVANTE_MOONSHOT_API_KEY=your-moonshot-api-key > ``` > > **Global API Keys (Legacy)** > > You can still use the traditional global API keys if you prefer: > > For Claude: > > ```sh > export ANTHROPIC_API_KEY=your-api-key > ``` > > For OpenAI: > > ```sh > export OPENAI_API_KEY=your-api-key > ``` > > For Azure OpenAI: > > ```sh > export AZURE_OPENAI_API_KEY=your-api-key > ``` > > For Amazon Bedrock: > > You can specify the `BEDROCK_KEYS` environment variable to set credentials. When this variable is not specified, bedrock will use the default AWS credentials chain (see below). > > ```sh > export BEDROCK_KEYS=aws_access_key_id,aws_secret_access_key,aws_region[,aws_session_token] > ``` > > Note: The aws_session_token is optional and only needed when using temporary AWS credentials > > Alternatively Bedrock tries to resolve AWS credentials using the [Default Credentials Provider Chain](https://docs.aws.amazon.com/cli/v1/userguide/cli-chap-authentication.html). > This means you can have credentials e.g. configured via the AWS CLI, stored in your ~/.aws/profile, use AWS SSO etc. > In this case `aws_region` and optionally `aws_profile` should be specified via the bedrock config, e.g.: > > ```lua > bedrock = { > model = "us.anthropic.claude-3-5-sonnet-20241022-v2:0", > aws_profile = "bedrock", > aws_region = "us-east-1", > }, > ``` > > Note: Bedrock requires the [AWS CLI](https://aws.amazon.com/cli/) to be installed on your system. 1. Open a code file in Neovim. 2. Use the `:AvanteAsk` command to query the AI about the code. 3. Review the AI's suggestions. 4. Apply the recommended changes directly to your code with a simple command or key binding. **Note**: The plugin is still under active development, and both its functionality and interface are subject to significant changes. Expect some rough edges and instability as the project evolves. ## Key Bindings The following key bindings are available for use with `avante.nvim`: | Key Binding | Description | | ----------------------------------------- | -------------------------------------- | | **Sidebar** | | | ]p | next prompt | | [p | previous prompt | | A | apply all | | a | apply cursor | | r | retry user request | | e | edit user request | | <Tab> | switch windows | | <S-Tab> | reverse switch windows | | d | remove file | | @ | add file | | q | close sidebar | | Leaderaa | show sidebar | | Leaderat | toggle sidebar visibility | | Leaderar | refresh sidebar | | Leaderaf | switch sidebar focus | | **Suggestion** | | | Leadera? | select model | | Leaderan | new ask | | Leaderae | edit selected blocks | | LeaderaS | stop current AI request | | Leaderah | select between chat histories | | <M-l> | accept suggestion | | <M-]> | next suggestion | | <M-[> | previous suggestion | | <C-]> | dismiss suggestion | | Leaderad | toggle debug mode | | Leaderas | toggle suggestion display | | LeaderaR | toggle repomap | | **Files** | | | Leaderac | add current buffer to selected files | | LeaderaB | add all buffer files to selected files | | **Diff** | | | co | choose ours | | ct | choose theirs | | ca | choose all theirs | | cb | choose both | | cc | choose cursor | | ]x | move to next conflict | | [x | move to previous conflict | | **Confirm** | | | Ctrlwf | focus confirm window | | c | confirm code | | r | confirm response | | i | confirm input | > [!NOTE] > > If you are using `lazy.nvim`, then all keymap here will be safely set, meaning if `aa` is already binded, then avante.nvim won't bind this mapping. > In this case, user will be responsible for setting up their own. See [notes on keymaps](https://github.com/yetone/avante.nvim/wiki#keymaps-and-api-i-guess) for more details. ### Neotree shortcut In the neotree sidebar, you can also add a new keyboard shortcut to quickly add `file/folder` to `Avante Selected Files`.
Neotree configuration ```lua return { { 'nvim-neo-tree/neo-tree.nvim', config = function() require('neo-tree').setup({ filesystem = { commands = { avante_add_files = function(state) local node = state.tree:get_node() local filepath = node:get_id() local relative_path = require('avante.utils').relative_path(filepath) local sidebar = require('avante').get() local open = sidebar:is_open() -- ensure avante sidebar is open if not open then require('avante.api').ask() sidebar = require('avante').get() end sidebar.file_selector:add_selected_file(relative_path) -- remove neo tree buffer if not open then sidebar.file_selector:remove_selected_file('neo-tree filesystem [1]') end end, }, window = { mappings = { ['oa'] = 'avante_add_files', }, }, }, }) end, }, } ```
## Commands | Command | Description | Examples | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | `:AvanteAsk [question] [position]` | Ask AI about your code. Optional `position` set window position and `ask` enable/disable direct asking mode | `:AvanteAsk position=right Refactor this code here` | | `:AvanteBuild` | Build dependencies for the project | | | `:AvanteChat` | Start a chat session with AI about your codebase. Default is `ask`=false | | | `:AvanteChatNew` | Start a new chat session. The current chat can be re-opened with the chat session selector | | | `:AvanteHistory` | Opens a picker for your previous chat sessions | | | `:AvanteClear` | Clear the chat history for your current chat session | | | `:AvanteEdit` | Edit the selected code blocks | | | `:AvanteFocus` | Switch focus to/from the sidebar | | | `:AvanteRefresh` | Refresh all Avante windows | | | `:AvanteStop` | Stop the current AI request | | | `:AvanteSwitchProvider` | Switch AI provider (e.g. openai) | | | `:AvanteShowRepoMap` | Show repo map for project's structure | | | `:AvanteToggle` | Toggle the Avante sidebar | | | `:AvanteModels` | Show model list | | | `:AvanteSwitchSelectorProvider` | Switch avante selector provider (e.g. native, telescope, fzf_lua, mini_pick, snacks) | | ## Highlight Groups | Highlight Group | Description | Notes | | --------------------------- | --------------------------------------------- | -------------------------------------------- | | AvanteTitle | Title | | | AvanteReversedTitle | Used for rounded border | | | AvanteSubtitle | Selected code title | | | AvanteReversedSubtitle | Used for rounded border | | | AvanteThirdTitle | Prompt title | | | AvanteReversedThirdTitle | Used for rounded border | | | AvanteConflictCurrent | Current conflict highlight | Default to `Config.highlights.diff.current` | | AvanteConflictIncoming | Incoming conflict highlight | Default to `Config.highlights.diff.incoming` | | AvanteConflictCurrentLabel | Current conflict label highlight | Default to shade of `AvanteConflictCurrent` | | AvanteConflictIncomingLabel | Incoming conflict label highlight | Default to shade of `AvanteConflictIncoming` | | AvantePopupHint | Usage hints in popup menus | | | AvanteInlineHint | The end-of-line hint displayed in visual mode | | | AvantePromptInput | The body highlight of the prompt input | | | AvantePromptInputBorder | The border highlight of the prompt input | Default to `NormalFloat` | See [highlights.lua](./lua/avante/highlights.lua) for more information ## Fast Apply Fast Apply is a feature that enables instant code edits with high accuracy by leveraging specialized models. It replicates Cursor's instant apply functionality, allowing for seamless code modifications without the typical delays associated with traditional code generation. ### Purpose and Benefits Fast Apply addresses the common pain point of slow code application in AI-assisted development. Instead of waiting for a full language model to process and apply changes, Fast Apply uses a specialized "apply model" that can quickly and accurately merge code edits with 96-98% accuracy at speeds of 2500-4500+ tokens per second. Key benefits: - **Instant application**: Code changes are applied immediately without noticeable delays - **High accuracy**: Specialized models achieve 96-98% accuracy for code edits - **Seamless workflow**: Maintains the natural flow of development without interruptions - **Large context support**: Handles up to 16k tokens for both input and output ### Configuration To enable Fast Apply, you need to: 1. **Enable Fast Apply in your configuration**: ```lua behaviour = { enable_fastapply = true, -- Enable Fast Apply feature }, -- ... other configuration ``` 2. **Get your Morph API key**: Go to [morphllm.com](https://morphllm.com/api-keys) and create an account and get the API key. 3. **Set your Morph API key**: ```bash export MORPH_API_KEY="your-api-key" ``` 4. **Change Morph model**: ```lua providers = { morph = { model = "morph-v3-large", }, } ``` ### Model Options Morph provides different models optimized for different use cases: | Model | Speed | Accuracy | Context Limit | | ---------------- | ----------------- | -------- | ------------- | | `morph-v3-fast` | 4500+ tok/sec | 96% | 16k tokens | | `morph-v3-large` | 2500+ tok/sec | 98% | 16k tokens | | `auto` | 2500-4500 tok/sec | 98% | 16k tokens | ### How It Works When Fast Apply is enabled and a Morph provider is configured, avante.nvim will: 1. Use the `edit_file` tool for code modifications instead of traditional tools 2. Send the original code, edit instructions, and update snippet to the Morph API 3. Receive the fully merged code back from the specialized apply model 4. Apply the changes directly to your files with high accuracy The process uses a specialized prompt format that includes: - ``: Clear description of what changes to make - ``: The original code content - ``: The specific changes using truncation markers (`// ... existing code ...`) This approach ensures that the apply model can quickly and accurately merge your changes without the overhead of full code generation. ## Ollama Ollama is a first-class provider for avante.nvim. To start using it you need to set `provider = "ollama"` in the configuration, set the `model` field in `ollama` to the model you want to use. Ollama is disabled by default, you need to provide an implementation for its `is_env_set` method to properly enable it. For example: ```lua provider = "ollama", providers = { ollama = { model = "qwq:32b", is_env_set = require("avante.providers.ollama").check_endpoint_alive, }, } ``` ## ACP Support Avante.nvim now supports the [Agent Client Protocol (ACP)](https://agentclientprotocol.com/overview/introduction), enabling seamless integration with AI agents that follow this standardized communication protocol. ACP provides a unified way for AI agents to interact with development environments, offering enhanced capabilities for code editing, file operations, and tool execution. ### What is ACP? The Agent Client Protocol (ACP) is a standardized protocol that enables AI agents to communicate with development tools and environments. It provides: - **Standardized Communication**: A unified JSON-RPC based protocol for agent-client interactions - **Tool Integration**: Support for various development tools like file operations, code execution, and search - **Session Management**: Persistent sessions that maintain context across interactions - **Permission System**: Granular control over what agents can access and modify ### Enabling ACP To use ACP-compatible agents with Avante.nvim, you need to configure an ACP provider. Here are the currently supported ACP agents: #### Gemini CLI with ACP ```lua { provider = "gemini-cli", -- other configuration options... } ``` #### Claude Code with ACP ```lua { provider = "claude-code", -- other configuration options... } ``` #### Goose with ACP ```lua { provider = "goose", -- other configuration options... } ``` #### Codex with ACP ```lua { provider = "codex", -- other configuration options... } ``` #### Kimi CLI with ACP ```lua { provider = "kimi-cli", -- other configuration options... } ``` ### ACP Configuration ACP providers are configured in the `acp_providers` section of your configuration: ```lua { acp_providers = { ["gemini-cli"] = { command = "gemini", args = { "--experimental-acp" }, env = { NODE_NO_WARNINGS = "1", GEMINI_API_KEY = os.getenv("GEMINI_API_KEY"), }, }, ["claude-code"] = { command = "npx", args = { "@zed-industries/claude-code-acp" }, env = { NODE_NO_WARNINGS = "1", ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY"), }, }, ["goose"] = { command = "goose", args = { "acp" }, }, ["codex"] = { command = "npx", args = { "@zed-industries/codex-acp" }, env = { NODE_NO_WARNINGS = "1", OPENAI_API_KEY = os.getenv("OPENAI_API_KEY"), }, }, }, -- other configuration options... } ``` ### Prerequisites Before using ACP agents, ensure you have the required tools installed: - **For Gemini CLI**: Install the `gemini` CLI tool and set your `GEMINI_API_KEY` - **For Claude Code**: Install the `acp-claude-code` package via npm and set your `ANTHROPIC_API_KEY` ### ACP vs Traditional Providers ACP providers offer several advantages over traditional API-based providers: - **Enhanced Tool Access**: Agents can directly interact with your file system, run commands, and access development tools - **Persistent Context**: Sessions maintain state across multiple interactions - **Fine-grained Permissions**: Control exactly what agents can access and modify - **Standardized Protocol**: Compatible with any ACP-compliant agent ## Custom providers Avante provides a set of default providers, but users can also create their own providers. For more information, see [Custom Providers](https://github.com/yetone/avante.nvim/wiki/Custom-providers) ## RAG Service Avante provides a RAG service, which is a tool for obtaining the required context for the AI to generate the codes. By default, it is not enabled. You can enable it this way: ```lua rag_service = { -- RAG Service configuration enabled = false, -- Enables the RAG service host_mount = os.getenv("HOME"), -- Host mount path for the rag service (Docker will mount this path) runner = "docker", -- Runner for the RAG service (can use docker or nix) llm = { -- Language Model (LLM) configuration for RAG service provider = "openai", -- LLM provider endpoint = "https://api.openai.com/v1", -- LLM API endpoint api_key = "OPENAI_API_KEY", -- Environment variable name for the LLM API key model = "gpt-4o-mini", -- LLM model name extra = nil, -- Additional configuration options for LLM }, embed = { -- Embedding model configuration for RAG service provider = "openai", -- Embedding provider endpoint = "https://api.openai.com/v1", -- Embedding API endpoint api_key = "OPENAI_API_KEY", -- Environment variable name for the embedding API key model = "text-embedding-3-large", -- Embedding model name extra = nil, -- Additional configuration options for the embedding model }, docker_extra_args = "", -- Extra arguments to pass to the docker command }, ``` The RAG Service can currently configure the LLM and embedding models separately. In the `llm` and `embed` configuration blocks, you can set the following fields: - `provider`: Model provider (e.g., "openai", "ollama", "dashscope", and "openrouter") - `endpoint`: API endpoint - `api_key`: Environment variable name for the API key - `model`: Model name - `extra`: Additional configuration options For detailed configuration of different model providers, you can check [here](./py/rag-service/README.md). Additionally, RAG Service also depends on Docker! (For macOS users, OrbStack is recommended as a Docker alternative). `host_mount` is the path that will be mounted to the container, and the default is the home directory. The mount is required for the RAG service to access the files in the host machine. It is up to the user to decide if you want to mount the whole `/` directory, just the project directory, or the home directory. If you plan using avante and RAG event for projects stored outside your home directory, you will need to set the `host_mount` to the root directory of your file system. The mount will be read only. After changing the rag_service configuration, you need to manually delete the rag_service container to ensure the new configuration is used: `docker rm -fv avante-rag-service` ## Web Search Engines Avante's tools include some web search engines, currently support: - [Tavily](https://tavily.com/) - [SerpApi - Search API](https://serpapi.com/) - Google's [Programmable Search Engine](https://developers.google.com/custom-search/v1/overview) - [Kagi](https://help.kagi.com/kagi/api/search.html) - [Brave Search](https://api-dashboard.search.brave.com/app/documentation/web-search/get-started) - [SearXNG](https://searxng.github.io/searxng/) The default is Tavily, and can be changed through configuring `Config.web_search_engine.provider`: ```lua web_search_engine = { provider = "tavily", -- tavily, serpapi, google, kagi, brave, or searxng proxy = nil, -- proxy support, e.g., http://127.0.0.1:7890 } ``` Environment variables required for providers: - Tavily: `TAVILY_API_KEY` - SerpApi: `SERPAPI_API_KEY` - Google: - `GOOGLE_SEARCH_API_KEY` as the [API key](https://developers.google.com/custom-search/v1/overview) - `GOOGLE_SEARCH_ENGINE_ID` as the [search engine](https://programmablesearchengine.google.com) ID - Kagi: `KAGI_API_KEY` as the [API Token](https://kagi.com/settings?p=api) - Brave Search: `BRAVE_API_KEY` as the [API key](https://api-dashboard.search.brave.com/app/keys) - SearXNG: `SEARXNG_API_URL` as the [API URL](https://docs.searxng.org/dev/search_api.html) ## Disable Tools Avante enables tools by default, but some LLM models do not support tools. You can disable tools by setting `disable_tools = true` for the provider. For example: ```lua providers = { claude = { endpoint = "https://api.anthropic.com", model = "claude-sonnet-4-20250514", timeout = 30000, -- Timeout in milliseconds disable_tools = true, -- disable tools! extra_request_body = { temperature = 0, max_tokens = 4096, } } } ``` In case you want to ban some tools to avoid its usage (like Claude 3.7 overusing the python tool) you can disable just specific tools ```lua { disabled_tools = { "python" }, } ``` Tool list > rag_search, python, git_diff, git_commit, glob, search_keyword, read_file_toplevel_symbols, > read_file, create_file, move_path, copy_path, delete_path, create_dir, bash, web_search, fetch ## Custom Tools Avante allows you to define custom tools that can be used by the AI during code generation and analysis. These tools can execute shell commands, run scripts, or perform any custom logic you need. ### Example: Go Test Runner
Here's an example of a custom tool that runs Go unit tests: ```lua { custom_tools = { { name = "run_go_tests", -- Unique name for the tool description = "Run Go unit tests and return results", -- Description shown to AI command = "go test -v ./...", -- Shell command to execute param = { -- Input parameters (optional) type = "table", fields = { { name = "target", description = "Package or directory to test (e.g. './pkg/...' or './internal/pkg')", type = "string", optional = true, }, }, }, returns = { -- Expected return values { name = "result", description = "Result of the fetch", type = "string", }, { name = "error", description = "Error message if the fetch was not successful", type = "string", optional = true, }, }, func = function(params, on_log, on_complete) -- Custom function to execute local target = params.target or "./..." return vim.system({ "go", "test", "-v", target }, { text = true }):wait().stdout end, }, }, } ```
## MCP Now you can integrate MCP functionality for Avante through `mcphub.nvim`. For detailed documentation, please refer to [mcphub.nvim](https://ravitemer.github.io/mcphub.nvim/extensions/avante.html) ## Custom prompts By default, `avante.nvim` provides three different modes to interact with: `planning`, `editing`, and `suggesting`, followed with three different prompts per mode. - `planning`: Used with `require("avante").toggle()` on sidebar - `editing`: Used with `require("avante").edit()` on selection codeblock - `suggesting`: Used with `require("avante").get_suggestion():suggest()` on Tab flow. - `cursor-planning`: Used with `require("avante").toggle()` on Tab flow, but only when cursor planning mode is enabled. Users can customize the system prompts via `Config.system_prompt` or `Config.override_prompt_dir`. `Config.system_prompt` allows you to set a global system prompt. We recommend calling this in a custom Autocmds depending on your need: ```lua vim.api.nvim_create_autocmd("User", { pattern = "ToggleMyPrompt", callback = function() require("avante.config").override({system_prompt = "MY CUSTOM SYSTEM PROMPT"}) end, }) vim.keymap.set("n", "am", function() vim.api.nvim_exec_autocmds("User", { pattern = "ToggleMyPrompt" }) end, { desc = "avante: toggle my prompt" }) ``` `Config.override_prompt_dir` allows you to specify a directory containing your own custom prompt templates, which will override the built-in templates. This is useful if you want to maintain a set of custom prompts outside of your Neovim configuration. It can be a string representing the directory path, or a function that returns a string representing the directory path. ```lua -- Example: Override with prompts from a specific directory require("avante").setup({ override_prompt_dir = vim.fn.expand("~/.config/nvim/avante_prompts"), }) -- Example: Override with prompts from a function (dynamic directory) require("avante").setup({ override_prompt_dir = function() -- Your logic to determine the prompt directory return vim.fn.expand("~/.config/nvim/my_dynamic_prompts") end, }) ``` > [!WARNING] > > If you customize `base.avanterules`, please ensure that `{% block custom_prompt %}{% endblock %}` and `{% block extra_prompt %}{% endblock %}` exist, otherwise the entire plugin may become unusable. > If you are unsure about the specific reasons or what you are doing, please do not override the built-in prompts. The built-in prompts work very well. If you wish to custom prompts for each mode, `avante.nvim` will check for project root based on the given buffer whether it contains the following patterns: `*.{mode}.avanterules`. The rules for root hierarchy: - lsp workspace folders - lsp root_dir - root pattern of filename of the current buffer - root pattern of cwd You can also configure custom directories for your `avanterules` files using the `rules` option: ```lua require('avante').setup({ rules = { project_dir = '.avante/rules', -- relative to project root, can also be an absolute path global_dir = '~/.config/avante/rules', -- absolute path }, }) ``` The loading priority is as follows: 1. `rules.project_dir` 2. `rules.global_dir` 3. Project root
Example folder structure for custom prompt If you have the following structure: ```bash . ├── .git/ ├── typescript.planning.avanterules ├── snippets.editing.avanterules ├── suggesting.avanterules └── src/ ``` - `typescript.planning.avanterules` will be used for `planning` mode - `snippets.editing.avanterules` will be used for `editing` mode - `suggesting.avanterules` will be used for `suggesting` mode.
> [!important] > > `*.avanterules` is a jinja template file, in which will be rendered using [minijinja](https://github.com/mitsuhiko/minijinja). See [templates](https://github.com/yetone/avante.nvim/blob/main/lua/avante/templates) for example on how to extend current templates. ## Integration Avante.nvim can be extended to work with other plugins by using its extension modules. Below is an example of integrating Avante with [`nvim-tree`](https://github.com/nvim-tree/nvim-tree.lua), allowing you to select or deselect files directly from the NvimTree UI: ```lua { "yetone/avante.nvim", event = "VeryLazy", keys = { { "a+", function() local tree_ext = require("avante.extensions.nvim_tree") tree_ext.add_file() end, desc = "Select file in NvimTree", ft = "NvimTree", }, { "a-", function() local tree_ext = require("avante.extensions.nvim_tree") tree_ext.remove_file() end, desc = "Deselect file in NvimTree", ft = "NvimTree", }, }, opts = { --- other configurations selector = { exclude_auto_select = { "NvimTree" }, }, }, } ``` ## TODOs - [x] Chat with current file - [x] Apply diff patch - [x] Chat with the selected block - [x] Slash commands - [x] Edit the selected block - [x] Smart Tab (Cursor Flow) - [x] Chat with project (You can use `@codebase` to chat with the whole project) - [x] Chat with selected files - [x] Tool use - [x] MCP - [x] ACP - [ ] Better codebase indexing ## Roadmap - **Enhanced AI Interactions**: Improve the depth of AI analysis and recommendations for more complex coding scenarios. - **LSP + Tree-sitter + LLM Integration**: Integrate with LSP and Tree-sitter and LLM to provide more accurate and powerful code suggestions and analysis. ## FAQ ### How to disable agentic mode? Avante.nvim provides two interaction modes: - **`agentic`** (default): Uses AI tools to automatically generate and apply code changes - **`legacy`**: Uses the traditional planning method without automatic tool execution To disable agentic mode and switch to legacy mode, update your configuration: ```lua { mode = "legacy", -- Switch from "agentic" to "legacy" -- ... your other configuration options } ``` **What's the difference?** - **Agentic mode**: AI can automatically execute tools like file operations, bash commands, web searches, etc. to complete complex tasks - **Legacy mode**: AI provides suggestions and plans but requires manual approval for all actions **When should you use legacy mode?** - If you prefer more control over what actions the AI takes - If you're concerned about security with automatic tool execution - If you want to manually review each step before applying changes - If you're working in a sensitive environment where automatic code changes aren't desired You can also disable specific tools while keeping agentic mode enabled by configuring `disabled_tools`: ```lua { mode = "agentic", disabled_tools = { "bash", "python" }, -- Disable specific tools -- ... your other configuration options } ``` ## Contributing Contributions to avante.nvim are welcome! If you're interested in helping out, please feel free to submit pull requests or open issues. Before contributing, ensure that your code has been thoroughly tested. See [wiki](https://github.com/yetone/avante.nvim/wiki) for more recipes and tricks. ## Acknowledgments We would like to express our heartfelt gratitude to the contributors of the following open-source projects, whose code has provided invaluable inspiration and reference for the development of avante.nvim: | Nvim Plugin | License | Functionality | Location | | --------------------------------------------------------------------- | ------------------ | ----------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | [git-conflict.nvim](https://github.com/akinsho/git-conflict.nvim) | No License | Diff comparison functionality | [lua/avante/diff.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/diff.lua) | | [ChatGPT.nvim](https://github.com/jackMort/ChatGPT.nvim) | Apache 2.0 License | Calculation of tokens count | [lua/avante/utils/tokens.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/utils/tokens.lua) | | [img-clip.nvim](https://github.com/HakonHarnes/img-clip.nvim) | MIT License | Clipboard image support | [lua/avante/clipboard.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/clipboard.lua) | | [copilot.lua](https://github.com/zbirenbaum/copilot.lua) | MIT License | Copilot support | [lua/avante/providers/copilot.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/providers/copilot.lua) | | [jinja.vim](https://github.com/HiPhish/jinja.vim) | MIT License | Template filetype support | [syntax/jinja.vim](https://github.com/yetone/avante.nvim/blob/main/syntax/jinja.vim) | | [codecompanion.nvim](https://github.com/olimorris/codecompanion.nvim) | MIT License | Secrets logic support | [lua/avante/providers/init.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/providers/init.lua) | | [aider](https://github.com/paul-gauthier/aider) | Apache 2.0 License | Planning mode user prompt | [lua/avante/templates/planning.avanterules](https://github.com/yetone/avante.nvim/blob/main/lua/avante/templates/planning.avanterules) | The high quality and ingenuity of these projects' source code have been immensely beneficial throughout our development process. We extend our sincere thanks and respect to the authors and contributors of these projects. It is the selfless dedication of the open-source community that drives projects like avante.nvim forward. ## Business Sponsors
Meshy AI
Meshy AI
 
The #1 AI 3D Model Generator for Creators
BabelTower API
BabelTower API
 
No account needed, use any model instantly
## License avante.nvim is licensed under the Apache 2.0 License. For more details, please refer to the [LICENSE](./LICENSE) file. # Star History

NebulaGraph Data Intelligence Suite(ngdi)

================================================ FILE: README_zh.md ================================================
logo

avante.nvim

Neovim: v0.10+ Lua CI status Rust CI status pre-commit status Discord

**avante.nvim** 是一个 Neovim 插件,旨在模拟 [Cursor](https://www.cursor.com) AI IDE 的行为。它为用户提供 AI 驱动的代码建议,并能够轻松地将这些建议直接应用到源文件中。 [View in English](README.md) > [!NOTE] > > 🥰 该项目正在快速迭代中,许多令人兴奋的功能将陆续添加。敬请期待! ## 赞助 ❤️ 如果您喜欢这个项目,请考虑在 Patreon 上支持我,因为这有助于我继续维护和改进它: [赞助我](https://patreon.com/yetone) ## 功能 - **AI 驱动的代码辅助**:与 AI 互动,询问有关当前代码文件的问题,并接收智能建议以进行改进或修改。 - **一键应用**:通过单个命令快速将 AI 的建议更改应用到源代码中,简化编辑过程并节省时间。 ## 安装 如果您希望从源代码构建二进制文件,则需要 `cargo`。否则,将使用 `curl` 和 `tar` 从 GitHub 获取预构建的二进制文件。
lazy.nvim (推荐) ```lua { "yetone/avante.nvim", -- 如果您想从源代码构建,请执行 `make BUILD_FROM_SOURCE=true` -- ⚠️ 一定要加上这一行配置!!!!! build = vim.fn.has("win32") ~= 0 and "powershell -ExecutionPolicy Bypass -File Build.ps1 -BuildFromSource false" or "make", event = "VeryLazy", version = false, -- 永远不要将此值设置为 "*"!永远不要! ---@module 'avante' ---@type avante.Config opts = { -- 在此处添加任何选项 -- 例如 provider = "claude", providers = { claude = { endpoint = "https://api.anthropic.com", model = "claude-sonnet-4-20250514", timeout = 30000, -- Timeout in milliseconds extra_request_body = { temperature = 0.75, max_tokens = 20480, }, }, moonshot = { endpoint = "https://api.moonshot.ai/v1", model = "kimi-k2-0711-preview", timeout = 30000, -- 超时时间(毫秒) extra_request_body = { temperature = 0.75, max_tokens = 32768, }, }, }, }, dependencies = { "nvim-lua/plenary.nvim", "MunifTanjim/nui.nvim", --- 以下依赖项是可选的, "echasnovski/mini.pick", -- 用于文件选择器提供者 mini.pick "nvim-telescope/telescope.nvim", -- 用于文件选择器提供者 telescope "hrsh7th/nvim-cmp", -- avante 命令和提及的自动完成 "ibhagwan/fzf-lua", -- 用于文件选择器提供者 fzf "nvim-tree/nvim-web-devicons", -- 或 echasnovski/mini.icons "zbirenbaum/copilot.lua", -- 用于 providers='copilot' { -- 支持图像粘贴 "HakonHarnes/img-clip.nvim", event = "VeryLazy", opts = { -- 推荐设置 default = { embed_image_as_base64 = false, prompt_for_file_name = false, drag_and_drop = { insert_mode = true, }, -- Windows 用户必需 use_absolute_path = true, }, }, }, { -- 如果您有 lazy=true,请确保正确设置 'MeanderingProgrammer/render-markdown.nvim', opts = { file_types = { "markdown", "Avante" }, }, ft = { "markdown", "Avante" }, }, }, } ```
vim-plug ```vim " 依赖项 Plug 'nvim-lua/plenary.nvim' Plug 'MunifTanjim/nui.nvim' Plug 'MeanderingProgrammer/render-markdown.nvim' " 可选依赖项 Plug 'hrsh7th/nvim-cmp' Plug 'nvim-tree/nvim-web-devicons' "或 Plug 'echasnovski/mini.icons' Plug 'HakonHarnes/img-clip.nvim' Plug 'zbirenbaum/copilot.lua' " Yay,如果您想从源代码构建,请传递 source=true Plug 'yetone/avante.nvim', { 'branch': 'main', 'do': 'make' } autocmd! User avante.nvim lua << EOF require('avante').setup() EOF ```
mini.deps ```lua local add, later, now = MiniDeps.add, MiniDeps.later, MiniDeps.now add({ source = 'yetone/avante.nvim', monitor = 'main', depends = { 'nvim-lua/plenary.nvim', 'MunifTanjim/nui.nvim', 'echasnovski/mini.icons' }, hooks = { post_checkout = function() vim.cmd('make') end } }) --- 可选 add({ source = 'hrsh7th/nvim-cmp' }) add({ source = 'zbirenbaum/copilot.lua' }) add({ source = 'HakonHarnes/img-clip.nvim' }) add({ source = 'MeanderingProgrammer/render-markdown.nvim' }) later(function() require('render-markdown').setup({...}) end) later(function() require('img-clip').setup({...}) -- 配置 img-clip require("copilot").setup({...}) -- 根据您的喜好设置 copilot require("avante").setup({...}) -- 配置 avante.nvim end) ```
Packer ```vim -- 必需插件 use 'nvim-lua/plenary.nvim' use 'MunifTanjim/nui.nvim' use 'MeanderingProgrammer/render-markdown.nvim' -- 可选依赖项 use 'hrsh7th/nvim-cmp' use 'nvim-tree/nvim-web-devicons' -- 或使用 'echasnovski/mini.icons' use 'HakonHarnes/img-clip.nvim' use 'zbirenbaum/copilot.lua' -- Avante.nvim 带有构建过程 use { 'yetone/avante.nvim', branch = 'main', run = 'make', config = function() require('avante').setup() end } ```
Home Manager ```nix programs.neovim = { plugins = [ { plugin = pkgs.vimPlugins.avante-nvim; type = "lua"; config = '' require("avante_lib").load() require("avante").setup() '' # 或 builtins.readFile ./plugins/avante.lua; } ]; }; ```
Nixvim ```nix plugins.avante.enable = true; plugins.avante.settings = { # 在此处设置选项 }; ```
Lua ```lua -- 依赖项: require('cmp').setup ({ -- 使用上面的推荐设置 }) require('img-clip').setup ({ -- 使用上面的推荐设置 }) require('copilot').setup ({ -- 使用上面的推荐设置 }) require('render-markdown').setup ({ -- 使用上面的推荐设置 }) require('avante').setup ({ -- 在此处配置! }) ```
> [!IMPORTANT] > > `avante.nvim` 目前仅兼容 Neovim 0.10.1 或更高版本。请确保您的 Neovim 版本符合这些要求后再继续。 > [!NOTE] > > 在同步加载插件时,我们建议在您的配色方案之后的某个时间 `require` 它。 > [!NOTE] > > 推荐的 **Neovim** 选项: > > ```lua > -- 视图只能通过全局状态栏完全折叠 > vim.opt.laststatus = 3 > ``` > [!TIP] > > 任何支持 markdown 的渲染插件都可以与 Avante 一起使用,只要您添加支持的文件类型 `Avante`。有关更多信息,请参见 和 [此评论](https://github.com/yetone/avante.nvim/issues/175#issuecomment-2313749363)。 ### 默认设置配置 _请参见 [config.lua#L9](./lua/avante/config.lua) 以获取完整配置_
默认配置 ```lua { ---@alias Provider "claude" | "openai" | "azure" | "gemini" | "cohere" | "copilot" | string provider = "claude", -- 在 Aider 模式或 Cursor 规划模式的规划阶段使用的提供者 -- 警告:由于自动建议是高频操作,因此成本较高, -- 目前将其指定为 `copilot` 提供者是危险的,因为:https://github.com/yetone/avante.nvim/issues/1048 -- 当然,您可以通过增加 `suggestion.debounce` 来减少请求频率。 auto_suggestions_provider = "claude", providers = { claude = { endpoint = "https://api.anthropic.com", model = "claude-3-5-sonnet-20241022", extra_request_body = { temperature = 0.75, max_tokens = 4096, }, }, moonshot = { endpoint = "https://api.moonshot.ai/v1", model = "kimi-k2-0711-preview", timeout = 30000, -- 超时时间(毫秒) extra_request_body = { temperature = 0.75, max_tokens = 32768, }, }, }, ---指定特殊的 dual_boost 模式 ---1. enabled: 是否启用 dual_boost 模式。默认为 false。 ---2. first_provider: 第一个提供者用于生成响应。默认为 "openai"。 ---3. second_provider: 第二个提供者用于生成响应。默认为 "claude"。 ---4. prompt: 用于根据两个参考输出生成响应的提示。 ---5. timeout: 超时时间(毫秒)。默认为 60000。 ---工作原理: --- 启用 dual_boost 后,avante 将分别从 first_provider 和 second_provider 生成两个响应。然后使用 first_provider 的响应作为 provider1_output,second_provider 的响应作为 provider2_output。最后,avante 将根据提示和两个参考输出生成响应,默认提供者与正常情况相同。 ---注意:这是一个实验性功能,可能无法按预期工作。 dual_boost = { enabled = false, first_provider = "openai", second_provider = "claude", prompt = "根据以下两个参考输出,生成一个结合两者元素但反映您自己判断和独特视角的响应。不要提供任何解释,只需直接给出响应。参考输出 1: [{{provider1_output}}], 参考输出 2: [{{provider2_output}}]", timeout = 60000, -- 超时时间(毫秒) }, behaviour = { auto_suggestions = false, -- 实验阶段 auto_set_highlight_group = true, auto_set_keymaps = true, auto_apply_diff_after_generation = false, support_paste_from_clipboard = false, minimize_diff = true, -- 是否在应用代码块时删除未更改的行 enable_token_counting = true, -- 是否启用令牌计数。默认为 true。 auto_add_current_file = true, -- 打开新聊天时是否自动添加当前文件。默认为 true。 enable_cursor_planning_mode = false, -- 是否启用 Cursor 规划模式。默认为 false。 enable_claude_text_editor_tool_mode = false, -- 是否启用 Claude 文本编辑器工具模式。 ---@type "popup" | "inline_buttons" confirmation_ui_style = "inline_buttons", }, mappings = { --- @class AvanteConflictMappings diff = { ours = "co", theirs = "ct", all_theirs = "ca", both = "cb", cursor = "cc", next = "]x", prev = "[x", }, suggestion = { accept = "", next = "", prev = "", dismiss = "", }, jump = { next = "]]", prev = "[[", }, submit = { normal = "", insert = "", }, cancel = { normal = { "", "", "q" }, insert = { "" }, }, sidebar = { apply_all = "A", apply_cursor = "a", retry_user_request = "r", edit_user_request = "e", switch_windows = "", reverse_switch_windows = "", remove_file = "d", add_file = "@", close = { "", "q" }, close_from_input = nil, -- 例如,{ normal = "", insert = "" } }, }, selection = { enabled = true, hint_display = "delayed", }, windows = { ---@type "right" | "left" | "top" | "bottom" position = "right", -- 侧边栏的位置 wrap = true, -- 类似于 vim.o.wrap width = 30, -- 默认基于可用宽度的百分比 sidebar_header = { enabled = true, -- true, false 启用/禁用标题 align = "center", -- left, center, right 用于标题 rounded = true, }, spinner = { editing = { "⡀", "⠄", "⠂", "⠁", "⠈", "⠐", "⠠", "⢀", "⣀", "⢄", "⢂", "⢁", "⢈", "⢐", "⢠", "⣠", "⢤", "⢢", "⢡", "⢨", "⢰", "⣰", "⢴", "⢲", "⢱", "⢸", "⣸", "⢼", "⢺", "⢹", "⣹", "⢽", "⢻", "⣻", "⢿", "⣿" }, generating = { "·", "✢", "✳", "∗", "✻", "✽" }, -- '生成中' 状态的旋转字符 thinking = { "🤯", "🙄" }, -- '思考中' 状态的旋转字符 }, input = { prefix = "> ", height = 8, -- 垂直布局中输入窗口的高度 }, edit = { border = "rounded", start_insert = true, -- 打开编辑窗口时开始插入模式 }, ask = { floating = false, -- 在浮动窗口中打开 'AvanteAsk' 提示 start_insert = true, -- 打开询问窗口时开始插入模式 border = "rounded", ---@type "ours" | "theirs" focus_on_apply = "ours", -- 应用后聚焦的差异 }, }, highlights = { ---@type AvanteConflictHighlights diff = { current = "DiffText", incoming = "DiffAdd", }, }, --- @class AvanteConflictUserConfig diff = { autojump = true, ---@type string | fun(): any list_opener = "copen", --- 覆盖悬停在差异上时的 'timeoutlen' 设置(请参阅 :help timeoutlen)。 --- 有助于避免进入以 `c` 开头的差异映射的操作员挂起模式。 --- 通过设置为 -1 禁用。 override_timeoutlen = 500, }, suggestion = { debounce = 600, throttle = 600, }, } ```
## Blink.cmp 用户 对于 blink cmp 用户(nvim-cmp 替代品),请查看以下配置说明 这是通过使用 blink.compat 模拟 nvim-cmp 实现的 或者您可以使用 [Kaiser-Yang/blink-cmp-avante](https://github.com/Kaiser-Yang/blink-cmp-avante)。
Lua ```lua selector = { --- @alias avante.SelectorProvider "native" | "fzf_lua" | "mini_pick" | "snacks" | "telescope" | fun(selector: avante.ui.Selector): nil provider = "fzf", -- 自定义提供者的选项覆盖 provider_opts = {}, } ``` 要创建自定义选择器,您可以指定一个自定义函数来启动选择器以选择项目,并将选定的项目传递给 `on_select` 回调。 ```lua selector = { ---@param selector avante.ui.Selector provider = function(selector) local items = selector.items ---@type avante.ui.SelectorItem[] local title = selector.title ---@type string local on_select = selector.on_select ---@type fun(selected_item_ids: string[]|nil): nil --- 在这里添加您的自定义选择器逻辑 end, } ``` 选择 native 以外的选择器,默认情况下目前存在问题 对于 lazyvim 用户,请从网站复制 blink.cmp 的完整配置或扩展选项 ```lua compat = { "avante_commands", "avante_mentions", "avante_files", } ``` 对于其他用户,只需添加自定义提供者 ### 可用的补全项 Avante.nvim 提供了多个可以与 blink.cmp 集成的补全项: #### 提及功能 (`@` 触发器) 提及功能允许您快速引用特定功能或将文件添加到聊天上下文: - `@codebase` - 启用项目上下文和仓库映射 - `@diagnostics` - 启用诊断信息 - `@file` - 打开文件选择器以将文件添加到聊天上下文 - `@quickfix` - 将快速修复列表中的文件添加到聊天上下文 - `@buffers` - 将打开的缓冲区添加到聊天上下文 #### 斜杠命令 (`/` 触发器) 内置斜杠命令用于常见操作: - `/help` - 显示可用命令的帮助信息 - `/init` - 基于当前项目初始化 AGENTS.md - `/clear` - 清除聊天历史 - `/new` - 开始新聊天 - `/compact` - 压缩历史消息以节省令牌 - `/lines - ` - 询问特定行的问题 - `/commit` - 为更改生成提交消息 #### 快捷方式 (`#` 触发器) 快捷方式提供对预定义提示模板的快速访问。您可以在配置中自定义这些: ```lua { shortcuts = { { name = "refactor", description = "使用最佳实践重构代码", details = "自动重构代码以提高可读性、可维护性,并遵循最佳实践,同时保持功能不变", prompt = "请按照最佳实践重构此代码,提高可读性和可维护性,同时保持功能不变。" }, { name = "test", description = "生成单元测试", details = "创建全面的单元测试,涵盖边界情况、错误场景和各种输入条件", prompt = "请为此代码生成全面的单元测试,涵盖边界情况和错误场景。" }, -- 添加更多自定义快捷方式... } } ``` 当您在输入中键入 `#refactor` 时,它将自动替换为相应的提示文本。 ### 配置示例 以下是包含所有 Avante 源的完整 blink.cmp 配置示例: ```lua default = { ... "avante_commands", "avante_mentions", "avante_shortcuts", "avante_files", } ``` ```lua providers = { avante_commands = { name = "avante_commands", module = "blink.compat.source", score_offset = 90, -- 显示优先级高于 lsp opts = {}, }, avante_files = { name = "avante_files", module = "blink.compat.source", score_offset = 100, -- 显示优先级高于 lsp opts = {}, }, avante_mentions = { name = "avante_mentions", module = "blink.compat.source", score_offset = 1000, -- 显示优先级高于 lsp opts = {}, }, avante_shortcuts = { name = "avante_shortcuts", module = "blink.compat.source", score_offset = 1000, -- 显示优先级高于 lsp opts = {}, } ... } ```
## 用法 鉴于其早期阶段,`avante.nvim` 目前支持以下基本功能: > [!IMPORTANT] > > 为了在 neovim 会话之间保持一致性,建议在 shell 文件中设置环境变量。 > 默认情况下,`Avante` 会在启动时提示您输入所选提供者的 API 密钥。 > > **作用域 API 密钥(推荐用于隔离)** > > Avante 现在支持作用域 API 密钥,允许您专门为 Avante 隔离 API 密钥,而不影响其他应用程序。只需在任何 API 密钥前加上 `AVANTE_` 前缀: > > ```sh > # 作用域密钥(推荐) > export AVANTE_ANTHROPIC_API_KEY=your-claude-api-key > export AVANTE_OPENAI_API_KEY=your-openai-api-key > export AVANTE_AZURE_OPENAI_API_KEY=your-azure-api-key > export AVANTE_GEMINI_API_KEY=your-gemini-api-key > export AVANTE_CO_API_KEY=your-cohere-api-key > export AVANTE_AIHUBMIX_API_KEY=your-aihubmix-api-key > export AVANTE_MOONSHOT_API_KEY=your-moonshot-api-key > ``` > > **全局 API 密钥(传统方式)** > > 如果您愿意,仍然可以使用传统的全局 API 密钥: > > 对于 Claude: > > ```sh > export ANTHROPIC_API_KEY=your-api-key > ``` > > 对于 OpenAI: > > ```sh > export OPENAI_API_KEY=your-api-key > ``` > > 对于 Azure OpenAI: > > ```sh > export AZURE_OPENAI_API_KEY=your-api-key > ``` > > 对于 Amazon Bedrock: > > ```sh > export BEDROCK_KEYS=aws_access_key_id,aws_secret_access_key,aws_region[,aws_session_token] > > ``` > > 注意:aws_session_token 是可选的,仅在使用临时 AWS 凭证时需要 1. 在 Neovim 中打开代码文件。 2. 使用 `:AvanteAsk` 命令查询 AI 关于代码的问题。 3. 查看 AI 的建议。 4. 通过简单的命令或按键绑定将推荐的更改直接应用到代码中。 **注意**:该插件仍在积极开发中,其功能和界面可能会发生重大变化。随着项目的发展,预计会有一些粗糙的边缘和不稳定性。 ## 键绑定 以下键绑定可用于 `avante.nvim`: | 键绑定 | 描述 | | ----------------------------------------- | ----------------------------- | | Leaderaa | 显示侧边栏 | | Leaderat | 切换侧边栏可见性 | | Leaderar | 刷新侧边栏 | | Leaderaf | 切换侧边栏焦点 | | Leadera? | 选择模型 | | Leaderae | 编辑选定的块 | | LeaderaS | 停止当前 AI 请求 | | co | 选择我们的 | | ct | 选择他们的 | | ca | 选择所有他们的 | | c0 | 选择无 | | cb | 选择两者 | | cc | 选择光标 | | ]x | 移动到上一个冲突 | | [x | 移动到下一个冲突 | | [[ | 跳转到上一个代码块 (结果窗口) | | ]] | 跳转到下一个代码块 (结果窗口) | > [!NOTE] > > 如果您使用 `lazy.nvim`,那么此处的所有键映射都将安全设置,这意味着如果 `aa` 已经绑定,则 avante.nvim 不会绑定此映射。 > 在这种情况下,用户将负责设置自己的。有关更多详细信息,请参见 [关于键映射的说明](https://github.com/yetone/avante.nvim/wiki#keymaps-and-api-i-guess)。 ### Neotree 快捷方式 在 neotree 侧边栏中,您还可以添加新的键盘快捷方式,以快速将 `file/folder` 添加到 `Avante Selected Files`。
Neotree 配置 ```lua return { { 'nvim-neo-tree/neo-tree.nvim', config = function() require('neo-tree').setup({ filesystem = { commands = { avante_add_files = function(state) local node = state.tree:get_node() local filepath = node:get_id() local relative_path = require('avante.utils').relative_path(filepath) local sidebar = require('avante').get() local open = sidebar:is_open() -- 确保 avante 侧边栏已打开 if not open then require('avante.api').ask() sidebar = require('avante').get() end sidebar.file_selector:add_selected_file(relative_path) -- 删除 neo tree 缓冲区 if not open then sidebar.file_selector:remove_selected_file('neo-tree filesystem [1]') end end, }, window = { mappings = { ['oa'] = 'avante_add_files', }, }, }, }) end, }, } ```
## 命令 | 命令 | 描述 | 示例 | | ---------------------------------- | ---------------------------------------------------------------------------------------- | --------------------------------------------------- | | `:AvanteAsk [question] [position]` | 询问 AI 关于您的代码的问题。可选的 `position` 设置窗口位置和 `ask` 启用/禁用直接询问模式 | `:AvanteAsk position=right Refactor this code here` | | `:AvanteBuild` | 构建项目的依赖项 | | | `:AvanteChat` | 启动与 AI 的聊天会话,讨论您的代码库。默认情况下 `ask`=false | | | `:AvanteClear` | 清除聊天记录 | | | `:AvanteEdit` | 编辑选定的代码块 | | | `:AvanteFocus` | 切换焦点到/从侧边栏 | | | `:AvanteRefresh` | 刷新所有 Avante 窗口 | | | `:AvanteStop` | 停止当前 AI 请求 | | | `:AvanteSwitchProvider` | 切换 AI 提供者(例如 openai) | | | `:AvanteShowRepoMap` | 显示项目结构的 repo map | | | `:AvanteToggle` | 切换 Avante 侧边栏 | | | `:AvanteModels` | 显示模型列表 | | ## 高亮组 | 高亮组 | 描述 | 备注 | | --------------------------- | -------------------------- | ------------------------------------------ | | AvanteTitle | 标题 | | | AvanteReversedTitle | 用于圆角边框 | | | AvanteSubtitle | 选定代码标题 | | | AvanteReversedSubtitle | 用于圆角边框 | | | AvanteThirdTitle | 提示标题 | | | AvanteReversedThirdTitle | 用于圆角边框 | | | AvanteConflictCurrent | 当前冲突高亮 | 默认值为 `Config.highlights.diff.current` | | AvanteConflictIncoming | 即将到来的冲突高亮 | 默认值为 `Config.highlights.diff.incoming` | | AvanteConflictCurrentLabel | 当前冲突标签高亮 | 默认值为 `AvanteConflictCurrent` 的阴影 | | AvanteConflictIncomingLabel | 即将到来的冲突标签高亮 | 默认值为 `AvanteConflictIncoming` 的阴影 | | AvantePopupHint | 弹出菜单中的使用提示 | | | AvanteInlineHint | 在可视模式下显示的行尾提示 | | 有关更多信息,请参见 [highlights.lua](./lua/avante/highlights.lua) ## Ollama ollama 是 avante.nvim 的一流提供者。要开始使用它,您需要在配置中设置 `provider = "ollama"`,并将 `ollama` 中的 `model` 字段设置为您想要使用的模型。Ollama 默认是禁用的,您需要为其 `is_env_set` 方法提供一个实现来正确地启用它。例如: ```lua provider = "ollama", providers = { ollama = { model = "qwq:32b", is_env_set = require("avante.providers.ollama").check_endpoint_alive, }, } ``` ## 自定义提供者 Avante 提供了一组默认提供者,但用户也可以创建自己的提供者。 有关更多信息,请参见 [自定义提供者](https://github.com/yetone/avante.nvim/wiki/Custom-providers) ## Cursor 规划模式 因为 avante.nvim 一直使用 Aider 的方法进行规划应用,但其提示对模型要求很高,需要像 claude-3.5-sonnet 或 gpt-4o 这样的模型才能正常工作。 因此,我采用了 Cursor 的方法来实现规划应用。有关实现的详细信息,请参阅 [cursor-planning-mode.md](./cursor-planning-mode.md) ## RAG 服务 Avante 提供了一个 RAG 服务,这是一个用于获取 AI 生成代码所需上下文的工具。默认情况下,它未启用。您可以通过以下方式启用它: ```lua rag_service = { -- RAG 服务配置 enabled = false, -- 启用 RAG 服务 host_mount = os.getenv("HOME"), -- RAG 服务的主机挂载路径 (Docker 将挂载此路径) runner = "docker", -- RAG 服务的运行器 (可以使用 docker 或 nix) llm = { -- RAG 服务使用的语言模型 (LLM) 配置 provider = "openai", -- LLM 提供者 endpoint = "https://api.openai.com/v1", -- LLM API 端点 api_key = "OPENAI_API_KEY", -- LLM API 密钥的环境变量名称 model = "gpt-4o-mini", -- LLM 模型名称 extra = nil, -- LLM 的额外配置选项 }, embed = { -- RAG 服务使用的嵌入模型配置 provider = "openai", -- 嵌入提供者 endpoint = "https://api.openai.com/v1", -- 嵌入 API 端点 api_key = "OPENAI_API_KEY", -- 嵌入 API 密钥的环境变量名称 model = "text-embedding-3-large", -- 嵌入模型名称 extra = nil, -- 嵌入模型的额外配置选项 }, docker_extra_args = "", -- 传递给 docker 命令的额外参数 }, ``` RAG 服务可以单独设置llm模型和嵌入模型。在 `llm` 和 `embed` 配置块中,您可以设置以下字段: - `provider`: 模型提供者(例如 "openai", "ollama", "dashscope"以及"openrouter") - `endpoint`: API 端点 - `api_key`: API 密钥的环境变量名称 - `model`: 模型名称 - `extra`: 额外的配置选项 有关不同模型提供商的详细配置,你可以在[这里](./py/rag-service/README.md)查看。 此外,RAG 服务还依赖于 Docker!(对于 macOS 用户,推荐使用 OrbStack 作为 Docker 的替代品)。 `host_mount` 是将挂载到容器的路径,默认是主目录。挂载是 RAG 服务访问主机机器中文件所必需的。用户可以决定是否要挂载整个 `/` 目录、仅项目目录或主目录。如果您计划使用 avante 和 RAG 事件处理存储在主目录之外的项目,您需要将 `host_mount` 设置为文件系统的根目录。 挂载将是只读的。 更改 rag_service 配置后,您需要手动删除 rag_service 容器以确保使用新配置:`docker rm -fv avante-rag-service` ## Web 搜索引擎 Avante 的工具包括一些 Web 搜索引擎,目前支持: - [Tavily](https://tavily.com/) - [SerpApi - Search API](https://serpapi.com/) - Google's [Programmable Search Engine](https://developers.google.com/custom-search/v1/overview) - [Kagi](https://help.kagi.com/kagi/api/search.html) - [Brave Search](https://api-dashboard.search.brave.com/app/documentation/web-search/get-started) - [SearXNG](https://searxng.github.io/searxng/) 默认是 Tavily,可以通过配置 `Config.web_search_engine.provider` 进行更改: ```lua web_search_engine = { provider = "tavily", -- tavily, serpapi, google, kagi, brave 或 searxng proxy = nil, -- proxy support, e.g., http://127.0.0.1:7890 } ``` 提供者所需的环境变量: - Tavily: `TAVILY_API_KEY` - SerpApi: `SERPAPI_API_KEY` - Google: - `GOOGLE_SEARCH_API_KEY` 作为 [API 密钥](https://developers.google.com/custom-search/v1/overview) - `GOOGLE_SEARCH_ENGINE_ID` 作为 [搜索引擎](https://programmablesearchengine.google.com) ID - Kagi: `KAGI_API_KEY` 作为 [API 令牌](https://kagi.com/settings?p=api) - Brave Search: `BRAVE_API_KEY` 作为 [API 密钥](https://api-dashboard.search.brave.com/app/keys) - SearXNG: `SEARXNG_API_URL` 作为 [API URL](https://docs.searxng.org/dev/search_api.html) ## 禁用工具 Avante 默认启用工具,但某些 LLM 模型不支持工具。您可以通过为提供者设置 `disable_tools = true` 来禁用工具。例如: ```lua { claude = { endpoint = "https://api.anthropic.com", model = "claude-3-5-sonnet-20241022", timeout = 30000, -- 超时时间(毫秒) temperature = 0, max_tokens = 4096, disable_tools = true, -- 禁用工具! }, } ``` 如果您想禁止某些工具以避免其使用(例如 Claude 3.7 过度使用 python 工具),您可以仅禁用特定工具 ```lua { disabled_tools = { "python" }, } ``` 工具列表 > rag_search, python, git_diff, git_commit, glob, search_keyword, read_file_toplevel_symbols, > read_file, create_file, move_path, copy_path, delete_path, create_dir, bash, web_search, fetch ## 自定义工具 Avante 允许您定义自定义工具,AI 可以在代码生成和分析期间使用这些工具。这些工具可以执行 shell 命令、运行脚本或执行您需要的任何自定义逻辑。 ### 示例:Go 测试运行器
以下是一个运行 Go 单元测试的自定义工具示例: ```lua { custom_tools = { { name = "run_go_tests", -- 工具的唯一名称 description = "运行 Go 单元测试并返回结果", -- 显示给 AI 的描述 command = "go test -v ./...", -- 要执行的 shell 命令 param = { -- 输入参数(可选) type = "table", fields = { { name = "target", description = "要测试的包或目录(例如 './pkg/...' 或 './internal/pkg')", type = "string", optional = true, }, }, }, returns = { -- 预期返回值 { name = "result", description = "获取的结果", type = "string", }, { name = "error", description = "如果获取不成功的错误消息", type = "string", optional = true, }, }, func = function(params, on_log, on_complete) -- 要执行的自定义函数 local target = params.target or "./..." return vim.system({ "go", "test", "-v", target }, { text = true }):wait().stdout end, }, }, } ```
## MCP 现在您可以通过 `mcphub.nvim` 为 Avante 集成 MCP 功能。有关详细文档,请参阅 [mcphub.nvim](https://ravitemer.github.io/mcphub.nvim/extensions/avante.html) ## Claude 文本编辑器工具模式 Avante 利用 [Claude 文本编辑器工具](https://docs.anthropic.com/en/docs/build-with-claude/tool-use/text-editor-tool) 提供更优雅的代码编辑体验。您现在可以通过在 `behaviour` 配置中将 `enable_claude_text_editor_tool_mode` 设置为 `true` 来启用此功能: ```lua { behaviour = { enable_claude_text_editor_tool_mode = true, }, } ``` > [!NOTE] > 要启用 **Claude 文本编辑器工具模式**,您必须使用 `claude-3-5-sonnet-*` 或 `claude-3-7-sonnet-*` 模型与 `claude` 提供者!此功能不支持任何其他模型! ## 自定义提示 默认情况下,`avante.nvim` 提供三种不同的模式进行交互:`planning`、`editing` 和 `suggesting`,每种模式都有三种不同的提示。 - `planning`:与侧边栏上的 `require("avante").toggle()` 一起使用 - `editing`:与选定代码块上的 `require("avante").edit()` 一起使用 - `suggesting`:与 Tab 流上的 `require("avante").get_suggestion():suggest()` 一起使用。 - `cursor-planning`:与 Tab 流上的 `require("avante").toggle()` 一起使用,但仅在启用 cursor 规划模式时。 用户可以通过 `Config.system_prompt` 或 `Config.override_prompt_dir` 自定义系统提示。 `Config.system_prompt` 允许您设置全局系统提示。我们建议根据您的需要在自定义 Autocmds 中调用此方法: ```lua vim.api.nvim_create_autocmd("User", { pattern = "ToggleMyPrompt", callback = function() require("avante.config").override({system_prompt = "MY CUSTOM SYSTEM PROMPT"}) end, }) vim.keymap.set("n", "am", function() vim.api.nvim_exec_autocmds("User", { pattern = "ToggleMyPrompt" }) end, { desc = "avante: toggle my prompt" }) ``` `Config.override_prompt_dir` 允许您指定一个目录,其中包含您自己的自定义提示模板,这将覆盖内置模板。如果您想在 Neovim 配置之外维护一组自定义提示,这将非常有用。它可以是一个表示目录路径的字符串,也可以是一个返回表示目录路径的字符串的函数。 ```lua -- 示例:使用特定目录中的提示进行覆盖 require("avante").setup({ override_prompt_dir = vim.fn.expand("~/.config/nvim/avante_prompts"), }) -- 示例:使用函数(动态目录)中的提示进行覆盖 require("avante").setup({ override_prompt_dir = function() -- 确定提示目录的逻辑 return vim.fn.expand("~/.config/nvim/my_dynamic_prompts") end, }) ``` > [!WARNING] > > 如果您自定 `base.avanterules`,请一定要确保 `{% block custom_prompt %}{% endblock %}` 和 `{% block extra_prompt %}{% endblock %}` 存在,否则可能会导致整个插件无法使用。 > 如果您不清楚具体原因或者您不知道自己在干什么,请不要覆盖内置 prompt。内置 prompt 工作得非常好。 如果希望为每种模式自定义提示,`avante.nvim` 将根据给定缓冲区的项目根目录检查是否包含以下模式:`*.{mode}.avanterules`。 根目录层次结构的规则: - lsp 工作区文件夹 - lsp root_dir - 当前缓冲区的文件名的根模式 - cwd 的根模式 您还可以使用 `rules` 选项为您的 `avanterules` 文件配置自定义目录: ```lua require('avante').setup({ rules = { project_dir = '.avante/rules', -- 相对于项目根目录,也可以是绝对路径 global_dir = '~/.config/avante/rules', -- 绝对路径 }, }) ``` 加载优先级如下: 1. `rules.project_dir` 2. `rules.global_dir` 3. 项目根目录
自定义提示的示例文件夹结构 如果您有以下结构: ```bash . ├── .git/ ├── typescript.planning.avanterules ├── snippets.editing.avanterules ├── suggesting.avanterules └── src/ ``` - `typescript.planning.avanterules` 将用于 `planning` 模式 - `snippets.editing.avanterules` 将用于 `editing` 模式 - `suggesting.avanterules` 将用于 `suggesting` 模式。
> [!important] > > `*.avanterules` 是一个 jinja 模板文件,将使用 [minijinja](https://github.com/mitsuhiko/minijinja) 渲染。有关如何扩展当前模板的示例,请参见 [templates](https://github.com/yetone/avante.nvim/blob/main/lua/avante/templates)。 ## 集成 Avante.nvim 可以通过其扩展模块与其他插件协同工作。下面是一个将 Avante 与 nvim-tree 集成的示例,允许你直接从 NvimTree UI 中选择或取消选择文件: ```lua { "yetone/avante.nvim", event = "VeryLazy", keys = { { "a+", function() local tree_ext = require("avante.extensions.nvim_tree") tree_ext.add_file() end, desc = "Select file in NvimTree", ft = "NvimTree", }, { "a-", function() local tree_ext = require("avante.extensions.nvim_tree") tree_ext.remove_file() end, desc = "Deselect file in NvimTree", ft = "NvimTree", }, }, opts = { --- 其他配置 selector = { exclude_auto_select = { "NvimTree" }, }, }, } ``` ## TODOs - [x] 与当前文件聊天 - [x] 应用差异补丁 - [x] 与选定的块聊天 - [x] 斜杠命令 - [x] 编辑选定的块 - [x] 智能 Tab(Cursor 流) - [x] 与项目聊天(您可以使用 `@codebase` 与整个项目聊天) - [x] 与选定文件聊天 - [x] 工具使用 - [x] MCP - [ ] 更好的代码库索引 ## 路线图 - **增强的 AI 交互**:提高 AI 分析和建议的深度,以应对更复杂的编码场景。 - **LSP + Tree-sitter + LLM 集成**:与 LSP 和 Tree-sitter 以及 LLM 集成,以提供更准确和强大的代码建议和分析。 ## 贡献 欢迎为 avante.nvim 做出贡献!如果您有兴趣提供帮助,请随时提交拉取请求或打开问题。在贡献之前,请确保您的代码已经过彻底测试。 有关更多配方和技巧,请参见 [wiki](https://github.com/yetone/avante.nvim/wiki)。 ## 致谢 我们要向以下开源项目的贡献者表示衷心的感谢,他们的代码为 avante.nvim 的开发提供了宝贵的灵感和参考: | Nvim 插件 | 许可证 | 功能 | 位置 | | --------------------------------------------------------------------- | ----------------- | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | [git-conflict.nvim](https://github.com/akinsho/git-conflict.nvim) | 无许可证 | 差异比较功能 | [lua/avante/diff.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/diff.lua) | | [ChatGPT.nvim](https://github.com/jackMort/ChatGPT.nvim) | Apache 2.0 许可证 | 令牌计数的计算 | [lua/avante/utils/tokens.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/utils/tokens.lua) | | [img-clip.nvim](https://github.com/HakonHarnes/img-clip.nvim) | MIT 许可证 | 剪贴板图像支持 | [lua/avante/clipboard.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/clipboard.lua) | | [copilot.lua](https://github.com/zbirenbaum/copilot.lua) | MIT 许可证 | Copilot 支持 | [lua/avante/providers/copilot.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/providers/copilot.lua) | | [jinja.vim](https://github.com/HiPhish/jinja.vim) | MIT 许可证 | 模板文件类型支持 | [syntax/jinja.vim](https://github.com/yetone/avante.nvim/blob/main/syntax/jinja.vim) | | [codecompanion.nvim](https://github.com/olimorris/codecompanion.nvim) | MIT 许可证 | Secrets 逻辑支持 | [lua/avante/providers/init.lua](https://github.com/yetone/avante.nvim/blob/main/lua/avante/providers/init.lua) | | [aider](https://github.com/paul-gauthier/aider) | Apache 2.0 许可证 | 规划模式用户提示 | [lua/avante/templates/planning.avanterules](https://github.com/yetone/avante.nvim/blob/main/lua/avante/templates/planning.avanterules) | 这些项目的源代码的高质量和独创性在我们的开发过程中提供了极大的帮助。我们向这些项目的作者和贡献者表示诚挚的感谢和敬意。正是开源社区的无私奉献推动了像 avante.nvim 这样的项目向前发展。 ## 商业赞助商
Meshy AI
Meshy AI
 
为创作者提供的 #1 AI 3D 模型生成器
BabelTower API
BabelTower API
 
无需帐户,立即使用任何模型
## 许可证 avante.nvim 根据 Apache 2.0 许可证授权。有关更多详细信息,请参阅 [LICENSE](./LICENSE) 文件。 # Star 历史

NebulaGraph Data Intelligence Suite(ngdi)

================================================ FILE: autoload/avante.vim ================================================ function avante#build(...) abort let l:source = get(a:, 1, v:false) return join([luaeval("require('avante_lib').load()") ,luaeval("require('avante.api').build(_A)", l:source)], "\n") endfunction ================================================ FILE: build.sh ================================================ #!/usr/bin/env bash set -e REPO_OWNER="yetone" REPO_NAME="avante.nvim" SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)" # Set the target directory to clone the artifact TARGET_DIR="${SCRIPT_DIR}/build" # Get the artifact download URL based on the platform and Lua version case "$(uname -s)" in Linux*) PLATFORM="linux" LIB_EXT="so" ;; Darwin*) PLATFORM="darwin" LIB_EXT="dylib" ;; CYGWIN* | MINGW* | MSYS*) PLATFORM="windows" LIB_EXT="dll" ;; *) echo "Unsupported platform" exit 1 ;; esac # Get the architecture (x86_64 or aarch64) case "$(uname -m)" in x86_64) ARCH="x86_64" ;; aarch64) ARCH="aarch64" ;; arm64) ARCH="aarch64" ;; *) echo "Unsupported architecture" exit 1 ;; esac # Set the Lua version (lua54 or luajit) LUA_VERSION="${LUA_VERSION:-luajit}" # Set the artifact name pattern ARTIFACT_NAME_PATTERN="avante_lib-$PLATFORM-$ARCH-$LUA_VERSION" test_command() { command -v "$1" >/dev/null 2>&1 } test_gh_auth() { if gh api user >/dev/null 2>&1; then return 0 else return 1 fi } fetch_remote_tags() { git ls-remote --tags origin | cut -f2 | sed 's|refs/tags/||' | while read tag; do if ! git rev-parse "$tag" >/dev/null 2>&1; then git fetch origin "refs/tags/$tag:refs/tags/$tag" fi done } if [ ! -d "$TARGET_DIR" ]; then mkdir -p "$TARGET_DIR" fi fetch_remote_tags latest_tag="$(git describe --tags --abbrev=0 || true)" # will be empty in clone repos built_tag="$(cat build/.tag 2>/dev/null || true)" save_tag() { echo "$latest_tag" > build/.tag } if [[ "$latest_tag" = "$built_tag" && -n "$latest_tag" ]]; then echo "Local build is up to date $latest_tag. No download needed." elif [[ "$latest_tag" != "$built_tag" && -n "$latest_tag" ]]; then echo "Local build is out of date $built_tag. Downloading latest $latest_tag." if test_command "gh" && test_gh_auth; then gh release download "$latest_tag" --repo "github.com/$REPO_OWNER/$REPO_NAME" --pattern "*$ARTIFACT_NAME_PATTERN*" --clobber --output - | tar -zxv -C "$TARGET_DIR" save_tag else # Get the artifact download URL ARTIFACT_URL=$(curl -s "https://api.github.com/repos/$REPO_OWNER/$REPO_NAME/releases/tags/$latest_tag" | grep "browser_download_url" | cut -d '"' -f 4 | grep $ARTIFACT_NAME_PATTERN) set -x mkdir -p "$TARGET_DIR" curl -L "$ARTIFACT_URL" | tar -zxv -C "$TARGET_DIR" save_tag fi else echo "No latest tag found. Building from source." cargo build --release --features=$LUA_VERSION for f in target/release/lib*.$LIB_EXT; do cp "$f" "build/$(echo $f | sed 's#.*/lib##')" done fi ================================================ FILE: crates/avante-html2md/Cargo.toml ================================================ [lib] crate-type = ["cdylib"] [package] name = "avante-html2md" edition.workspace = true rust-version.workspace = true license.workspace = true version.workspace = true [dependencies] htmd = "0.1.6" #html2md = "0.2.15" html2md = { git = "https://gitlab.com/Kanedias/html2md.git", rev = "850ccf756a87fedebcea707c5c981c3103019238" } mlua.workspace = true reqwest = { version = "0.12.12", features = ["blocking", "native-tls-vendored"] } [lints] workspace = true [features] lua51 = ["mlua/lua51"] lua52 = ["mlua/lua52"] lua53 = ["mlua/lua53"] lua54 = ["mlua/lua54"] luajit = ["mlua/luajit"] ================================================ FILE: crates/avante-html2md/src/lib.rs ================================================ use htmd::HtmlToMarkdown; use mlua::prelude::*; use std::error::Error; #[derive(Debug)] enum MyError { HtmlToMd(String), Request(String), } impl std::fmt::Display for MyError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { MyError::HtmlToMd(e) => write!(f, "HTML to Markdown error: {e}"), MyError::Request(e) => write!(f, "Request error: {e}"), } } } impl Error for MyError {} fn do_html2md(html: &str) -> Result { let converter = HtmlToMarkdown::builder() .skip_tags(vec!["script", "style", "header", "footer"]) .build(); let md = converter .convert(html) .map_err(|e| MyError::HtmlToMd(e.to_string()))?; Ok(md) } fn do_fetch_md(url: &str) -> Result { let mut headers = reqwest::header::HeaderMap::new(); headers.insert( reqwest::header::USER_AGENT, reqwest::header::HeaderValue::from_static("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/103.0.0.0 Safari/537.36"), ); let client = reqwest::blocking::Client::builder() .default_headers(headers) .build() .map_err(|e| MyError::Request(e.to_string()))?; let response = client .get(url) .send() .map_err(|e| MyError::Request(e.to_string()))?; let body = response .text() .map_err(|e| MyError::Request(e.to_string()))?; let html = body.trim().to_string(); let md = do_html2md(&html)?; Ok(md) } #[mlua::lua_module] fn avante_html2md(lua: &Lua) -> LuaResult { let exports = lua.create_table()?; exports.set( "fetch_md", lua.create_function(move |_, url: String| -> LuaResult { do_fetch_md(&url).map_err(|e| mlua::Error::RuntimeError(e.to_string())) })?, )?; exports.set( "html2md", lua.create_function(move |_, html: String| -> LuaResult { do_html2md(&html).map_err(|e| mlua::Error::RuntimeError(e.to_string())) })?, )?; Ok(exports) } #[cfg(test)] mod tests { use super::*; #[test] fn test_fetch_md() { let md = do_fetch_md("https://github.com/yetone/avante.nvim").unwrap(); println!("{md}"); } } ================================================ FILE: crates/avante-repo-map/Cargo.toml ================================================ [lib] crate-type = ["cdylib"] [package] name = "avante-repo-map" edition.workspace = true rust-version.workspace = true license.workspace = true version.workspace = true [build-dependencies] cc="*" [dependencies] mlua = { workspace = true } minijinja = { workspace = true } serde = { workspace = true, features = ["derive"] } tree-sitter = "0.23" tree-sitter-language = "0.1" tree-sitter-rust = "0.23" tree-sitter-php = "0.23.11" tree-sitter-python = "0.23" tree-sitter-java = "0.23.5" tree-sitter-javascript = "0.23" tree-sitter-typescript = "0.23" tree-sitter-go = "0.23" tree-sitter-c = "0.23" tree-sitter-cpp = "0.23" tree-sitter-lua = "0.2" tree-sitter-ruby = "0.23" tree-sitter-zig = "1.0.2" tree-sitter-scala = "0.23" tree-sitter-swift = "0.7.0" tree-sitter-elixir = "0.3.1" tree-sitter-c-sharp = "0.23" [lints] workspace = true [features] lua51 = ["mlua/lua51"] lua52 = ["mlua/lua52"] lua53 = ["mlua/lua53"] lua54 = ["mlua/lua54"] luajit = ["mlua/luajit"] ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-c-defs.scm ================================================ ;; Capture extern functions, variables, public classes, and methods (function_definition (storage_class_specifier) @extern ) @function (struct_specifier) @struct (struct_specifier body: (field_declaration_list (field_declaration declarator: (field_identifier))? @class_variable ) ) (declaration (storage_class_specifier) @extern ) @variable ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-c-sharp-defs.scm ================================================ (class_declaration name: (identifier) @class (parameter_list)? @method) ;; Primary constructor (record_declaration name: (identifier) @class (parameter_list)? @method) ;; Primary constructor (interface_declaration name: (identifier) @class) (method_declaration) @method (constructor_declaration) @method (property_declaration) @class_variable (field_declaration (variable_declaration (variable_declarator))) @class_variable (enum_declaration body: (enum_member_declaration_list (enum_member_declaration) @enum_item)) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-cpp-defs.scm ================================================ ;; Capture functions, variables, nammespaces, classes, methods, and enums (namespace_definition) @namespace (function_definition) @function (class_specifier) @class (class_specifier body: (field_declaration_list (declaration declarator: (function_declarator))? @method (field_declaration declarator: (function_declarator))? @method (function_definition)? @method (function_declarator)? @method (field_declaration declarator: (field_identifier))? @class_variable ) ) (struct_specifier) @struct (struct_specifier body: (field_declaration_list (declaration declarator: (function_declarator))? @method (field_declaration declarator: (function_declarator))? @method (function_definition)? @method (function_declarator)? @method (field_declaration declarator: (field_identifier))? @class_variable ) ) ((declaration type: (_))) @variable (enumerator_list ((enumerator) @enum_item)) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-elixir-defs.scm ================================================ ; * modules and protocols (call target: (identifier) @ignore (arguments (alias) @class) (#match? @ignore "^(defmodule|defprotocol)$")) ; * functions (call target: (identifier) @ignore (arguments [ ; zero-arity functions with no parentheses (identifier) @method ; regular function clause (call target: (identifier) @method) ; function clause with a guard clause (binary_operator left: (call target: (identifier) @method) operator: "when") ]) (#match? @ignore "^(def|defdelegate|defguard|defn)$")) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-go-defs.scm ================================================ ;; Capture top-level functions and struct definitions (source_file (var_declaration (var_spec) @variable ) ) (source_file (const_declaration (const_spec) @variable ) ) (source_file (function_declaration) @function ) (source_file (type_declaration (type_spec (struct_type)) @class ) ) (source_file (type_declaration (type_spec (struct_type (field_declaration_list (field_declaration) @class_variable))) ) ) (source_file (method_declaration) @method ) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-java-defs.scm ================================================ ;; Capture exported functions, arrow functions, variables, classes, and method definitions (class_declaration name: (identifier) @class) (interface_declaration name: (identifier) @class) (enum_declaration name: (identifier) @enum) (enum_constant name: (identifier) @enum_item) (class_body (field_declaration) @class_variable) (class_body (constructor_declaration) @method) (class_body (method_declaration) @method) (interface_body (method_declaration) @method) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-javascript-defs.scm ================================================ ;; Capture exported functions, arrow functions, variables, classes, and method definitions (export_statement declaration: (lexical_declaration (variable_declarator) @variable ) ) (export_statement declaration: (function_declaration) @function ) (export_statement declaration: (class_declaration body: (class_body (field_definition) @class_variable ) ) ) (export_statement declaration: (class_declaration body: (class_body (method_definition) @method ) ) ) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-lua-defs.scm ================================================ ;; Capture function and method definitions (variable_list) @variable (function_declaration) @function ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-php-defs.scm ================================================ ;; Capture exported functions, arrow functions, variables, classes, and method definitions (class_declaration) @class (interface_declaration) @class (function_definition) @function (assignment_expression) @assignment (const_declaration (const_element (name) @variable)) (_ body: (declaration_list (property_declaration) @class_variable)) (_ body: (declaration_list (method_declaration) @method)) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-python-defs.scm ================================================ ;; Capture top-level functions, class, and method definitions (module (expression_statement (assignment) @assignment ) ) (module (function_definition) @function ) (module (class_definition body: (block (expression_statement (assignment) @class_assignment ) ) ) ) (module (class_definition body: (block (function_definition) @method ) ) ) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-ruby-defs.scm ================================================ ;; Capture top-level methods, class definitions, and methods within classes (class (body_statement (call)? @class_call (assignment)? @class_assignment (method)? @method ) ) @class (program (method) @function ) (program (assignment) @assignment ) (module) @module (module (body_statement (call)? @class_call (assignment)? @class_assignment (method)? @method ) ) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-rust-defs.scm ================================================ ;; Capture public functions, structs, methods, and variable definitions (function_item) @function (impl_item body: (declaration_list (function_item) @method ) ) (struct_item) @class (struct_item body: (field_declaration_list (field_declaration) @class_variable ) ) (enum_item body: (enum_variant_list (enum_variant) @enum_item ) ) (const_item) @variable (static_item) @variable ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-scala-defs.scm ================================================ (class_definition name: (identifier) @class) (object_definition name: (identifier) @class) (trait_definition name: (identifier) @class) (simple_enum_case name: (identifier) @enum_item) (full_enum_case name: (identifier) @enum_item) (template_body (function_definition) @method ) (template_body (function_declaration) @method ) (template_body (val_definition) @class_variable ) (template_body (val_declaration) @class_variable ) (template_body (var_definition) @class_variable ) (template_body (var_declaration) @class_variable ) (compilation_unit (function_definition) @function ) (compilation_unit (val_definition) @variable ) (compilation_unit (var_definition) @variable ) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-swift-defs.scm ================================================ (property_declaration) @variable (function_declaration) @function (class_declaration _? [ "struct" "class" ]) @class (class_declaration _? "enum" ) @enum (class_body (property_declaration) @class_variable) (class_body (function_declaration) @method) (class_body (init_declaration) @method) (protocol_declaration body: (protocol_body (protocol_function_declaration) @function)) (protocol_declaration body: (protocol_body (protocol_property_declaration) @class_variable)) (class_declaration body: (enum_class_body (enum_entry) @enum_item)) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-typescript-defs.scm ================================================ ;; Capture exported functions, arrow functions, variables, classes, and method definitions (export_statement declaration: (lexical_declaration (variable_declarator) @variable ) ) (export_statement declaration: (function_declaration) @function ) (export_statement declaration: (class_declaration body: (class_body (public_field_definition) @class_variable ) ) ) (interface_declaration body: (interface_body (property_signature) @class_variable ) ) (type_alias_declaration value: (object_type (property_signature) @class_variable ) ) (export_statement declaration: (class_declaration body: (class_body (method_definition) @method ) ) ) ================================================ FILE: crates/avante-repo-map/queries/tree-sitter-zig-defs.scm ================================================ ;; Capture functions, structs, methods, variable definitions, and unions in Zig (variable_declaration (identifier) (struct_declaration (container_field) @class_variable)) (variable_declaration (identifier) (struct_declaration (function_declaration name: (identifier) @method))) (variable_declaration (identifier) (enum_declaration (container_field type: (identifier) @enum_item))) (variable_declaration (identifier) (union_declaration (container_field name: (identifier) @union_item))) (source_file (function_declaration) @function) (source_file (variable_declaration (identifier) @variable)) ================================================ FILE: crates/avante-repo-map/src/lib.rs ================================================ #![allow(clippy::unnecessary_map_or)] use mlua::prelude::*; use std::cell::RefCell; use std::collections::BTreeMap; use tree_sitter::{Node, Parser, Query, QueryCursor}; use tree_sitter_language::LanguageFn; #[derive(Debug, Clone)] pub struct Func { pub name: String, pub params: String, pub return_type: String, pub accessibility_modifier: Option, } #[derive(Debug, Clone)] pub struct Class { pub type_name: String, pub name: String, pub methods: Vec, pub properties: Vec, pub visibility_modifier: Option, } #[derive(Debug, Clone)] pub struct Enum { pub name: String, pub items: Vec, } #[derive(Debug, Clone)] pub struct Union { pub name: String, pub items: Vec, } #[derive(Debug, Clone)] pub struct Variable { pub name: String, pub value_type: String, } #[derive(Debug, Clone)] pub enum Definition { Func(Func), Class(Class), Module(Class), Enum(Enum), Variable(Variable), Union(Union), // TODO: Namespace support } fn get_ts_language(language: &str) -> Option { match language { "rust" => Some(tree_sitter_rust::LANGUAGE), "python" => Some(tree_sitter_python::LANGUAGE), "php" => Some(tree_sitter_php::LANGUAGE_PHP), "java" => Some(tree_sitter_java::LANGUAGE), "javascript" => Some(tree_sitter_javascript::LANGUAGE), "typescript" => Some(tree_sitter_typescript::LANGUAGE_TSX), "go" => Some(tree_sitter_go::LANGUAGE), "c" => Some(tree_sitter_c::LANGUAGE), "cpp" => Some(tree_sitter_cpp::LANGUAGE), "lua" => Some(tree_sitter_lua::LANGUAGE), "ruby" => Some(tree_sitter_ruby::LANGUAGE), "zig" => Some(tree_sitter_zig::LANGUAGE), "scala" => Some(tree_sitter_scala::LANGUAGE), "swift" => Some(tree_sitter_swift::LANGUAGE), "elixir" => Some(tree_sitter_elixir::LANGUAGE), "csharp" => Some(tree_sitter_c_sharp::LANGUAGE), _ => None, } } const C_QUERY: &str = include_str!("../queries/tree-sitter-c-defs.scm"); const CPP_QUERY: &str = include_str!("../queries/tree-sitter-cpp-defs.scm"); const GO_QUERY: &str = include_str!("../queries/tree-sitter-go-defs.scm"); const JAVA_QUERY: &str = include_str!("../queries/tree-sitter-java-defs.scm"); const JAVASCRIPT_QUERY: &str = include_str!("../queries/tree-sitter-javascript-defs.scm"); const LUA_QUERY: &str = include_str!("../queries/tree-sitter-lua-defs.scm"); const PYTHON_QUERY: &str = include_str!("../queries/tree-sitter-python-defs.scm"); const PHP_QUERY: &str = include_str!("../queries/tree-sitter-php-defs.scm"); const RUST_QUERY: &str = include_str!("../queries/tree-sitter-rust-defs.scm"); const ZIG_QUERY: &str = include_str!("../queries/tree-sitter-zig-defs.scm"); const TYPESCRIPT_QUERY: &str = include_str!("../queries/tree-sitter-typescript-defs.scm"); const RUBY_QUERY: &str = include_str!("../queries/tree-sitter-ruby-defs.scm"); const SCALA_QUERY: &str = include_str!("../queries/tree-sitter-scala-defs.scm"); const SWIFT_QUERY: &str = include_str!("../queries/tree-sitter-swift-defs.scm"); const ELIXIR_QUERY: &str = include_str!("../queries/tree-sitter-elixir-defs.scm"); const CSHARP_QUERY: &str = include_str!("../queries/tree-sitter-c-sharp-defs.scm"); fn get_definitions_query(language: &str) -> Result { let ts_language = get_ts_language(language); if ts_language.is_none() { return Err(format!("Unsupported language: {language}")); } let ts_language = ts_language.unwrap(); let contents = match language { "c" => C_QUERY, "cpp" => CPP_QUERY, "go" => GO_QUERY, "java" => JAVA_QUERY, "javascript" => JAVASCRIPT_QUERY, "lua" => LUA_QUERY, "php" => PHP_QUERY, "python" => PYTHON_QUERY, "rust" => RUST_QUERY, "zig" => ZIG_QUERY, "typescript" => TYPESCRIPT_QUERY, "ruby" => RUBY_QUERY, "scala" => SCALA_QUERY, "swift" => SWIFT_QUERY, "elixir" => ELIXIR_QUERY, "csharp" => CSHARP_QUERY, _ => return Err(format!("Unsupported language: {language}")), }; let query = Query::new(&ts_language.into(), contents) .unwrap_or_else(|e| panic!("Failed to parse query for {language}: {e}")); Ok(query) } fn get_closest_ancestor_name(node: &Node, source: &str) -> String { let mut parent = node.parent(); while let Some(parent_node) = parent { let name_node = parent_node.child_by_field_name("name"); if let Some(name_node) = name_node { return get_node_text(&name_node, source.as_bytes()).to_string(); } parent = parent_node.parent(); } String::new() } fn find_ancestor_by_type<'a>(node: &'a Node, parent_type: &str) -> Option> { let mut parent = node.parent(); while let Some(parent_node) = parent { if parent_node.kind() == parent_type { return Some(parent_node); } parent = parent_node.parent(); } None } fn find_first_ancestor_by_types<'a>( node: &'a Node, possible_parent_types: &[&str], ) -> Option> { let mut parent = node.parent(); while let Some(parent_node) = parent { if possible_parent_types.contains(&parent_node.kind()) { return Some(parent_node); } parent = parent_node.parent(); } None } fn find_descendant_by_type<'a>(node: &'a Node, child_type: &str) -> Option> { let mut cursor = node.walk(); for i in 0..node.descendant_count() { cursor.goto_descendant(i); let node = cursor.node(); if node.kind() == child_type { return Some(node); } } None } fn ruby_method_is_private<'a>(node: &'a Node, source: &'a [u8]) -> bool { let mut prev_sibling = node.prev_sibling(); while let Some(prev_sibling_node) = prev_sibling { if prev_sibling_node.kind() == "identifier" { let text = prev_sibling_node.utf8_text(source).unwrap_or_default(); if text == "private" { return true; } else if text == "public" || text == "protected" { return false; } } else if prev_sibling_node.kind() == "class" || prev_sibling_node.kind() == "module" { return false; } prev_sibling = prev_sibling_node.prev_sibling(); } false } fn find_child_by_type<'a>(node: &'a Node, child_type: &str) -> Option> { node.children(&mut node.walk()) .find(|child| child.kind() == child_type) } // Zig-specific function to find the parent variable declaration fn zig_find_parent_variable_declaration_name<'a>( node: &'a Node, source: &'a [u8], ) -> Option { let vardec = find_ancestor_by_type(node, "variable_declaration"); if let Some(vardec) = vardec { // Find the identifier child node, which represents the class name let identifier_node = find_child_by_type(&vardec, "identifier"); if let Some(identifier_node) = identifier_node { return Some(get_node_text(&identifier_node, source)); } } None } fn zig_is_declaration_public<'a>(node: &'a Node, declaration_type: &str, source: &'a [u8]) -> bool { let declaration = find_ancestor_by_type(node, declaration_type); if let Some(declaration) = declaration { let declaration_text = get_node_text(&declaration, source); return declaration_text.starts_with("pub"); } false } fn zig_is_variable_declaration_public<'a>(node: &'a Node, source: &'a [u8]) -> bool { zig_is_declaration_public(node, "variable_declaration", source) } fn zig_is_function_declaration_public<'a>(node: &'a Node, source: &'a [u8]) -> bool { zig_is_declaration_public(node, "function_declaration", source) } fn zig_find_type_in_parent<'a>(node: &'a Node, source: &'a [u8]) -> Option { // First go to the parent and then get the child_by_field_name "type" if let Some(parent) = node.parent() { if let Some(type_node) = parent.child_by_field_name("type") { return Some(get_node_text(&type_node, source)); } } None } fn csharp_is_primary_constructor(node: &Node) -> bool { node.kind() == "parameter_list" && node.parent().map_or(false, |n| { n.kind() == "class_declaration" || n.kind() == "record_declaration" }) } fn csharp_find_parent_type_node<'a>(node: &'a Node) -> Option> { find_first_ancestor_by_types(node, &["class_declaration", "record_declaration"]) } fn ex_find_parent_module_declaration_name<'a>(node: &'a Node, source: &'a [u8]) -> Option { let mut parent = node.parent(); while let Some(parent_node) = parent { if parent_node.kind() == "call" { let text = get_node_text(&parent_node, source); if text.starts_with("defmodule ") { let arguments_node = find_child_by_type(&parent_node, "arguments"); if let Some(arguments_node) = arguments_node { return Some(get_node_text(&arguments_node, source)); } } } parent = parent_node.parent(); } None } fn ruby_find_parent_module_declaration_name<'a>( node: &'a Node, source: &'a [u8], ) -> Option { let mut path_parts = Vec::new(); let mut current = Some(*node); while let Some(current_node) = current { if current_node.kind() == "module" || current_node.kind() == "class" { if let Some(name_node) = current_node.child_by_field_name("name") { path_parts.push(get_node_text(&name_node, source)); } } current = current_node.parent(); } if path_parts.is_empty() { None } else { path_parts.reverse(); Some(path_parts.join("::")) } } fn get_node_text<'a>(node: &'a Node, source: &'a [u8]) -> String { node.utf8_text(source).unwrap_or_default().to_string() } fn get_node_type<'a>(node: &'a Node, source: &'a [u8]) -> String { let predefined_type_node = find_descendant_by_type(node, "predefined_type"); if let Some(type_node) = predefined_type_node { return type_node.utf8_text(source).unwrap().to_string(); } let value_type_node = node.child_by_field_name("type"); value_type_node .map(|n| n.utf8_text(source).unwrap().to_string()) .unwrap_or_default() } fn is_first_letter_uppercase(name: &str) -> bool { if name.is_empty() { return false; } name.chars().next().unwrap().is_uppercase() } // Given a language, parse the given source code and return exported definitions fn extract_definitions(language: &str, source: &str) -> Result, String> { let ts_language = get_ts_language(language); if ts_language.is_none() { return Ok(vec![]); } let ts_language = ts_language.unwrap(); let mut definitions = Vec::new(); let mut parser = Parser::new(); parser .set_language(&ts_language.into()) .unwrap_or_else(|_| panic!("Failed to set language for {language}")); let tree = parser .parse(source, None) .unwrap_or_else(|| panic!("Failed to parse source code for {language}")); let root_node = tree.root_node(); let query = get_definitions_query(language)?; let mut query_cursor = QueryCursor::new(); let captures = query_cursor.captures(&query, root_node, source.as_bytes()); let mut class_def_map: BTreeMap> = BTreeMap::new(); let mut enum_def_map: BTreeMap> = BTreeMap::new(); let mut union_def_map: BTreeMap> = BTreeMap::new(); let ensure_class_def = |language: &str, name: &str, class_def_map: &mut BTreeMap>| { let mut type_name = "class"; if language == "elixir" { type_name = "module"; } class_def_map.entry(name.to_string()).or_insert_with(|| { RefCell::new(Class { type_name: type_name.to_string(), name: name.to_string(), methods: vec![], properties: vec![], visibility_modifier: None, }) }); }; let ensure_module_def = |name: &str, class_def_map: &mut BTreeMap>| { class_def_map.entry(name.to_string()).or_insert_with(|| { RefCell::new(Class { name: name.to_string(), type_name: "module".to_string(), methods: vec![], properties: vec![], visibility_modifier: None, }) }); }; let ensure_enum_def = |name: &str, enum_def_map: &mut BTreeMap>| { enum_def_map.entry(name.to_string()).or_insert_with(|| { RefCell::new(Enum { name: name.to_string(), items: vec![], }) }); }; let ensure_union_def = |name: &str, union_def_map: &mut BTreeMap>| { union_def_map.entry(name.to_string()).or_insert_with(|| { RefCell::new(Union { name: name.to_string(), items: vec![], }) }); }; // Sometimes, multiple queries capture the same node with the same capture name. // We need to ensure that we only add the node to the definition map once. let mut captured_nodes: BTreeMap> = BTreeMap::new(); for (m, _) in captures { for capture in m.captures { let capture_name = &query.capture_names()[capture.index as usize]; let node = capture.node; let node_text = node.utf8_text(source.as_bytes()).unwrap(); let node_id = node.id(); if captured_nodes .get(*capture_name) .map_or(false, |v| v.contains(&node_id)) { continue; } captured_nodes .entry(String::from(*capture_name)) .or_default() .push(node_id); let name = match language { "cpp" => { if *capture_name == "class" { node.child_by_field_name("name") .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(node_text) .to_string() } else { let ident = find_descendant_by_type(&node, "field_identifier") .or_else(|| find_descendant_by_type(&node, "operator_name")) .or_else(|| find_descendant_by_type(&node, "identifier")) .map(|n| n.utf8_text(source.as_bytes()).unwrap()); if let Some(ident) = ident { let scope = node .child_by_field_name("declarator") .and_then(|n| n.child_by_field_name("declarator")) .and_then(|n| n.child_by_field_name("scope")); if let Some(scope_node) = scope { format!( "{}::{}", scope_node.utf8_text(source.as_bytes()).unwrap(), ident ) } else { ident.to_string() } } else { node_text.to_string() } } } "scala" => node .child_by_field_name("name") .or_else(|| node.child_by_field_name("pattern")) .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(node_text) .to_string(), "csharp" => { let mut identifier = node; // Handle primary constructors (they are direct children of *_declaration) if *capture_name == "method" && csharp_is_primary_constructor(&node) { identifier = node.parent().unwrap_or(node); } else if *capture_name == "class_variable" { identifier = find_descendant_by_type(&node, "variable_declarator").unwrap_or(node); } identifier .child_by_field_name("name") .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(node_text) .to_string() } "ruby" => { let name = node .child_by_field_name("name") .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(node_text) .to_string(); if *capture_name == "class" || *capture_name == "module" { ruby_find_parent_module_declaration_name(&node, source.as_bytes()) .unwrap_or(name) } else { name } } _ => node .child_by_field_name("name") .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(node_text) .to_string(), }; match *capture_name { "class" => { if !name.is_empty() { if language == "go" && !is_first_letter_uppercase(&name) { continue; } ensure_class_def(language, &name, &mut class_def_map); let visibility_modifier_node = find_child_by_type(&node, "visibility_modifier"); let visibility_modifier = visibility_modifier_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); let class_def = class_def_map.get_mut(&name).unwrap(); class_def.borrow_mut().visibility_modifier = if visibility_modifier.is_empty() { None } else { Some(visibility_modifier.to_string()) }; } } "module" => { if !name.is_empty() { ensure_module_def(&name, &mut class_def_map); } } "enum_item" => { let visibility_modifier_node = find_descendant_by_type(&node, "visibility_modifier"); let visibility_modifier = visibility_modifier_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); if language == "rust" && !visibility_modifier.contains("pub") { continue; } if language == "zig" && !zig_is_variable_declaration_public(&node, source.as_bytes()) { continue; } let mut enum_name = get_closest_ancestor_name(&node, source); if language == "zig" { enum_name = zig_find_parent_variable_declaration_name(&node, source.as_bytes()) .unwrap_or_default(); } if language == "scala" { if let Some(enum_node) = find_ancestor_by_type(&node, "enum_definition") { if let Some(name_node) = enum_node.child_by_field_name("name") { enum_name = name_node.utf8_text(source.as_bytes()).unwrap().to_string(); } } } if !enum_name.is_empty() && language == "go" && !is_first_letter_uppercase(&enum_name) { continue; } ensure_enum_def(&enum_name, &mut enum_def_map); let enum_def = enum_def_map.get_mut(&enum_name).unwrap(); let enum_type_node = find_descendant_by_type(&node, "type_identifier"); let enum_type = enum_type_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); let variable = Variable { name: name.to_string(), value_type: enum_type.to_string(), }; enum_def.borrow_mut().items.push(variable); } "union_item" => { if language != "zig" { continue; } if !zig_is_variable_declaration_public(&node, source.as_bytes()) { continue; } let union_name = zig_find_parent_variable_declaration_name(&node, source.as_bytes()) .unwrap_or_default(); ensure_union_def(&union_name, &mut union_def_map); let union_def = union_def_map.get_mut(&union_name).unwrap(); let union_type_node = find_descendant_by_type(&node, "type_identifier"); let union_type = union_type_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); let variable = Variable { name: name.to_string(), value_type: union_type.to_string(), }; union_def.borrow_mut().items.push(variable); } "method" => { // TODO: C++: Skip private/protected class/struct methods let visibility_modifier_node = find_descendant_by_type(&node, "visibility_modifier"); let visibility_modifier = visibility_modifier_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); if language == "swift" { if visibility_modifier.contains("private") { continue; } } if language == "java" { let modifier_node = find_descendant_by_type(&node, "modifiers"); if modifier_node.is_some() { let modifier_text = modifier_node.unwrap().utf8_text(source.as_bytes()).unwrap(); if modifier_text.contains("private") { continue; } } } if language == "rust" && !visibility_modifier.contains("pub") { continue; } if language == "zig" && !(zig_is_function_declaration_public(&node, source.as_bytes()) && zig_is_variable_declaration_public(&node, source.as_bytes())) { continue; } if language == "cpp" && find_descendant_by_type(&node, "destructor_name").is_some() { continue; } if !name.is_empty() && language == "go" && !is_first_letter_uppercase(&name) { continue; } if language == "csharp" { let csharp_visibility = find_descendant_by_type(&node, "modifier"); if csharp_visibility.is_none() && !csharp_is_primary_constructor(&node) { continue; } if csharp_visibility.is_some() { let csharp_visibility_text = csharp_visibility .unwrap() .utf8_text(source.as_bytes()) .unwrap(); if csharp_visibility_text == "private" { continue; } } } let mut params_node = node .child_by_field_name("parameters") .or_else(|| find_descendant_by_type(&node, "parameter_list")); let zig_function_node = find_ancestor_by_type(&node, "function_declaration"); if language == "zig" { params_node = zig_function_node .as_ref() .and_then(|n| find_child_by_type(n, "parameters")); } let ex_function_node = find_ancestor_by_type(&node, "call"); if language == "elixir" { params_node = ex_function_node .as_ref() .and_then(|n| find_child_by_type(n, "arguments")); } let params = params_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or("()"); let mut return_type_node = match language { "cpp" => node.child_by_field_name("type"), "csharp" => node.child_by_field_name("returns"), _ => node.child_by_field_name("return_type"), }; if language == "cpp" { let class_specifier_node = find_ancestor_by_type(&node, "class_specifier"); let type_identifier_node = class_specifier_node.and_then(|n| n.child_by_field_name("name")); if let Some(type_identifier_node) = type_identifier_node { let type_identifier_text = type_identifier_node.utf8_text(source.as_bytes()).unwrap(); if name == type_identifier_text { return_type_node = Some(type_identifier_node); } } } if language == "csharp" { let type_specifier_node = csharp_find_parent_type_node(&node); let type_identifier_node = type_specifier_node.and_then(|n| n.child_by_field_name("name")); if let Some(type_identifier_node) = type_identifier_node { let type_identifier_text = type_identifier_node.utf8_text(source.as_bytes()).unwrap(); if name == type_identifier_text { return_type_node = Some(type_identifier_node); } } } if return_type_node.is_none() { return_type_node = node.child_by_field_name("result"); } let mut return_type = "void".to_string(); if language == "elixir" { return_type = String::new(); } if return_type_node.is_some() { return_type = get_node_type(&return_type_node.unwrap(), source.as_bytes()); if return_type.is_empty() { return_type = return_type_node .unwrap() .utf8_text(source.as_bytes()) .unwrap_or("void") .to_string(); } } let impl_item_node = find_ancestor_by_type(&node, "impl_item"); let receiver_node = node.child_by_field_name("receiver"); let class_name = if language == "zig" { zig_find_parent_variable_declaration_name(&node, source.as_bytes()) .unwrap_or_default() } else if language == "elixir" { ex_find_parent_module_declaration_name(&node, source.as_bytes()) .unwrap_or_default() } else if language == "cpp" { find_ancestor_by_type(&node, "class_specifier") .or_else(|| find_ancestor_by_type(&node, "struct_specifier")) .and_then(|n| n.child_by_field_name("name")) .and_then(|n| n.utf8_text(source.as_bytes()).ok()) .unwrap_or("") .to_string() } else if language == "csharp" { csharp_find_parent_type_node(&node) .and_then(|n| n.child_by_field_name("name")) .and_then(|n| n.utf8_text(source.as_bytes()).ok()) .unwrap_or("") .to_string() } else if language == "ruby" { ruby_find_parent_module_declaration_name(&node, source.as_bytes()) .unwrap_or_default() } else if let Some(impl_item) = impl_item_node { let impl_type_node = impl_item.child_by_field_name("type"); impl_type_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or("") .to_string() } else if let Some(receiver) = receiver_node { let type_identifier_node = find_descendant_by_type(&receiver, "type_identifier"); type_identifier_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or("") .to_string() } else { get_closest_ancestor_name(&node, source).to_string() }; if language == "go" && !is_first_letter_uppercase(&class_name) { continue; } ensure_class_def(language, &class_name, &mut class_def_map); let class_def = class_def_map.get_mut(&class_name).unwrap(); let accessibility_modifier_node = find_descendant_by_type(&node, "accessibility_modifier"); let accessibility_modifier = if language == "ruby" { if ruby_method_is_private(&node, source.as_bytes()) { "private" } else { "" } } else { accessibility_modifier_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or("") }; let func = Func { name: name.to_string(), params: params.to_string(), return_type: return_type.to_string(), accessibility_modifier: if accessibility_modifier.is_empty() { None } else { Some(accessibility_modifier.to_string()) }, }; class_def.borrow_mut().methods.push(func); } "class_assignment" => { let visibility_modifier_node = find_descendant_by_type(&node, "visibility_modifier"); let visibility_modifier = visibility_modifier_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); if language == "swift" || language == "java" { if visibility_modifier.contains("private") { continue; } } if language == "java" { let modifier_node = find_descendant_by_type(&node, "modifiers"); if modifier_node.is_some() { let modifier_text = modifier_node.unwrap().utf8_text(source.as_bytes()).unwrap(); if modifier_text.contains("private") { continue; } } } if language == "rust" && !visibility_modifier.contains("pub") { continue; } let left_node = node.child_by_field_name("left"); let left = left_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); let value_type = get_node_type(&node, source.as_bytes()); let mut class_name = get_closest_ancestor_name(&node, source); if !class_name.is_empty() { if language == "ruby" { if let Some(namespaced_name) = ruby_find_parent_module_declaration_name(&node, source.as_bytes()) { class_name = namespaced_name; } } else if language == "go" && !is_first_letter_uppercase(&class_name) { continue; } } if class_name.is_empty() { continue; } ensure_class_def(language, &class_name, &mut class_def_map); let class_def = class_def_map.get_mut(&class_name).unwrap(); let variable = Variable { name: left.to_string(), value_type: value_type.to_string(), }; class_def.borrow_mut().properties.push(variable); } "class_variable" => { // TODO: C++: Skip private/protected class/struct variables let visibility_modifier_node = find_descendant_by_type(&node, "visibility_modifier"); let visibility_modifier = visibility_modifier_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); if language == "rust" && !visibility_modifier.contains("pub") { continue; } if language == "swift" || language == "java" { if visibility_modifier.contains("private") { continue; } } if language == "java" { let modifier_node = find_descendant_by_type(&node, "modifiers"); if modifier_node.is_some() { let modifier_text = modifier_node.unwrap().utf8_text(source.as_bytes()).unwrap(); if modifier_text.contains("private") { continue; } } } let value_type = get_node_type(&node, source.as_bytes()); if language == "zig" { // when top level class is not public, skip if !zig_is_variable_declaration_public(&node, source.as_bytes()) { continue; } } let mut class_name = get_closest_ancestor_name(&node, source); if language == "cpp" { class_name = find_ancestor_by_type(&node, "class_specifier") .or_else(|| find_ancestor_by_type(&node, "struct_specifier")) .and_then(|n| n.child_by_field_name("name")) .and_then(|n| n.utf8_text(source.as_bytes()).ok()) .unwrap_or("") .to_string(); } if language == "csharp" { let csharp_visibility = find_descendant_by_type(&node, "modifier"); if csharp_visibility.is_none() { continue; } let csharp_visibility_text = csharp_visibility .unwrap() .utf8_text(source.as_bytes()) .unwrap(); if csharp_visibility_text == "private" { continue; } } if language == "zig" { class_name = zig_find_parent_variable_declaration_name(&node, source.as_bytes()) .unwrap_or_default(); } if !class_name.is_empty() && language == "go" && !is_first_letter_uppercase(&class_name) { continue; } if class_name.is_empty() { continue; } if !name.is_empty() && language == "go" && !is_first_letter_uppercase(&name) { continue; } ensure_class_def(language, &class_name, &mut class_def_map); let class_def = class_def_map.get_mut(&class_name).unwrap(); let variable = Variable { name: name.to_string(), value_type: value_type.to_string(), }; class_def.borrow_mut().properties.push(variable); } "function" | "arrow_function" => { let visibility_modifier_node = find_descendant_by_type(&node, "visibility_modifier"); let visibility_modifier = visibility_modifier_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); if language == "swift" || language == "java" { if visibility_modifier.contains("private") { continue; } if node.parent().is_some() { continue; } } if language == "java" { let modifier_node = find_descendant_by_type(&node, "modifiers"); if modifier_node.is_some() { let modifier_text = modifier_node.unwrap().utf8_text(source.as_bytes()).unwrap(); if modifier_text.contains("private") { continue; } } } if language == "rust" && !visibility_modifier.contains("pub") { continue; } if language == "zig" { let variable_declaration_text = node.utf8_text(source.as_bytes()).unwrap_or(""); if !variable_declaration_text.contains("pub") { continue; } } if !name.is_empty() && language == "go" && !is_first_letter_uppercase(&name) { continue; } let impl_item_node = find_ancestor_by_type(&node, "impl_item"); if impl_item_node.is_some() { continue; } let class_specifier_node = find_ancestor_by_type(&node, "class_specifier"); if class_specifier_node.is_some() { continue; } let struct_specifier_node = find_ancestor_by_type(&node, "struct_specifier"); if struct_specifier_node.is_some() { continue; } let function_node = find_ancestor_by_type(&node, "function_declaration") .or_else(|| find_ancestor_by_type(&node, "function_definition")); if function_node.is_some() { continue; } let params_node = node .child_by_field_name("parameters") .or_else(|| find_descendant_by_type(&node, "parameter_list")); let params = params_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or("()"); let mut return_type = "void".to_string(); let return_type_node = match language { "cpp" => node.child_by_field_name("type"), _ => node .child_by_field_name("return_type") .or_else(|| node.child_by_field_name("result")), }; if return_type_node.is_some() { return_type = get_node_type(&return_type_node.unwrap(), source.as_bytes()); if return_type.is_empty() { return_type = return_type_node .unwrap() .utf8_text(source.as_bytes()) .unwrap_or("void") .to_string(); } } let accessibility_modifier_node = find_descendant_by_type(&node, "accessibility_modifier"); let accessibility_modifier = accessibility_modifier_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); let func = Func { name: name.to_string(), params: params.to_string(), return_type: return_type.to_string(), accessibility_modifier: if accessibility_modifier.is_empty() { None } else { Some(accessibility_modifier.to_string()) }, }; definitions.push(Definition::Func(func)); } "assignment" => { let visibility_modifier_node = find_descendant_by_type(&node, "visibility_modifier"); let visibility_modifier = visibility_modifier_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); if language == "swift" || language == "java" { if visibility_modifier.contains("private") { continue; } } if language == "java" { let modifier_node = find_descendant_by_type(&node, "modifiers"); if modifier_node.is_some() { let modifier_text = modifier_node.unwrap().utf8_text(source.as_bytes()).unwrap(); if modifier_text.contains("private") { continue; } } } if language == "rust" && !visibility_modifier.contains("pub") { continue; } let impl_item_node = find_ancestor_by_type(&node, "impl_item") .or_else(|| find_ancestor_by_type(&node, "class_declaration")) .or_else(|| find_ancestor_by_type(&node, "class_definition")); if impl_item_node.is_some() { continue; } let function_node = find_ancestor_by_type(&node, "function_declaration") .or_else(|| find_ancestor_by_type(&node, "function_definition")); if function_node.is_some() { continue; } let left_node = node.child_by_field_name("left"); let left = left_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); if !left.is_empty() && language == "go" && !is_first_letter_uppercase(left) { continue; } let value_type = get_node_type(&node, source.as_bytes()); let variable = Variable { name: left.to_string(), value_type: value_type.to_string(), }; definitions.push(Definition::Variable(variable)); } "variable" => { let visibility_modifier_node = find_descendant_by_type(&node, "visibility_modifier"); let visibility_modifier = visibility_modifier_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or(""); if language == "swift" { if visibility_modifier.contains("private") { continue; } } if language == "java" { let modifier_node = find_descendant_by_type(&node, "modifiers"); if modifier_node.is_some() { let modifier_text = modifier_node.unwrap().utf8_text(source.as_bytes()).unwrap(); if modifier_text.contains("private") { continue; } } } if language == "rust" && !visibility_modifier.contains("pub") { continue; } if language == "zig" && !zig_is_variable_declaration_public(&node, source.as_bytes()) { continue; } let impl_item_node = find_ancestor_by_type(&node, "impl_item") .or_else(|| find_ancestor_by_type(&node, "class_declaration")) .or_else(|| find_ancestor_by_type(&node, "class_definition")); if impl_item_node.is_some() { continue; } let function_node = find_ancestor_by_type(&node, "function_declaration") .or_else(|| find_ancestor_by_type(&node, "function_definition")); if function_node.is_some() { continue; } let value_node = node.child_by_field_name("value"); if value_node.is_some() { let value_type = value_node.unwrap().kind(); if value_type == "arrow_function" { let params_node = value_node.unwrap().child_by_field_name("parameters"); let params = params_node .map(|n| n.utf8_text(source.as_bytes()).unwrap()) .unwrap_or("()"); let mut return_type = "void".to_string(); let return_type_node = value_node.unwrap().child_by_field_name("return_type"); if return_type_node.is_some() { return_type = get_node_type(&return_type_node.unwrap(), source.as_bytes()); } let func = Func { name: name.to_string(), params: params.to_string(), return_type, accessibility_modifier: None, }; definitions.push(Definition::Func(func)); continue; } } let mut value_type = get_node_type(&node, source.as_bytes()); if language == "zig" { if let Some(zig_type) = zig_find_type_in_parent(&node, source.as_bytes()) { value_type = zig_type; } else { continue; }; } if !name.is_empty() && language == "go" && !is_first_letter_uppercase(&name) { continue; } let variable = Variable { name: name.to_string(), value_type: value_type.to_string(), }; definitions.push(Definition::Variable(variable)); } _ => {} } } } for (_, def) in class_def_map { let class_def = def.into_inner(); if language == "rust" { if let Some(visibility_modifier) = &class_def.visibility_modifier { if visibility_modifier.contains("pub") { definitions.push(Definition::Class(class_def)); } } } else { definitions.push(Definition::Class(class_def)); } } for (_, def) in enum_def_map { definitions.push(Definition::Enum(def.into_inner())); } for (_, def) in union_def_map { definitions.push(Definition::Union(def.into_inner())); } Ok(definitions) } fn stringify_function(func: &Func) -> String { let mut res = format!("func {}", func.name); if func.params.is_empty() { res = format!("{res}()"); } else { res = format!("{res}{}", func.params); } if !func.return_type.is_empty() { res = format!("{res} -> {}", func.return_type); } if let Some(modifier) = &func.accessibility_modifier { res = format!("{modifier} {res}"); } format!("{res};") } fn stringify_variable(variable: &Variable) -> String { let mut res = format!("var {}", variable.name); if !variable.value_type.is_empty() { res = format!("{res}:{}", variable.value_type); } format!("{res};") } fn stringify_enum_item(item: &Variable) -> String { let mut res = item.name.clone(); if !item.value_type.is_empty() { res = format!("{res}:{}", item.value_type); } format!("{res};") } fn stringify_union_item(item: &Variable) -> String { let mut res = item.name.clone(); if !item.value_type.is_empty() { res = format!("{res}:{}", item.value_type); } format!("{res};") } fn stringify_class(class: &Class) -> String { let mut res = format!("{} {}{{", class.type_name, class.name); for method in &class.methods { let method_str = stringify_function(method); res = format!("{res}{method_str}"); } for property in &class.properties { let property_str = stringify_variable(property); res = format!("{res}{property_str}"); } format!("{res}}};") } fn stringify_enum(enum_def: &Enum) -> String { let mut res = format!("enum {}{{", enum_def.name); for item in &enum_def.items { let item_str = stringify_enum_item(item); res = format!("{res}{item_str}"); } format!("{res}}};") } fn stringify_union(union_def: &Union) -> String { let mut res = format!("union {}{{", union_def.name); for item in &union_def.items { let item_str = stringify_union_item(item); res = format!("{res}{item_str}"); } format!("{res}}};") } fn stringify_definitions(definitions: &Vec) -> String { let mut res = String::new(); for definition in definitions { match definition { Definition::Class(class) => res = format!("{res}{}", stringify_class(class)), Definition::Module(module) => res = format!("{res}{}", stringify_class(module)), Definition::Enum(enum_def) => res = format!("{res}{}", stringify_enum(enum_def)), Definition::Union(union_def) => res = format!("{res}{}", stringify_union(union_def)), Definition::Func(func) => res = format!("{res}{}", stringify_function(func)), Definition::Variable(variable) => { let variable_str = stringify_variable(variable); res = format!("{res}{variable_str}"); } } } res } pub fn get_definitions_string(language: &str, source: &str) -> LuaResult { let definitions = extract_definitions(language, source).map_err(|e| LuaError::RuntimeError(e.to_string()))?; let stringified = stringify_definitions(&definitions); Ok(stringified) } #[mlua::lua_module] fn avante_repo_map(lua: &Lua) -> LuaResult { let exports = lua.create_table()?; exports.set( "stringify_definitions", lua.create_function(move |_, (language, source): (String, String)| { get_definitions_string(language.as_str(), source.as_str()) })?, )?; Ok(exports) } #[cfg(test)] mod tests { use super::*; #[test] fn test_rust() { let source = r#" // This is a test comment pub const TEST_CONST: u32 = 1; pub static TEST_STATIC: u32 = 2; const INNER_TEST_CONST: u32 = 3; static INNER_TEST_STATIC: u32 = 4; pub(crate) struct TestStruct { pub test_field: String, inner_test_field: String, } impl TestStruct { pub fn test_method(&self, a: u32, b: u32) -> u32 { a + b } fn inner_test_method(&self, a: u32, b: u32) -> u32 { a + b } } struct InnerTestStruct { pub test_field: String, inner_test_field: String, } impl InnerTestStruct { pub fn test_method(&self, a: u32, b: u32) -> u32 { a + b } fn inner_test_method(&self, a: u32, b: u32) -> u32 { a + b } } pub enum TestEnum { TestEnumField1, TestEnumField2, } enum InnerTestEnum { InnerTestEnumField1, InnerTestEnumField2, } pub fn test_fn(a: u32, b: u32) -> u32 { let inner_var_in_func = 1; struct InnerStructInFunc { c: u32, } a + b + c } fn inner_test_fn(a: u32, b: u32) -> u32 { a + b } "#; let definitions = extract_definitions("rust", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "var TEST_CONST:u32;var TEST_STATIC:u32;func test_fn(a: u32, b: u32) -> u32;class TestStruct{func test_method(&self, a: u32, b: u32) -> u32;var test_field:String;};"; assert_eq!(stringified, expected); } #[test] fn test_zig() { let source = r#" // This is a test comment pub const TEST_CONST: u32 = 1; pub var TEST_VAR: u32 = 2; const INNER_TEST_CONST: u32 = 3; var INNER_TEST_VAR: u32 = 4; pub const TestStruct = struct { test_field: []const u8, test_field2: u64, pub fn test_method(_: *TestStruct, a: u32, b: u32) u32 { return a + b; } fn inner_test_method(_: *TestStruct, a: u32, b: u32) u32 { return a + b; } }; const InnerTestStruct = struct { test_field: []const u8, test_field2: u64, pub fn test_method(_: *InnerTestStruct, a: u32, b: u32) u32 { return a + b; } fn inner_test_method(_: *InnerTestStruct, a: u32, b: u32) u32 { return a + b; } }; pub const TestEnum = enum { TestEnumField1, TestEnumField2, }; const InnerTestEnum = enum { InnerTestEnumField1, InnerTestEnumField2, }; pub const TestUnion = union { TestUnionField1: u32, TestUnionField2: u64, }; const InnerTestUnion = union { InnerTestUnionField1: u32, InnerTestUnionField2: u64, }; pub fn test_fn(a: u32, b: u32) u32 { const inner_var_in_func = 1; const InnerStructInFunc = struct { c: u32, }; _ = InnerStructInFunc; return a + b + inner_var_in_func; } fn inner_test_fn(a: u32, b: u32) u32 { return a + b; } "#; let definitions = extract_definitions("zig", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "var TEST_CONST:u32;var TEST_VAR:u32;func test_fn() -> void;class TestStruct{func test_method(_: *TestStruct, a: u32, b: u32) -> void;var test_field:[]const u8;var test_field2:u64;};enum TestEnum{TestEnumField1;TestEnumField2;};union TestUnion{TestUnionField1;TestUnionField2;};"; assert_eq!(stringified, expected); } #[test] fn test_go() { let source = r#" // This is a test comment package main import "fmt" const TestConst string = "test" const innerTestConst string = "test" var TestVar string var innerTestVar string type TestStruct struct { TestField string innerTestField string } func (t *TestStruct) TestMethod(a int, b int) (int, error) { var InnerVarInFunc int = 1 type InnerStructInFunc struct { C int } return a + b, nil } func (t *TestStruct) innerTestMethod(a int, b int) (int, error) { return a + b, nil } type innerTestStruct struct { innerTestField string } func (t *innerTestStruct) testMethod(a int, b int) (int, error) { return a + b, nil } func (t *innerTestStruct) innerTestMethod(a int, b int) (int, error) { return a + b, nil } func TestFunc(a int, b int) (int, error) { return a + b, nil } func innerTestFunc(a int, b int) (int, error) { return a + b, nil } "#; let definitions = extract_definitions("go", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "var TestConst:string;var TestVar:string;func TestFunc(a int, b int) -> (int, error);class TestStruct{func TestMethod(a int, b int) -> (int, error);var TestField:string;};"; assert_eq!(stringified, expected); } #[test] fn test_python() { let source = r#" # This is a test comment test_var: str = "test" class TestClass: def __init__(self, a, b): self.a = a self.b = b def test_method(self, a: int, b: int) -> int: inner_var_in_method: int = 1 return a + b def test_func(a: int, b: int) -> int: inner_var_in_func: str = "test" class InnerClassInFunc: def __init__(self, a, b): self.a = a self.b = b def test_method(self, a: int, b: int) -> int: return a + b def inne_func_in_func(a: int, b: int) -> int: return a + b return a + b "#; let definitions = extract_definitions("python", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "var test_var:str;func test_func(a: int, b: int) -> int;class TestClass{func __init__(self, a, b) -> void;func test_method(self, a: int, b: int) -> int;};"; assert_eq!(stringified, expected); } #[test] fn test_typescript() { let source = r#" // This is a test comment export const testVar: string = "test"; const innerTestVar: string = "test"; export class TestClass { a: number; b: number; constructor(a: number, b: number) { this.a = a; this.b = b; } testMethod(a: number, b: number): number { const innerConstInMethod: number = 1; function innerFuncInMethod(a: number, b: number): number { return a + b; } return a + b; } } class InnerTestClass { a: number; b: number; } export function testFunc(a: number, b: number) { const innerConstInFunc: number = 1; function innerFuncInFunc(a: number, b: number): number { return a + b; } return a + b; } export const testFunc2 = (a: number, b: number) => { return a + b; } export const testFunc3 = (a: number, b: number): number => { return a + b; } function innerTestFunc(a: number, b: number) { return a + b; } "#; let definitions = extract_definitions("typescript", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "var testVar:string;func testFunc(a: number, b: number) -> void;func testFunc2(a: number, b: number) -> void;func testFunc3(a: number, b: number) -> number;class TestClass{func constructor(a: number, b: number) -> void;func testMethod(a: number, b: number) -> number;var a:number;var b:number;};" ; assert_eq!(stringified, expected); } #[test] fn test_javascript() { let source = r#" // This is a test comment export const testVar = "test"; const innerTestVar = "test"; export class TestClass { constructor(a, b) { this.a = a; this.b = b; } testMethod(a, b) { const innerConstInMethod = 1; function innerFuncInMethod(a, b) { return a + b; } return a + b; } } class InnerTestClass { constructor(a, b) { this.a = a; this.b = b; } } export const testFunc = function(a, b) { const innerConstInFunc = 1; function innerFuncInFunc(a, b) { return a + b; } return a + b; } export const testFunc2 = (a, b) => { return a + b; } export const testFunc3 = (a, b) => a + b; function innerTestFunc(a, b) { return a + b; } "#; let definitions = extract_definitions("javascript", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "var testVar;var testFunc;func testFunc2(a, b) -> void;func testFunc3(a, b) -> void;class TestClass{func constructor(a, b) -> void;func testMethod(a, b) -> void;};"; assert_eq!(stringified, expected); } #[test] fn test_ruby() { let source = r#" # This is a test comment test_var = "test" def test_func(a, b) inner_var_in_func = "test" class InnerClassInFunc attr_accessor :a, :b def initialize(a, b) @a = a @b = b end def test_method(a, b) return a + b end end return a + b end class TestClass attr_accessor :a, :b def initialize(a, b) @a = a @b = b end def test_method(a, b) inner_var_in_method = 1 def inner_func_in_method(a, b) return a + b end return a + b end end "#; let definitions = extract_definitions("ruby", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); // FIXME: let expected = "var test_var;func test_func(a, b) -> void;class InnerClassInFunc{func initialize(a, b) -> void;func test_method(a, b) -> void;};class TestClass{func initialize(a, b) -> void;func test_method(a, b) -> void;};"; assert_eq!(stringified, expected); } #[test] fn test_ruby2() { let source = r#" # frozen_string_literal: true require('jwt') top_level_var = 1 def top_level_func inner_var_in_func = 2 end module A module B @module_var = :foo def module_method @module_var end class C < Base TEST_CONST = 1 @class_var = :bar attr_accessor :a, :b def initialize(a, b) @a = a @b = b super end def bar inner_var_in_method = 1 true end private def baz(request, params) auth_header = request.headers['Authorization'] parts = auth_header.try(:split, /\s+/) JWT.decode(parts.last) end end end end "#; let definitions = extract_definitions("ruby", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "var top_level_var;func top_level_func() -> void;module A{};module A::B{func module_method() -> void;var @module_var;};class A::B::C{func initialize(a, b) -> void;func bar() -> void;private func baz(request, params) -> void;var TEST_CONST;var @class_var;};"; assert_eq!(stringified, expected); } #[test] fn test_lua() { let source = r#" -- This is a test comment local test_var = "test" function test_func(a, b) local inner_var_in_func = 1 function inner_func_in_func(a, b) return a + b end return a + b end "#; let definitions = extract_definitions("lua", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "var test_var;func test_func(a, b) -> void;"; assert_eq!(stringified, expected); } #[test] fn test_c() { let source = r#" #include int test_var = 2; extern int extern_test_var; int TestFunc(bool b) { return b ? 42 : -1; } extern void ExternTestFunc(); struct Foo { int a; int b; }; typedef int my_int; "#; let definitions = extract_definitions("c", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "var extern int extern_test_var;:int;var extern void ExternTestFunc();:void;class Foo{var int a;:int;var int b;:int;};"; assert_eq!(stringified, expected); } #[test] fn test_cpp() { let source = r#" // This is a test comment #include namespace { constexpr int TEST_CONSTEXPR = 1; const int TEST_CONST = 1; }; // namespace int test_var = 2; int TestFunc(bool b) { return b ? 42 : -1; } template class TestClass { public: TestClass(); TestClass(T a, T b); ~TestClass(); bool operator==(const TestClass &other); T testMethod(T x, T y) { return x + y; } T c; private: void privateMethod(); T a = 0; T b; }; struct TestStruct { public: TestStruct(int a, int b); ~TestStruct(); bool operator==(const TestStruct &other); int testMethod(int x, int y) { return x + y; } static int c; private: int a = 0; int b; }; bool TestStruct::operator==(const TestStruct &other) { return true; } int TestStruct::c = 0; int testFunction(int a, int b) { return a + b; } namespace TestNamespace { class InnerClass { public: bool innerMethod(int a) const; }; bool InnerClass::innerMethod(int a) const { return doSomething(a * 2); } } // namespace TestNamespace enum TestEnum { ENUM_VALUE_1, ENUM_VALUE_2 }; "#; let definitions = extract_definitions("cpp", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{}", stringified); let expected = "var TEST_CONSTEXPR:int;var TEST_CONST:int;var test_var:int;func TestFunc(bool b) -> int;func TestStruct::operator==(const TestStruct &other) -> bool;var TestStruct::c:int;func testFunction(int a, int b) -> int;func InnerClass::innerMethod(int a) -> bool;class InnerClass{func innerMethod(int a) -> bool;};class TestClass{func TestClass() -> TestClass;func operator==(const TestClass &other) -> bool;func testMethod(T x, T y) -> T;func privateMethod() -> void;func TestClass(T a, T b) -> TestClass;var c:T;var a:T;var b:T;};class TestStruct{func TestStruct(int a, int b) -> void;func operator==(const TestStruct &other) -> bool;func testMethod(int x, int y) -> int;var c:int;var a:int;var b:int;};enum TestEnum{ENUM_VALUE_1;ENUM_VALUE_2;};"; assert_eq!(stringified, expected); } #[test] fn test_scala() { let source = r#" object Main { def main(args: Array[String]): Unit = { println("Hello, World!") } } class TestClass { val testVal: String = "test" var testVar = 42 def testMethod(a: Int, b: Int): Int = { a + b } } // braceless syntax is also supported trait TestTrait: def abstractMethod(x: Int): Int def concreteMethod(y: Int): Int = y * 2 case class TestCaseClass(name: String, age: Int) enum TestEnum { case First, Second, Third } val foo: TestClass = ??? "#; let definitions = extract_definitions("scala", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "var foo:TestClass;class Main{func main(args: Array[String]) -> Unit;};class TestCaseClass{};class TestClass{func testMethod(a: Int, b: Int) -> Int;var testVal:String;var testVar;};class TestTrait{func abstractMethod(x: Int) -> Int;func concreteMethod(y: Int) -> Int;};enum TestEnum{First;Second;Third;};"; assert_eq!(stringified, expected); } #[test] fn test_elixir() { let source = r#" defmodule TestModule do @moduledoc """ This is a test module """ @test_const "test" @other_const 123 def test_func(a, b) do a + b end defp private_func(x) do x * 2 end defmacro test_macro(expr) do quote do unquote(expr) end end end defmodule AnotherModule do def another_func() do :ok end end "#; let definitions = extract_definitions("elixir", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "module AnotherModule{func another_func();};module TestModule{func test_func(a, b);};"; assert_eq!(stringified, expected); } #[test] fn test_csharp() { let source = r#" using System; namespace TestNamespace; public class TestClass(TestDependency m) { private int PrivateTestProperty { get; set; } private int _privateTestField; public int TestProperty { get; set; } public string TestField; public TestClass() { TestProperty = 0; } public void TestMethod(int a, int b) { var innerVarInMethod = 1; return a + b; } public int TestMethod(int a, int b, int c) => a + b + c; private void PrivateMethod() { return; } public class MyInnerClass(InnerClassDependency m) {} public record MyInnerRecord(int a); } public record TestRecord(int a, int b); public enum TestEnum { Value1, Value2 } "#; let definitions = extract_definitions("csharp", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "class MyInnerClass{func MyInnerClass(InnerClassDependency m) -> MyInnerClass;};class MyInnerRecord{func MyInnerRecord(int a) -> MyInnerRecord;};class TestClass{func TestClass(TestDependency m) -> TestClass;func TestClass() -> TestClass;func TestMethod(int a, int b) -> void;func TestMethod(int a, int b, int c) -> int;var TestProperty:int;var TestField:string;};class TestRecord{func TestRecord(int a, int b) -> TestRecord;};enum TestEnum{Value1;Value2;};"; assert_eq!(stringified, expected); } #[test] fn test_swift() { let source = r#" import Foundation private var myVariable = 0 public var myPublicVariable = 0 struct MyStruct { public var myPublicVariable = 0 private var myPrivateVariable = 0 func myPublicMethod(with parameter: Int) -> { } private func myPrivateMethod(with parameter: Int) -> { } } class MyClass { public var myPublicVariable = 0 private var myPrivateVariable = 0 init(myParameter: Int, myOtherParameter: Int) { } func myPublicMethod(with parameter: Int) -> { } private func myPrivateMethod(with parameter: Int) -> { } func myMethod() { print("Hello, world!") } } "#; let definitions = extract_definitions("swift", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "var myPublicVariable;class MyClass{func init() -> void;func myPublicMethod() -> void;func myMethod() -> void;var myPublicVariable;};class MyStruct{func myPublicMethod() -> void;var myPublicVariable;};"; assert_eq!(stringified, expected); } #[test] fn test_php() { let source = r#" "#; let definitions = extract_definitions("php", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "class MyClass{func myPublicMethod($parameter) -> void;func myPrivateMethod($parameter) -> void;func myMethod() -> void;var public $myPublicVariable = 0;;var private $myPrivateVariable = 0;;};"; assert_eq!(stringified, expected); } #[test] fn test_java() { let source = r#" public class MyClass { public void myPublicMethod(String parameter) { System.out.println("Hello, world!"); } private void myPrivateMethod(String parameter) { System.out.println("Hello, world!"); } void myMethod() { System.out.println("Hello, world!"); } } "#; let definitions = extract_definitions("java", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = "class MyClass{func myPublicMethod(String parameter) -> void;func myMethod() -> void;};"; assert_eq!(stringified, expected); } #[test] fn test_unsupported_language() { let source = "print('Hello, world!')"; let definitions = extract_definitions("unknown", source).unwrap(); let stringified = stringify_definitions(&definitions); println!("{stringified}"); let expected = ""; assert_eq!(stringified, expected); } } ================================================ FILE: crates/avante-templates/Cargo.toml ================================================ [lib] crate-type = ["cdylib"] [package] name = "avante-templates" edition.workspace = true rust-version.workspace = true license.workspace = true version.workspace = true [dependencies] mlua = { workspace = true } minijinja = { workspace = true } serde = { workspace = true, features = ["derive"] } [lints] workspace = true [features] lua51 = ["mlua/lua51"] lua52 = ["mlua/lua52"] lua53 = ["mlua/lua53"] lua54 = ["mlua/lua54"] luajit = ["mlua/luajit"] ================================================ FILE: crates/avante-templates/src/lib.rs ================================================ use minijinja::{context, Environment}; use mlua::prelude::*; use serde::{Deserialize, Serialize}; use std::path::Path; use std::sync::{Arc, Mutex}; struct State<'a> { environment: Mutex>>, } impl State<'_> { fn new() -> Self { State { environment: Mutex::new(None), } } } #[derive(Debug, Serialize, Deserialize)] struct SelectedCode { path: String, content: Option, file_type: String, } #[derive(Debug, Serialize, Deserialize)] struct SelectedFile { path: String, content: Option, file_type: String, } #[derive(Debug, Serialize, Deserialize)] struct TemplateContext { ask: bool, code_lang: String, selected_files: Option>, selected_code: Option, recently_viewed_files: Option>, relevant_files: Option>, project_context: Option, diagnostics: Option, system_info: Option, model_name: Option, memory: Option, todos: Option, enable_fastapply: Option, use_react_prompt: Option, } // Given the file name registered after add, the context table in Lua, resulted in a formatted // Lua string #[allow(clippy::needless_pass_by_value)] fn render(state: &State, template: &str, context: TemplateContext) -> LuaResult { let environment = state.environment.lock().unwrap(); match environment.as_ref() { Some(environment) => { let jinja_template = environment .get_template(template) .map_err(LuaError::external) .unwrap(); Ok(jinja_template .render(context! { ask => context.ask, code_lang => context.code_lang, selected_files => context.selected_files, selected_code => context.selected_code, recently_viewed_files => context.recently_viewed_files, relevant_files => context.relevant_files, project_context => context.project_context, diagnostics => context.diagnostics, system_info => context.system_info, model_name => context.model_name, memory => context.memory, todos => context.todos, enable_fastapply => context.enable_fastapply, use_react_prompt => context.use_react_prompt, }) .map_err(LuaError::external) .unwrap()) } None => Err(LuaError::RuntimeError( "Environment not initialized".to_string(), )), } } fn initialize(state: &State, cache_directory: String, project_directory: String) { let mut environment_mutex = state.environment.lock().unwrap(); let mut env = Environment::new(); // Create a custom loader that searches both cache and project directories let cache_dir = cache_directory.clone(); let project_dir = project_directory.clone(); env.set_loader( move |name: &str| -> Result, minijinja::Error> { // First try the cache directory (for built-in templates) let cache_path = Path::new(&cache_dir).join(name); if cache_path.exists() { match std::fs::read_to_string(&cache_path) { Ok(content) => return Ok(Some(content)), Err(_) => {} // Continue to try project directory } } // Then try the project directory (for custom includes) let project_path = Path::new(&project_dir).join(name); if project_path.exists() { match std::fs::read_to_string(&project_path) { Ok(content) => return Ok(Some(content)), Err(_) => {} // File not found or read error } } // Template not found in either directory Ok(None) }, ); *environment_mutex = Some(env); } #[mlua::lua_module] fn avante_templates(lua: &Lua) -> LuaResult { let core = State::new(); let state = Arc::new(core); let state_clone = Arc::clone(&state); let exports = lua.create_table()?; exports.set( "initialize", lua.create_function( move |_, (cache_directory, project_directory): (String, String)| { initialize(&state, cache_directory, project_directory); Ok(()) }, )?, )?; exports.set( "render", lua.create_function_mut(move |lua, (template, context): (String, LuaValue)| { let ctx = lua.from_value(context)?; render(&state_clone, template.as_str(), ctx) })?, )?; Ok(exports) } ================================================ FILE: crates/avante-tokenizers/Cargo.toml ================================================ [lib] crate-type = ["cdylib"] [package] name = "avante-tokenizers" edition = { workspace = true } version = { workspace = true } rust-version = { workspace = true } license = { workspace = true } [lints] workspace = true [dependencies] dirs = "5.0.1" regex = "1.11.1" hf-hub = { git = "https://github.com/yetone/hf-hub", branch='main', features = ["default", "ureq"] } ureq = { version = "2.10.1", features = ["json", "socks-proxy"] } mlua = { workspace = true } tiktoken-rs = { workspace = true } tokenizers = { workspace = true } [features] lua51 = ["mlua/lua51"] lua52 = ["mlua/lua52"] lua53 = ["mlua/lua53"] lua54 = ["mlua/lua54"] luajit = ["mlua/luajit"] ================================================ FILE: crates/avante-tokenizers/README.md ================================================ A simple crate to unify hf/tokenizers and tiktoken-rs ================================================ FILE: crates/avante-tokenizers/src/lib.rs ================================================ use hf_hub::{api::sync::ApiBuilder, Repo, RepoType}; use mlua::prelude::*; use regex::Regex; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use tiktoken_rs::{get_bpe_from_model, CoreBPE}; use tokenizers::Tokenizer; struct Tiktoken { bpe: CoreBPE, } impl Tiktoken { fn new(model: &str) -> Self { let bpe = get_bpe_from_model(model).unwrap(); Self { bpe } } fn encode(&self, text: &str) -> (Vec, usize, usize) { let tokens = self.bpe.encode_with_special_tokens(text); let num_tokens = tokens.len(); let num_chars = text.chars().count(); (tokens, num_tokens, num_chars) } } struct HuggingFaceTokenizer { tokenizer: Tokenizer, } fn is_valid_url(url: &str) -> bool { let url_regex = Regex::new(r"^https?://[^\s/$.?#].[^\s]*$").unwrap(); url_regex.is_match(url) } impl HuggingFaceTokenizer { fn new(model: &str) -> Self { let tokenizer_path = if is_valid_url(model) { Self::get_cached_tokenizer(model) } else { // Use existing HuggingFace Hub logic for model names let identifier = model.to_string(); let api = ApiBuilder::new().with_progress(false).build().unwrap(); let repo = Repo::new(identifier, RepoType::Model); let api = api.repo(repo); api.get("tokenizer.json").unwrap() }; let tokenizer = Tokenizer::from_file(tokenizer_path).unwrap(); Self { tokenizer } } fn encode(&self, text: &str) -> (Vec, usize, usize) { let encoding = self.tokenizer.encode(text, false).unwrap(); let tokens = encoding.get_ids().to_vec(); let num_tokens = tokens.len(); let num_chars = encoding.get_offsets().last().unwrap().1; (tokens, num_tokens, num_chars) } fn get_cached_tokenizer(url: &str) -> PathBuf { let cache_dir = dirs::home_dir() .map(|h| h.join(".cache").join("avante")) .unwrap(); std::fs::create_dir_all(&cache_dir).unwrap(); // Extract filename from URL let filename = url.split('/').last().unwrap(); let cached_path = cache_dir.join(filename); if !cached_path.exists() { let response = ureq::get(url).call().unwrap(); let mut file = std::fs::File::create(&cached_path).unwrap(); let mut reader = response.into_reader(); std::io::copy(&mut reader, &mut file).unwrap(); } cached_path } } enum TokenizerType { Tiktoken(Tiktoken), HuggingFace(Box), } struct State { tokenizer: Mutex>, } impl State { fn new() -> Self { State { tokenizer: Mutex::new(None), } } } fn encode(state: &State, text: &str) -> LuaResult<(Vec, usize, usize)> { let tokenizer = state.tokenizer.lock().unwrap(); match tokenizer.as_ref() { Some(TokenizerType::Tiktoken(tokenizer)) => Ok(tokenizer.encode(text)), Some(TokenizerType::HuggingFace(tokenizer)) => Ok(tokenizer.encode(text)), None => Err(LuaError::RuntimeError( "Tokenizer not initialized".to_string(), )), } } fn from_pretrained(state: &State, model: &str) { let mut tokenizer_mutex = state.tokenizer.lock().unwrap(); *tokenizer_mutex = Some(match model { "gpt-4o" => TokenizerType::Tiktoken(Tiktoken::new(model)), _ => TokenizerType::HuggingFace(Box::new(HuggingFaceTokenizer::new(model))), }); } #[mlua::lua_module] fn avante_tokenizers(lua: &Lua) -> LuaResult { let core = State::new(); let state = Arc::new(core); let state_clone = Arc::clone(&state); let exports = lua.create_table()?; exports.set( "from_pretrained", lua.create_function(move |_, model: String| { from_pretrained(&state, model.as_str()); Ok(()) })?, )?; exports.set( "encode", lua.create_function(move |_, text: String| encode(&state_clone, text.as_str()))?, )?; Ok(exports) } #[cfg(test)] mod tests { use super::*; #[test] fn test_tiktoken() { let model = "gpt-4o"; let source = "Hello, world!"; let tokenizer = Tiktoken::new(model); let (tokens, num_tokens, num_chars) = tokenizer.encode(source); assert_eq!(tokens, vec![13225, 11, 2375, 0]); assert_eq!(num_tokens, 4); assert_eq!(num_chars, source.chars().count()); } #[test] fn test_hf() { let model = "gpt2"; let source = "Hello, world!"; let tokenizer = HuggingFaceTokenizer::new(model); let (tokens, num_tokens, num_chars) = tokenizer.encode(source); assert_eq!(tokens, vec![15496, 11, 995, 0]); assert_eq!(num_tokens, 4); assert_eq!(num_chars, source.chars().count()); } #[test] fn test_roundtrip() { let state = State::new(); let source = "Hello, world!"; let model = "gpt2"; from_pretrained(&state, model); let (tokens, num_tokens, num_chars) = encode(&state, "Hello, world!").unwrap(); assert_eq!(tokens, vec![15496, 11, 995, 0]); assert_eq!(num_tokens, 4); assert_eq!(num_chars, source.chars().count()); } // For example: https://storage.googleapis.com/cohere-public/tokenizers/command-r-08-2024.json // Disable testing on GitHub Actions to avoid rate limiting and file size limits #[test] fn test_public_url() { if std::env::var("GITHUB_ACTIONS").is_ok() { return; } let state = State::new(); let source = "Hello, world!"; let model = "https://storage.googleapis.com/cohere-public/tokenizers/command-r-08-2024.json"; from_pretrained(&state, model); let (tokens, num_tokens, num_chars) = encode(&state, "Hello, world!").unwrap(); assert_eq!(tokens, vec![28339, 19, 3845, 8]); assert_eq!(num_tokens, 4); assert_eq!(num_chars, source.chars().count()); } } ================================================ FILE: lua/avante/api.lua ================================================ local Config = require("avante.config") local Utils = require("avante.utils") local PromptInput = require("avante.ui.prompt_input") ---@class avante.ApiToggle ---@operator call(): boolean ---@field debug ToggleBind.wrap ---@field hint ToggleBind.wrap ---@class avante.Api ---@field toggle avante.ApiToggle local M = {} ---@param target_provider avante.SelectorProvider function M.switch_selector_provider(target_provider) require("avante.config").override({ selector = { provider = target_provider, }, }) end ---@param target_provider avante.InputProvider function M.switch_input_provider(target_provider) require("avante.config").override({ input = { provider = target_provider, }, }) end ---@param target avante.ProviderName function M.switch_provider(target) require("avante.providers").refresh(target) end ---@param path string local function to_windows_path(path) local winpath = path:gsub("/", "\\") if winpath:match("^%a:") then winpath = winpath:sub(1, 2):upper() .. winpath:sub(3) end winpath = winpath:gsub("\\$", "") return winpath end ---@param opts? {source: boolean} function M.build(opts) opts = opts or { source = true } local dirname = Utils.trim(string.sub(debug.getinfo(1).source, 2, #"/init.lua" * -1), { suffix = "/" }) local git_root = vim.fs.find(".git", { path = dirname, upward = true })[1] local build_directory = git_root and vim.fn.fnamemodify(git_root, ":h") or (dirname .. "/../../") if opts.source and not vim.fn.executable("cargo") then error("Building avante.nvim requires cargo to be installed.", 2) end ---@type string[] local cmd local os_name = Utils.get_os_name() if vim.tbl_contains({ "linux", "darwin" }, os_name) then cmd = { "sh", "-c", string.format("make BUILD_FROM_SOURCE=%s -C %s", opts.source == true and "true" or "false", build_directory), } elseif os_name == "windows" then build_directory = to_windows_path(build_directory) cmd = { "powershell", "-ExecutionPolicy", "Bypass", "-File", string.format("%s\\Build.ps1", build_directory), "-WorkingDirectory", build_directory, "-BuildFromSource", string.format("%s", opts.source == true and "true" or "false"), } else error("Unsupported operating system: " .. os_name, 2) end ---@type integer local pid local exit_code = { 0 } local ok, job_or_err = pcall(vim.system, cmd, { text = true }, function(obj) local stderr = obj.stderr and vim.split(obj.stderr, "\n") or {} local stdout = obj.stdout and vim.split(obj.stdout, "\n") or {} if vim.tbl_contains(exit_code, obj.code) then local output = stdout if #output == 0 then table.insert(output, "") Utils.debug("build output:", output) else Utils.debug("build error:", stderr) end end end) if not ok then Utils.error("Failed to build the command: " .. cmd .. "\n" .. job_or_err, { once = true }) end pid = job_or_err.pid return pid end ---@class AskOptions ---@field question? string optional questions ---@field win? table windows options similar to |nvim_open_win()| ---@field ask? boolean ---@field floating? boolean whether to open a floating input to enter the question ---@field new_chat? boolean whether to open a new chat ---@field without_selection? boolean whether to open a new chat without selection ---@field sidebar_pre_render? fun(sidebar: avante.Sidebar) ---@field sidebar_post_render? fun(sidebar: avante.Sidebar) ---@field project_root? string optional project root ---@field show_logo? boolean whether to show the logo function M.full_view_ask() M.ask({ show_logo = true, sidebar_post_render = function(sidebar) sidebar:toggle_code_window() -- vim.wo[sidebar.containers.result.winid].number = true -- vim.wo[sidebar.containers.result.winid].relativenumber = true end, }) end M.zen_mode = M.full_view_ask ---@param opts? AskOptions function M.ask(opts) opts = opts or {} Config.ask_opts = opts if type(opts) == "string" then Utils.warn("passing 'ask' as string is deprecated, do {question = '...'} instead", { once = true }) opts = { question = opts } end local has_question = opts.question ~= nil and opts.question ~= "" local new_chat = opts.new_chat == true if Utils.is_sidebar_buffer(0) and not has_question and not new_chat then require("avante").close_sidebar() return false end opts = vim.tbl_extend("force", { selection = Utils.get_visual_selection_and_range() }, opts) ---@param input string | nil local function ask(input) if input == nil or input == "" then input = opts.question end local sidebar = require("avante").get() if sidebar and sidebar:is_open() and sidebar.code.bufnr ~= vim.api.nvim_get_current_buf() then sidebar:close({ goto_code_win = false }) end require("avante").open_sidebar(opts) sidebar = require("avante").get() if new_chat then sidebar:new_chat() end if opts.without_selection then sidebar.code.selection = nil sidebar.file_selector:reset() if sidebar.containers.selected_files then sidebar.containers.selected_files:unmount() end end if input == nil or input == "" then return true end vim.api.nvim_exec_autocmds("User", { pattern = "AvanteInputSubmitted", data = { request = input } }) return true end if opts.floating == true or (Config.windows.ask.floating == true and not has_question and opts.floating == nil) then local prompt_input = PromptInput:new({ submit_callback = function(input) ask(input) end, close_on_submit = true, win_opts = { border = Config.windows.ask.border, title = { { "Avante Ask", "FloatTitle" } }, }, start_insert = Config.windows.ask.start_insert, default_value = opts.question, }) prompt_input:open() return true end return ask() end ---@param request? string ---@param line1? integer ---@param line2? integer function M.edit(request, line1, line2) local _, selection = require("avante").get() if not selection then require("avante")._init(vim.api.nvim_get_current_tabpage()) end _, selection = require("avante").get() if not selection then return end selection:create_editing_input(request, line1, line2) if request ~= nil and request ~= "" then vim.api.nvim_exec_autocmds("User", { pattern = "AvanteEditSubmitted", data = { request = request } }) end end ---@return avante.Suggestion | nil function M.get_suggestion() local _, _, suggestion = require("avante").get() return suggestion end ---@param opts? AskOptions function M.refresh(opts) opts = opts or {} local sidebar = require("avante").get() if not sidebar then return end if not sidebar:is_open() then return end local curbuf = vim.api.nvim_get_current_buf() local focused = sidebar.containers.result.bufnr == curbuf or sidebar.containers.input.bufnr == curbuf if focused or not sidebar:is_open() then return end local listed = vim.api.nvim_get_option_value("buflisted", { buf = curbuf }) if Utils.is_sidebar_buffer(curbuf) or not listed then return end local curwin = vim.api.nvim_get_current_win() sidebar:close() sidebar.code.winid = curwin sidebar.code.bufnr = curbuf sidebar:render(opts) end ---@param opts? AskOptions function M.focus(opts) opts = opts or {} local sidebar = require("avante").get() if not sidebar then return end local curbuf = vim.api.nvim_get_current_buf() local curwin = vim.api.nvim_get_current_win() if sidebar:is_open() then if curbuf == sidebar.containers.input.bufnr then if sidebar.code.winid and sidebar.code.winid ~= curwin then vim.api.nvim_set_current_win(sidebar.code.winid) end elseif curbuf == sidebar.containers.result.bufnr then if sidebar.code.winid and sidebar.code.winid ~= curwin then vim.api.nvim_set_current_win(sidebar.code.winid) end else if sidebar.containers.input.winid and sidebar.containers.input.winid ~= curwin then vim.api.nvim_set_current_win(sidebar.containers.input.winid) end end else if sidebar.code.winid then vim.api.nvim_set_current_win(sidebar.code.winid) end ---@cast opts SidebarOpenOptions sidebar:open(opts) if sidebar.containers.input.winid then vim.api.nvim_set_current_win(sidebar.containers.input.winid) end end end function M.select_model() require("avante.model_selector").open() end function M.select_history() local buf = vim.api.nvim_get_current_buf() require("avante.history_selector").open(buf, function(filename) vim.api.nvim_buf_call(buf, function() if not require("avante").is_sidebar_open() then require("avante").open_sidebar({}) end local Path = require("avante.path") Path.history.save_latest_filename(buf, filename) local sidebar = require("avante").get() sidebar:update_content_with_history() sidebar:create_todos_container() sidebar:initialize_token_count() vim.schedule(function() sidebar:focus_input() end) end) end) end function M.add_buffer_files() local sidebar = require("avante").get() if not sidebar then require("avante.api").ask() sidebar = require("avante").get() end if not sidebar:is_open() then sidebar:open({}) end sidebar.file_selector:add_buffer_files() end function M.add_selected_file(filepath) local rel_path = Utils.uniform_path(filepath) local sidebar = require("avante").get() if not sidebar then require("avante.api").ask() sidebar = require("avante").get() end if not sidebar:is_open() then sidebar:open({}) end sidebar.file_selector:add_selected_file(rel_path) end function M.remove_selected_file(filepath) ---@diagnostic disable-next-line: undefined-field local stat = vim.uv.fs_stat(filepath) local files if stat and stat.type == "directory" then files = Utils.scan_directory({ directory = filepath, add_dirs = true }) else files = { filepath } end local sidebar = require("avante").get() if not sidebar then require("avante.api").ask() sidebar = require("avante").get() end if not sidebar:is_open() then sidebar:open({}) end for _, file in ipairs(files) do local rel_path = Utils.uniform_path(file) sidebar.file_selector:remove_selected_file(rel_path) end end function M.stop() require("avante.llm").cancel_inflight_request() end return setmetatable(M, { __index = function(t, k) local module = require("avante") ---@class AvailableApi: ApiCaller ---@field api? boolean local has = module[k] if type(has) ~= "table" or not has.api then Utils.warn(k .. " is not a valid avante's API method", { once = true }) return end t[k] = has return t[k] end, }) --[[@as avante.Api]] ================================================ FILE: lua/avante/auth/pkce.lua ================================================ local M = {} local uv = vim.uv -- Generates sha256 bytes with vim.fn local function sha256_bytes(data) -- vim.fn.sha256 returns hex string (64 chars) local hex = vim.fn.sha256(data) -- vim.text.hexdecode returns raw bytes return vim.text.hexdecode(hex) end local function windows_random_bytes(n) local ps = [[ $bytes = New-Object byte[] (]] .. n .. [[); [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes); [Convert]::ToBase64String($bytes) ]] local result = vim.system({ "powershell", "-NoProfile", "-Command", ps }):wait() if result.code ~= 0 then return nil, result.stderr end local decoded = vim.base64.decode(vim.trim(result.stdout)) if not decoded or #decoded ~= n then return nil, "failed to decode bytes" end return decoded, nil end -- Reads random bytes from urandom local function random_bytes_urandom(n) local path = "/dev/urandom" local fd, open_err = uv.fs_open(path, "r", 438) -- 0666; ignored on most systems for read if not fd then return nil, ("uv.fs_open(%s) failed: %s"):format(path, tostring(open_err)) end local chunk, read_err = uv.fs_read(fd, n, 0) uv.fs_close(fd) if not chunk then return nil, ("uv.fs_read(%s) failed: %s"):format(path, tostring(read_err)) end if #chunk ~= n then return nil, ("short read from %s: wanted %d got %d"):format(path, n, #chunk) end return chunk, nil end ---Generates a random N number of bytes using crypto lib over ffi, falling back to urandom ---@param n integer number of bytes to generate ---@return string|nil bytes string of bytes generated, or nil if all methods fail ---@return string|nil error error message if generation failed local function get_random_bytes(n) if type(uv.random) == "function" then local ok, err_or_bytes, maybe_bytes = pcall(uv.random, n) if ok then if type(err_or_bytes) == "string" and maybe_bytes == nil then if #err_or_bytes == n then return err_or_bytes end else local err = err_or_bytes local bytes = maybe_bytes if err == 0 and type(bytes) == "string" and #bytes == n then return bytes end end end end -- Fallback if vim.uv.os_uname().sysname ~= "Windows_NT" then local bytes, err = random_bytes_urandom(n) if err ~= nil or #bytes ~= n then return nil, err or "Failed to generate random bytes using urandom" else return bytes, nil end else local bytes, err = windows_random_bytes(n) if err ~= nil or #bytes ~= n then return nil, err or "Failed to generate random bytes using powershell" else return bytes, nil end end end --- URL-safe base64 --- @param data string value to base64 encode --- @return string base64String base64 encoded string local function base64url_encode(data) local b64 = vim.base64.encode(data) local b64_string, _ = b64:gsub("+", "-"):gsub("/", "_"):gsub("=", "") return b64_string end -- Generate code_verifier (43-128 characters) --- @return string|nil verifier String representing pkce verifier or nil if generation fails --- @return string|nil error error message if generation failed function M.generate_verifier() local bytes, err = get_random_bytes(32) -- 256 bits if bytes then return base64url_encode(bytes), nil end return nil, err or "Failed to generate random bytes" end -- Generate code_challenge (S256 method) ---@return string|nil challenge String representing pkce challenge or nil if generation fails ---@return string|nil error error message if generation failed function M.generate_challenge(verifier) return base64url_encode(sha256_bytes(verifier)), nil end return M ================================================ FILE: lua/avante/clipboard.lua ================================================ ---NOTE: this module is inspired by https://github.com/HakonHarnes/img-clip.nvim/tree/main ---@see https://github.com/ekickx/clipboard-image.nvim/blob/main/lua/clipboard-image/paste.lua local Path = require("plenary.path") local Utils = require("avante.utils") local Config = require("avante.config") ---@module "img-clip" local ImgClip = nil ---@class AvanteClipboard ---@field get_base64_content fun(filepath: string): string | nil --- ---@class avante.Clipboard: AvanteClipboard local M = {} ---@type Path local paste_directory = nil ---@return Path local function get_paste_directory() if paste_directory then return paste_directory end paste_directory = Path:new(Config.history.storage_path):joinpath("pasted_images") return paste_directory end M.support_paste_image = Config.support_paste_image function M.setup() get_paste_directory() if not paste_directory:exists() then paste_directory:mkdir({ parent = true }) end if M.support_paste_image() and ImgClip == nil then ImgClip = require("img-clip") end end ---@param line? string function M.paste_image(line) line = line or nil if not Config.support_paste_image() then return false end local opts = { dir_path = paste_directory:absolute(), prompt_for_file_name = false, filetypes = { AvanteInput = Config.img_paste, }, } if vim.fn.has("wsl") > 0 or vim.fn.has("win32") > 0 then opts.use_absolute_path = true end ---@diagnostic disable-next-line: need-check-nil, undefined-field return ImgClip.paste_image(opts, line) end ---@param filepath string function M.get_base64_content(filepath) local os_mapping = Utils.get_os_name() ---@type vim.SystemCompleted local output if os_mapping == "darwin" or os_mapping == "linux" then output = Utils.shell_run(("cat %s | base64 | tr -d '\n'"):format(filepath)) else output = Utils.shell_run(("([Convert]::ToBase64String([IO.File]::ReadAllBytes('%s')) -replace '`r`n')"):format(filepath)) end if output.code == 0 then return output.stdout else error("Failed to convert image to base64") end end return M ================================================ FILE: lua/avante/config.lua ================================================ ---NOTE: user will be merged with defaults and ---we add a default var_accessor for this table to config values. ---@alias WebSearchEngineProviderResponseBodyFormatter fun(body: table): (string, string?) ---@alias avante.InputProvider "native" | "dressing" | "snacks" | fun(input: avante.ui.Input): nil local Utils = require("avante.utils") local function copilot_use_response_api(opts) local model = opts and opts.model return type(model) == "string" and model:match("gpt%-%d+%.?%d*%-codex") ~= nil end ---@class avante.file_selector.IParams ---@field public title string ---@field public filepaths string[] ---@field public handler fun(filepaths: string[]|nil): nil ---@class avante.file_selector.opts.IGetFilepathsParams ---@field public cwd string ---@field public selected_filepaths string[] ---@class avante.CoreConfig: avante.Config local M = {} --- Default configuration for project-specific instruction file M.instructions_file = "avante.md" ---@class avante.Config M._defaults = { debug = false, ---@alias avante.Mode "agentic" | "legacy" ---@type avante.Mode mode = "agentic", ---@alias avante.ProviderName "claude" | "openai" | "azure" | "gemini" | "vertex" | "cohere" | "copilot" | "bedrock" | "ollama" | "watsonx_code_assistant" | "mistral" | string ---@type avante.ProviderName provider = "claude", -- WARNING: Since auto-suggestions are a high-frequency operation and therefore expensive, -- currently designating it as `copilot` provider is dangerous because: https://github.com/yetone/avante.nvim/issues/1048 -- Of course, you can reduce the request frequency by increasing `suggestion.debounce`. auto_suggestions_provider = nil, memory_summary_provider = nil, ---@alias Tokenizer "tiktoken" | "hf" ---@type Tokenizer -- Used for counting tokens and encoding text. -- By default, we will use tiktoken. -- For most providers that we support we will determine this automatically. -- If you wish to use a given implementation, then you can override it here. tokenizer = "tiktoken", ---@type string | fun(): string | nil system_prompt = nil, ---@type string | fun(): string | nil override_prompt_dir = nil, rules = { project_dir = nil, ---@type string | nil (could be relative dirpath) global_dir = nil, ---@type string | nil (absolute dirpath) }, rag_service = { -- RAG service configuration enabled = false, -- Enables the RAG service host_mount = os.getenv("HOME"), -- Host mount path for the RAG service (Docker will mount this path) runner = "docker", -- The runner for the RAG service (can use docker or nix) -- The image to use to run the rag service if runner is docker image = "quay.io/yetoneful/avante-rag-service:0.0.11", llm = { -- Configuration for the Language Model (LLM) used by the RAG service provider = "openai", -- The LLM provider endpoint = "https://api.openai.com/v1", -- The LLM API endpoint api_key = "OPENAI_API_KEY", -- The environment variable name for the LLM API key model = "gpt-4o-mini", -- The LLM model name extra = nil, -- Extra configuration options for the LLM }, embed = { -- Configuration for the Embedding model used by the RAG service provider = "openai", -- The embedding provider endpoint = "https://api.openai.com/v1", -- The embedding API endpoint api_key = "OPENAI_API_KEY", -- The environment variable name for the embedding API key model = "text-embedding-3-large", -- The embedding model name extra = nil, -- Extra configuration options for the embedding model }, docker_extra_args = "", -- Extra arguments to pass to the docker command }, web_search_engine = { provider = "tavily", proxy = nil, providers = { tavily = { api_key_name = "TAVILY_API_KEY", extra_request_body = { include_answer = "basic", }, ---@type WebSearchEngineProviderResponseBodyFormatter format_response_body = function(body) return body.answer, nil end, }, serpapi = { api_key_name = "SERPAPI_API_KEY", extra_request_body = { engine = "google", google_domain = "google.com", }, ---@type WebSearchEngineProviderResponseBodyFormatter format_response_body = function(body) if body.answer_box ~= nil and body.answer_box.result ~= nil then return body.answer_box.result, nil end if body.organic_results ~= nil then local jsn = vim .iter(body.organic_results) :map( function(result) return { title = result.title, link = result.link, snippet = result.snippet, date = result.date, } end ) :take(10) :totable() return vim.json.encode(jsn), nil end return "", nil end, }, searchapi = { api_key_name = "SEARCHAPI_API_KEY", extra_request_body = { engine = "google", }, ---@type WebSearchEngineProviderResponseBodyFormatter format_response_body = function(body) if body.answer_box ~= nil then return body.answer_box.result, nil end if body.organic_results ~= nil then local jsn = vim .iter(body.organic_results) :map( function(result) return { title = result.title, link = result.link, snippet = result.snippet, date = result.date, } end ) :take(10) :totable() return vim.json.encode(jsn), nil end return "", nil end, }, google = { api_key_name = "GOOGLE_SEARCH_API_KEY", engine_id_name = "GOOGLE_SEARCH_ENGINE_ID", extra_request_body = {}, ---@type WebSearchEngineProviderResponseBodyFormatter format_response_body = function(body) if body.items ~= nil then local jsn = vim .iter(body.items) :map( function(result) return { title = result.title, link = result.link, snippet = result.snippet, } end ) :take(10) :totable() return vim.json.encode(jsn), nil end return "", nil end, }, kagi = { api_key_name = "KAGI_API_KEY", extra_request_body = { limit = "10", }, ---@type WebSearchEngineProviderResponseBodyFormatter format_response_body = function(body) if body.data ~= nil then local jsn = vim .iter(body.data) -- search results only :filter(function(result) return result.t == 0 end) :map( function(result) return { title = result.title, url = result.url, snippet = result.snippet, } end ) :take(10) :totable() return vim.json.encode(jsn), nil end return "", nil end, }, brave = { api_key_name = "BRAVE_API_KEY", extra_request_body = { count = "10", result_filter = "web", }, format_response_body = function(body) if body.web == nil then return "", nil end local jsn = vim.iter(body.web.results):map( function(result) return { title = result.title, url = result.url, snippet = result.description, } end ) return vim.json.encode(jsn), nil end, }, searxng = { api_url_name = "SEARXNG_API_URL", extra_request_body = { format = "json", }, ---@type WebSearchEngineProviderResponseBodyFormatter format_response_body = function(body) if body.results == nil then return "", nil end local jsn = vim.iter(body.results):map( function(result) return { title = result.title, url = result.url, snippet = result.content, } end ) return vim.json.encode(jsn), nil end, }, }, }, acp_providers = { ["gemini-cli"] = { command = "gemini", args = { "--experimental-acp" }, env = { NODE_NO_WARNINGS = "1", GEMINI_API_KEY = os.getenv("GEMINI_API_KEY"), }, auth_method = "gemini-api-key", }, ["claude-code"] = { command = "npx", args = { "-y", "-g", "@zed-industries/claude-code-acp" }, env = { NODE_NO_WARNINGS = "1", ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY"), ANTHROPIC_BASE_URL = os.getenv("ANTHROPIC_BASE_URL"), ACP_PATH_TO_CLAUDE_CODE_EXECUTABLE = vim.fn.exepath("claude"), ACP_PERMISSION_MODE = "bypassPermissions", }, }, ["goose"] = { command = "goose", args = { "acp" }, }, ["codex"] = { command = "npx", args = { "-y", "-g", "@zed-industries/codex-acp" }, env = { NODE_NO_WARNINGS = "1", HOME = os.getenv("HOME"), PATH = os.getenv("PATH"), OPENAI_API_KEY = os.getenv("OPENAI_API_KEY"), }, }, ["opencode"] = { command = "opencode", args = { "acp" }, }, ["kimi-cli"] = { command = "kimi", args = { "--acp" }, }, }, ---To add support for custom provider, follow the format below ---See https://github.com/yetone/avante.nvim/wiki#custom-providers for more details ---@type {[string]: AvanteProvider} providers = { ---@type AvanteSupportedProvider openai = { endpoint = "https://api.openai.com/v1", model = "gpt-4o", timeout = 30000, -- Timeout in milliseconds, increase this for reasoning models context_window = 128000, -- Number of tokens to send to the model for context use_response_api = copilot_use_response_api, -- Automatically switch to Response API for GPT-5 Codex models support_previous_response_id = true, -- OpenAI Response API supports previous_response_id for stateful conversations -- NOTE: Response API automatically manages conversation state using previous_response_id for tool calling extra_request_body = { temperature = 0.75, max_completion_tokens = 16384, -- Increase this to include reasoning tokens (for reasoning models). For Response API, will be converted to max_output_tokens reasoning_effort = "medium", -- low|medium|high, only used for reasoning models. For Response API, this will be converted to reasoning.effort -- background = false, -- Response API only: set to true to start a background task -- NOTE: previous_response_id is automatically managed by the provider for tool calling - don't set manually }, }, ---@type AvanteSupportedProvider copilot = { endpoint = "https://api.githubcopilot.com", model = "gpt-4o-2024-11-20", proxy = nil, -- [protocol://]host[:port] Use this proxy allow_insecure = false, -- Allow insecure server connections timeout = 30000, -- Timeout in milliseconds context_window = 64000, -- Number of tokens to send to the model for context use_response_api = copilot_use_response_api, -- Automatically switch to Response API for GPT-5 Codex models support_previous_response_id = false, -- Copilot doesn't support previous_response_id, must send full history -- NOTE: Copilot doesn't support previous_response_id, always sends full conversation history including tool_calls -- NOTE: Response API doesn't support some parameters like top_p, frequency_penalty, presence_penalty extra_request_body = { -- temperature is not supported by Response API for reasoning models max_tokens = 20480, }, }, ---@type AvanteAzureProvider azure = { endpoint = "", -- example: "https://.openai.azure.com" deployment = "", -- Azure deployment name (e.g., "gpt-4o", "my-gpt-4o-deployment") api_version = "2024-12-01-preview", timeout = 30000, -- Timeout in milliseconds, increase this for reasoning models extra_request_body = { temperature = 0.75, max_completion_tokens = 16384, -- Increase this toinclude reasoning tokens (for reasoning models); but too large default value will not fit for some models (e.g. gpt-5-chat supports at most 16384 completion tokens) reasoning_effort = "medium", -- low|medium|high, only used for reasoning models }, }, ---@type AvanteAnthropicProvider claude = { endpoint = "https://api.anthropic.com", auth_type = "api", model = "claude-sonnet-4-5-20250929", timeout = 30000, -- Timeout in milliseconds context_window = 200000, extra_request_body = { temperature = 0.75, max_tokens = 64000, }, }, ---@type AvanteSupportedProvider bedrock = { model = "us.anthropic.claude-3-7-sonnet-20250219-v1:0", model_names = { "anthropic.claude-3-5-sonnet-20241022-v2:0", "us.anthropic.claude-3-7-sonnet-20250219-v1:0", "us.anthropic.claude-opus-4-20250514-v1:0", "us.anthropic.claude-opus-4-1-20250805-v1:0", "us.anthropic.claude-sonnet-4-20250514-v1:0", }, timeout = 30000, -- Timeout in milliseconds extra_request_body = { temperature = 0.75, max_tokens = 20480, }, aws_region = "", -- AWS region to use for authentication and bedrock API aws_profile = "", -- AWS profile to use for authentication, if unspecified uses default credentials chain }, ---@type AvanteSupportedProvider gemini = { endpoint = "https://generativelanguage.googleapis.com/v1beta/models", model = "gemini-2.0-flash", timeout = 30000, -- Timeout in milliseconds context_window = 1048576, use_ReAct_prompt = true, extra_request_body = { generationConfig = { temperature = 0.75, }, }, }, ---@type AvanteSupportedProvider vertex = { endpoint = "https://aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/publishers/google/models", model = "gemini-1.5-flash-002", timeout = 30000, -- Timeout in milliseconds context_window = 1048576, use_ReAct_prompt = true, extra_request_body = { generationConfig = { temperature = 0.75, }, }, }, ---@type AvanteSupportedProvider cohere = { endpoint = "https://api.cohere.com/v2", model = "command-r-plus-08-2024", timeout = 30000, -- Timeout in milliseconds extra_request_body = { temperature = 0.75, max_tokens = 20480, }, }, ---@type AvanteSupportedProvider ollama = { endpoint = "http://127.0.0.1:11434", timeout = 30000, -- Timeout in milliseconds use_ReAct_prompt = true, extra_request_body = { options = { temperature = 0.75, num_ctx = 20480, keep_alive = "5m", }, }, }, ---@type AvanteSupportedProvider watsonx_code_assistant = { endpoint = "https://api.dataplatform.cloud.ibm.com/v2/wca/core/chat/text/generation", model = "granite-8b-code-instruct", timeout = 30000, -- Timeout in milliseconds extra_request_body = { -- Additional watsonx-specific parameters can be added here }, }, ---@type AvanteSupportedProvider vertex_claude = { endpoint = "https://LOCATION-aiplatform.googleapis.com/v1/projects/PROJECT_ID/locations/LOCATION/publishers/anthropic/models", model = "claude-3-5-sonnet-v2@20241022", timeout = 30000, -- Timeout in milliseconds extra_request_body = { temperature = 0.75, max_tokens = 20480, }, }, ---@type AvanteSupportedProvider ["claude-haiku"] = { __inherited_from = "claude", model = "claude-3-5-haiku-20241022", timeout = 30000, -- Timeout in milliseconds extra_request_body = { temperature = 0.75, max_tokens = 8192, }, }, ---@type AvanteSupportedProvider ["claude-opus"] = { __inherited_from = "claude", model = "claude-3-opus-20240229", timeout = 30000, -- Timeout in milliseconds extra_request_body = { temperature = 0.75, max_tokens = 20480, }, }, ["openai-gpt-4o-mini"] = { __inherited_from = "openai", model = "gpt-4o-mini", }, aihubmix = { __inherited_from = "openai", endpoint = "https://aihubmix.com/v1", model = "gpt-4o-2024-11-20", api_key_name = "AIHUBMIX_API_KEY", }, ["aihubmix-claude"] = { __inherited_from = "claude", endpoint = "https://aihubmix.com", model = "claude-3-7-sonnet-20250219", api_key_name = "AIHUBMIX_API_KEY", }, morph = { __inherited_from = "openai", endpoint = "https://api.morphllm.com/v1", model = "auto", api_key_name = "MORPH_API_KEY", }, moonshot = { __inherited_from = "openai", endpoint = "https://api.moonshot.ai/v1", model = "kimi-k2-0711-preview", api_key_name = "MOONSHOT_API_KEY", }, xai = { __inherited_from = "openai", endpoint = "https://api.x.ai/v1", model = "grok-code-fast-1", api_key_name = "XAI_API_KEY", }, glm = { __inherited_from = "openai", endpoint = "https://open.bigmodel.cn/api/coding/paas/v4", model = "GLM-4.7", api_key_name = "GLM_API_KEY", }, qwen = { __inherited_from = "openai", endpoint = "https://dashscope.aliyuncs.com/compatible-mode/v1", model = "qwen3-coder-plus", api_key_name = "DASHSCOPE_API_KEY", }, mistral = { __inherited_from = "openai", endpoint = "https://api.mistral.ai/v1", model = "mistral-large-latest", api_key_name = "MISTRAL_API_KEY", extra_request_body = { max_tokens = 4096, -- to avoid using the unsupported max_completion_tokens }, }, }, ---Specify the special dual_boost mode ---1. enabled: Whether to enable dual_boost mode. Default to false. ---2. first_provider: The first provider to generate response. Default to "openai". ---3. second_provider: The second provider to generate response. Default to "claude". ---4. prompt: The prompt to generate response based on the two reference outputs. ---5. timeout: Timeout in milliseconds. Default to 60000. ---How it works: --- When dual_boost is enabled, avante will generate two responses from the first_provider and second_provider respectively. Then use the response from the first_provider as provider1_output and the response from the second_provider as provider2_output. Finally, avante will generate a response based on the prompt and the two reference outputs, with the default Provider as normal. ---Note: This is an experimental feature and may not work as expected. dual_boost = { enabled = false, first_provider = "openai", second_provider = "claude", prompt = "Based on the two reference outputs below, generate a response that incorporates elements from both but reflects your own judgment and unique perspective. Do not provide any explanation, just give the response directly. Reference Output 1: [{{provider1_output}}], Reference Output 2: [{{provider2_output}}]", timeout = 60000, -- Timeout in milliseconds }, ---Specify the behaviour of avante.nvim ---1. auto_focus_sidebar : Whether to automatically focus the sidebar when opening avante.nvim. Default to true. ---2. auto_suggestions = false, -- Whether to enable auto suggestions. Default to false. ---3. auto_apply_diff_after_generation: Whether to automatically apply diff after LLM response. --- This would simulate similar behaviour to cursor. Default to false. ---4. auto_set_keymaps : Whether to automatically set the keymap for the current line. Default to true. --- Note that avante will safely set these keymap. See https://github.com/yetone/avante.nvim/wiki#keymaps-and-api-i-guess for more details. ---5. auto_set_highlight_group : Whether to automatically set the highlight group for the current line. Default to true. ---6. jump_result_buffer_on_finish = false, -- Whether to automatically jump to the result buffer after generation ---7. support_paste_from_clipboard : Whether to support pasting image from clipboard. This will be determined automatically based whether img-clip is available or not. ---8. minimize_diff : Whether to remove unchanged lines when applying a code block ---9. enable_token_counting : Whether to enable token counting. Default to true. ---10. auto_add_current_file : Whether to automatically add the current file when opening a new chat. Default to true. behaviour = { auto_focus_sidebar = true, auto_suggestions = false, -- Experimental stage auto_suggestions_respect_ignore = false, auto_set_highlight_group = true, auto_set_keymaps = true, auto_apply_diff_after_generation = false, jump_result_buffer_on_finish = false, support_paste_from_clipboard = false, minimize_diff = true, enable_token_counting = true, use_cwd_as_project_root = false, auto_focus_on_diff_view = false, ---@type boolean | string[] -- true: auto-approve all tools, false: normal prompts, string[]: auto-approve specific tools by name auto_approve_tool_permissions = true, -- Default: auto-approve all tools (no prompts) auto_check_diagnostics = true, allow_access_to_git_ignored_files = false, enable_fastapply = false, include_generated_by_commit_line = false, -- Controls if 'Generated-by: ' line is added to git commit message auto_add_current_file = true, -- Whether to automatically add the current file when opening a new chat --- popup is the original yes,all,no in a floating window --- inline_buttons is the new inline buttons in the sidebar ---@type "popup" | "inline_buttons" confirmation_ui_style = "inline_buttons", --- Whether to automatically open files and navigate to lines when ACP agent makes edits ---@type boolean acp_follow_agent_locations = true, }, prompt_logger = { -- logs prompts to disk (timestamped, for replay/debugging) enabled = true, -- toggle logging entirely log_dir = vim.fn.stdpath("cache"), -- directory where logs are saved max_entries = 100, -- the uplimit of entries that can be sotred next_prompt = { normal = "", -- load the next (newer) prompt log in normal mode insert = "", }, prev_prompt = { normal = "", -- load the previous (older) prompt log in normal mode insert = "", }, }, history = { max_tokens = 4096, carried_entry_count = nil, storage_path = Utils.join_paths(vim.fn.stdpath("state"), "avante"), paste = { extension = "png", filename = "pasted-%Y-%m-%d-%H-%M-%S", }, }, highlights = { diff = { current = nil, incoming = nil, }, }, img_paste = { url_encode_path = true, template = "\nimage: $FILE_PATH\n", }, mappings = { ---@class AvanteConflictMappings diff = { ours = "co", theirs = "ct", all_theirs = "ca", both = "cb", cursor = "cc", next = "]x", prev = "[x", }, suggestion = { accept = "", next = "", prev = "", dismiss = "", }, jump = { next = "]]", prev = "[[", }, submit = { normal = "", insert = "", }, cancel = { normal = { "", "", "q" }, insert = { "" }, }, -- NOTE: The following will be safely set by avante.nvim ask = "aa", new_ask = "an", zen_mode = "az", edit = "ae", refresh = "ar", focus = "af", stop = "aS", toggle = { default = "at", debug = "ad", selection = "aC", suggestion = "as", repomap = "aR", }, sidebar = { expand_tool_use = "", next_prompt = "]p", prev_prompt = "[p", apply_all = "A", apply_cursor = "a", retry_user_request = "r", edit_user_request = "e", switch_windows = "", reverse_switch_windows = "", toggle_code_window = "x", remove_file = "d", add_file = "@", close = { "q" }, ---@alias AvanteCloseFromInput { normal: string | nil, insert: string | nil } ---@type AvanteCloseFromInput | nil close_from_input = nil, -- e.g., { normal = "", insert = "" } ---@alias AvanteToggleCodeWindowFromInput { normal: string | nil, insert: string | nil } ---@type AvanteToggleCodeWindowFromInput | nil toggle_code_window_from_input = nil, -- e.g., { normal = "x", insert = "" } }, files = { add_current = "ac", -- Add current buffer to selected files add_all_buffers = "aB", -- Add all buffer files to selected files }, select_model = "a?", -- Select model command select_history = "ah", -- Select history command confirm = { focus_window = "f", code = "c", resp = "r", input = "i", }, }, windows = { ---@alias AvantePosition "right" | "left" | "top" | "bottom" | "smart" ---@type AvantePosition position = "right", fillchars = "eob: ", wrap = true, -- similar to vim.o.wrap width = 30, -- default % based on available width in vertical layout height = 30, -- default % based on available height in horizontal layout sidebar_header = { enabled = true, -- true, false to enable/disable the header align = "center", -- left, center, right for title rounded = true, include_model = false, }, spinner = { editing = { "⡀", "⠄", "⠂", "⠁", "⠈", "⠐", "⠠", "⢀", "⣀", "⢄", "⢂", "⢁", "⢈", "⢐", "⢠", "⣠", "⢤", "⢢", "⢡", "⢨", "⢰", "⣰", "⢴", "⢲", "⢱", "⢸", "⣸", "⢼", "⢺", "⢹", "⣹", "⢽", "⢻", "⣻", "⢿", "⣿", }, generating = { "·", "✢", "✳", "∗", "✻", "✽" }, thinking = { "🤯", "🙄" }, }, input = { prefix = "> ", height = 8, -- Height of the input window in vertical layout }, selected_files = { height = 6, -- Maximum height of the selected files window }, edit = { border = { " ", " ", " ", " ", " ", " ", " ", " " }, start_insert = true, -- Start insert mode when opening the edit window }, ask = { floating = false, -- Open the 'AvanteAsk' prompt in a floating window border = { " ", " ", " ", " ", " ", " ", " ", " " }, start_insert = true, -- Start insert mode when opening the ask window ---@alias AvanteInitialDiff "ours" | "theirs" ---@type AvanteInitialDiff focus_on_apply = "ours", -- which diff to focus after applying }, }, --- @class AvanteConflictConfig diff = { autojump = true, --- Override the 'timeoutlen' setting while hovering over a diff (see :help timeoutlen). --- Helps to avoid entering operator-pending mode with diff mappings starting with `c`. --- Disable by setting to -1. override_timeoutlen = 500, }, --- Allows selecting code or other data in a buffer and ask LLM questions about it or --- to perform edits/transformations. --- @class AvanteSelectionConfig --- @field enabled boolean --- @field hint_display "delayed" | "immediate" | "none" When to show key map hints. selection = { enabled = true, hint_display = "delayed", }, --- @class AvanteRepoMapConfig repo_map = { ignore_patterns = { "%.git", "%.worktree", "__pycache__", "node_modules" }, -- ignore files matching these negate_patterns = {}, -- negate ignore files matching these. }, --- @class AvanteFileSelectorConfig file_selector = { provider = nil, -- Options override for custom providers provider_opts = {}, }, selector = { ---@alias avante.SelectorProvider "native" | "fzf_lua" | "mini_pick" | "snacks" | "telescope" | fun(selector: avante.ui.Selector): nil ---@type avante.SelectorProvider provider = "native", provider_opts = {}, exclude_auto_select = {}, -- List of items to exclude from auto selection }, input = { provider = "native", provider_opts = {}, }, suggestion = { debounce = 600, throttle = 600, }, disabled_tools = {}, ---@type string[] ---@type AvanteLLMToolPublic[] | fun(): AvanteLLMToolPublic[] custom_tools = {}, ---@type AvanteSlashCommand[] slash_commands = {}, ---@type AvanteShortcut[] shortcuts = {}, ---@type AskOptions ask_opts = {}, } ---@type avante.Config ---@diagnostic disable-next-line: missing-fields M._options = {} local function get_config_dir_path() return Utils.join_paths(vim.fn.expand("~"), ".config", "avante.nvim") end local function get_config_file_path() return Utils.join_paths(get_config_dir_path(), "config.json") end --- Function to save the last used model ---@param model_name string function M.save_last_model(model_name, provider_name) local config_dir = get_config_dir_path() local storage_path = get_config_file_path() if not Utils.path_exists(config_dir) then vim.fn.mkdir(config_dir, "p") end local Providers = require("avante.providers") local provider = Providers[provider_name] local provider_model = provider and provider.model local file = io.open(storage_path, "w") if file then file:write( vim.json.encode({ last_model = model_name, last_provider = provider_name, provider_model = provider_model }) ) file:close() end end --- Retrieves names of the last used model and provider. May remove saved config if it is deemed invalid ---@param known_providers table ---@return string|nil Model name ---@return string|nil Provider name function M.get_last_used_model(known_providers) local storage_path = get_config_file_path() local file = io.open(storage_path, "r") if file then local content = file:read("*a") file:close() if not content or content == "" then Utils.warn("Last used model file is empty: " .. storage_path) -- Remove to not have repeated warnings os.remove(storage_path) end local success, data = pcall(vim.json.decode, content) if not success or not data or not data.last_model or data.last_model == "" or data.last_provider == "" then Utils.warn("Invalid or corrupt JSON in last used model file: " .. storage_path) -- Rename instead of deleting so user can examine contents os.rename(storage_path, storage_path .. ".bad") return end if data.last_provider then local provider = known_providers[data.last_provider] if not provider then Utils.warn( "Provider " .. data.last_provider .. " is no longer a valid provider, falling back to default configuration" ) os.remove(storage_path) return end if data.provider_model and provider.model and provider.model ~= data.provider_model then return provider.model, data.last_provider end end return data.last_model, data.last_provider end end ---Applies given model and provider to the config ---@param config avante.Config ---@param model_name string ---@param provider_name? string local function apply_model_selection(config, model_name, provider_name) local provider_list = config.providers or {} local current_provider_name = config.provider if config.acp_providers[current_provider_name] then return end local target_provider_name = provider_name or current_provider_name local target_provider = provider_list[target_provider_name] if not target_provider then return end local current_provider_data = provider_list[current_provider_name] local current_model_name = current_provider_data and current_provider_data.model if target_provider_name ~= current_provider_name or model_name ~= current_model_name then config.provider = target_provider_name target_provider.model = model_name if not target_provider.model_names then target_provider.model_names = {} end for _, model_name_ in ipairs({ model_name, current_model_name }) do if not vim.tbl_contains(target_provider.model_names, model_name_) then table.insert(target_provider.model_names, model_name_) end end if not config.windows.sidebar_header.include_model then Utils.info(string.format("Using previously selected model: %s/%s", target_provider_name, model_name)) end end end ---@param opts table|nil -- Optional table parameter for configuration settings function M.setup(opts) opts = opts or {} -- Ensure `opts` is defined with a default table if vim.fn.has("nvim-0.11") == 1 then vim.validate("opts", opts, "table", true) else vim.validate({ opts = { opts, "table", true } }) end opts = opts or {} local migration_url = "https://github.com/yetone/avante.nvim/wiki/Provider-configuration-migration-guide" if opts.providers ~= nil then for k, v in pairs(opts.providers) do local extra_request_body if type(v) == "table" then if M._defaults.providers[k] ~= nil then extra_request_body = M._defaults.providers[k].extra_request_body elseif v.__inherited_from ~= nil then if M._defaults.providers[v.__inherited_from] ~= nil then extra_request_body = M._defaults.providers[v.__inherited_from].extra_request_body end end end if extra_request_body ~= nil then for k_, v_ in pairs(v) do if extra_request_body[k_] ~= nil then opts.providers[k].extra_request_body = opts.providers[k].extra_request_body or {} opts.providers[k].extra_request_body[k_] = v_ Utils.warn( string.format( "[DEPRECATED] The configuration of `providers.%s.%s` should be placed in `providers.%s.extra_request_body.%s`; for detailed migration instructions, please visit: %s", k, k_, k, k_, migration_url ), { title = "Avante" } ) end end end end end for k, v in pairs(opts) do if M._defaults.providers[k] ~= nil then opts.providers = opts.providers or {} opts.providers[k] = v Utils.warn( string.format( "[DEPRECATED] The configuration of `%s` should be placed in `providers.%s`. For detailed migration instructions, please visit: %s", k, k, migration_url ), { title = "Avante" } ) local extra_request_body = M._defaults.providers[k].extra_request_body if type(v) == "table" and extra_request_body ~= nil then for k_, v_ in pairs(v) do if extra_request_body[k_] ~= nil then opts.providers[k].extra_request_body = opts.providers[k].extra_request_body or {} opts.providers[k].extra_request_body[k_] = v_ Utils.warn( string.format( "[DEPRECATED] The configuration of `%s.%s` should be placed in `providers.%s.extra_request_body.%s`; for detailed migration instructions, please visit: %s", k, k_, k, k_, migration_url ), { title = "Avante" } ) end end end end if k == "vendors" and v ~= nil then for k2, v2 in pairs(v) do opts.providers = opts.providers or {} opts.providers[k2] = v2 Utils.warn( string.format( "[DEPRECATED] The configuration of `vendors.%s` should be placed in `providers.%s`. For detailed migration instructions, please visit: %s", k2, k2, migration_url ), { title = "Avante" } ) if type(v2) == "table" and v2.__inherited_from ~= nil and M._defaults.providers[v2.__inherited_from] ~= nil then local extra_request_body = M._defaults.providers[v2.__inherited_from].extra_request_body if extra_request_body ~= nil then for k2_, v2_ in pairs(v2) do if extra_request_body[k2_] ~= nil then opts.providers[k2].extra_request_body = opts.providers[k2].extra_request_body or {} opts.providers[k2].extra_request_body[k2_] = v2_ Utils.warn( string.format( "[DEPRECATED] The configuration of `vendors.%s.%s` should be placed in `providers.%s.extra_request_body.%s`; for detailed migration instructions, please visit: %s", k2, k2_, k2, k2_, migration_url ), { title = "Avante" } ) end end end end end end end local merged = vim.tbl_deep_extend( "force", M._defaults, opts, ---@type avante.Config { behaviour = { support_paste_from_clipboard = M.support_paste_image(), }, } ) local last_model, last_provider = M.get_last_used_model(merged.providers or {}) if last_model then apply_model_selection(merged, last_model, last_provider) end M._options = merged ---@diagnostic disable-next-line: undefined-field if M._options.disable_tools ~= nil then Utils.warn( "`disable_tools` is provider-scoped, not globally scoped. Therefore, you cannot set `disable_tools` at the top level. It should be set under a provider, for example: `openai.disable_tools = true`", { title = "Avante" } ) end if type(M._options.disabled_tools) == "boolean" then Utils.warn( '`disabled_tools` must be a list, not a boolean. Please change it to `disabled_tools = { "tool1", "tool2" }`. Note the difference between `disabled_tools` and `disable_tools`.', { title = "Avante" } ) end if vim.fn.has("nvim-0.11") == 1 then vim.validate("provider", M._options.provider, "string", false) else vim.validate({ provider = { M._options.provider, "string", false } }) end for k, v in pairs(M._options.providers) do M._options.providers[k] = type(v) == "function" and v() or v end end ---@param opts table function M.override(opts) if vim.fn.has("nvim-0.11") == 1 then vim.validate("opts", opts, "table", true) else vim.validate({ opts = { opts, "table", true } }) end M._options = vim.tbl_deep_extend("force", M._options, opts or {}) for k, v in pairs(M._options.providers) do M._options.providers[k] = type(v) == "function" and v() or v end end M = setmetatable(M, { __index = function(_, k) if M._options[k] then return M._options[k] end end, }) function M.support_paste_image() return Utils.has("img-clip.nvim") or Utils.has("img-clip") end function M.get_window_width() return math.ceil(vim.o.columns * (M.windows.width / 100)) end ---get supported providers ---@param provider_name avante.ProviderName function M.get_provider_config(provider_name) local found = false local config = {} if M.providers[provider_name] ~= nil then found = true config = vim.tbl_deep_extend("force", config, vim.deepcopy(M.providers[provider_name], true)) end if not found then error("Failed to find provider: " .. provider_name, 2) end return config end return M ================================================ FILE: lua/avante/diff.lua ================================================ local api = vim.api local Config = require("avante.config") local Utils = require("avante.utils") local Highlights = require("avante.highlights") local H = {} local M = {} -----------------------------------------------------------------------------// -- REFERENCES: -----------------------------------------------------------------------------// -- Detecting the state of a git repository based on files in the .git directory. -- https://stackoverflow.com/questions/49774200/how-to-tell-if-my-git-repo-is-in-a-conflict -- git diff commands to git a list of conflicted files -- https://stackoverflow.com/questions/3065650/whats-the-simplest-way-to-list-conflicted-files-in-git -- how to show a full path for files in a git diff command -- https://stackoverflow.com/questions/10459374/making-git-diff-stat-show-full-file-path -- Advanced merging -- https://git-scm.com/book/en/v2/Git-Tools-Advanced-Merging -----------------------------------------------------------------------------// -- Types -----------------------------------------------------------------------------// ---@alias ConflictSide "'ours'"|"'theirs'"|"'all_theirs'"|"'both'"|"'cursor'"|"'base'"|"'none'" --- @class AvanteConflictHighlights --- @field current string --- @field incoming string ---@class RangeMark ---@field label integer ---@field content string --- @class PositionMarks --- @field current RangeMark --- @field incoming RangeMark --- @class Range --- @field range_start integer --- @field range_end integer --- @field content_start integer --- @field content_end integer --- @class ConflictPosition --- @field incoming Range --- @field middle Range --- @field current Range --- @field marks PositionMarks --- @class ConflictBufferCache --- @field lines table map of conflicted line numbers --- @field positions ConflictPosition[] --- @field tick integer --- @field bufnr integer -----------------------------------------------------------------------------// -- Constants -----------------------------------------------------------------------------// ---@enum AvanteConflictSides local SIDES = { OURS = "ours", THEIRS = "theirs", ALL_THEIRS = "all_theirs", BOTH = "both", NONE = "none", CURSOR = "cursor", } -- A mapping between the internal names and the display names local name_map = { ours = "current", theirs = "incoming", both = "both", none = "none", cursor = "cursor", } local CURRENT_HL = Highlights.CURRENT local INCOMING_HL = Highlights.INCOMING local CURRENT_LABEL_HL = Highlights.CURRENT_LABEL local INCOMING_LABEL_HL = Highlights.INCOMING_LABEL local PRIORITY = (vim.hl or vim.highlight).priorities.user local NAMESPACE = api.nvim_create_namespace("avante-conflict") local KEYBINDING_NAMESPACE = api.nvim_create_namespace("avante-conflict-keybinding") local AUGROUP_NAME = "avante_conflicts" local conflict_start = "^<<<<<<<" local conflict_middle = "^=======" local conflict_end = "^>>>>>>>" -----------------------------------------------------------------------------// --- @return table local function create_visited_buffers() return setmetatable({}, { __index = function(t, k) if type(k) == "number" then return t[api.nvim_buf_get_name(k)] end end, }) end --- A list of buffers that have conflicts in them. This is derived from --- git using the diff command, and updated at intervals local visited_buffers = create_visited_buffers() -----------------------------------------------------------------------------// ---Add the positions to the buffer in our in memory buffer list ---positions are keyed by a list of range start and end for each mark ---@param buf integer ---@param positions ConflictPosition[] local function update_visited_buffers(buf, positions) if not buf or not api.nvim_buf_is_valid(buf) then return end local name = api.nvim_buf_get_name(buf) -- If this buffer is not in the list if not visited_buffers[name] then return end visited_buffers[name].bufnr = buf visited_buffers[name].tick = vim.b[buf].changedtick visited_buffers[name].positions = positions end function M.add_visited_buffer(bufnr) local name = api.nvim_buf_get_name(bufnr) visited_buffers[name] = visited_buffers[name] or {} end ---Set an extmark for each section of the git conflict ---@param bufnr integer ---@param hl string ---@param range_start integer ---@param range_end integer ---@return integer? extmark_id local function hl_range(bufnr, hl, range_start, range_end) if not range_start or not range_end then return end return api.nvim_buf_set_extmark(bufnr, NAMESPACE, range_start, 0, { hl_group = hl, hl_eol = true, hl_mode = "combine", end_row = range_end, priority = PRIORITY, }) end ---Add highlights and additional data to each section heading of the conflict marker ---These works by covering the underlying text with an extmark that contains the same information ---with some extra detail appended. ---TODO: ideally this could be done by using virtual text at the EOL and highlighting the ---background but this doesn't work and currently this is done by filling the rest of the line with ---empty space and overlaying the line content ---@param bufnr integer ---@param hl_group string ---@param label string ---@param lnum integer ---@return integer extmark id local function draw_section_label(bufnr, hl_group, label, lnum) local remaining_space = api.nvim_win_get_width(0) - api.nvim_strwidth(label) return api.nvim_buf_set_extmark(bufnr, NAMESPACE, lnum, 0, { hl_group = hl_group, virt_text = { { label .. string.rep(" ", remaining_space), hl_group } }, virt_text_pos = "overlay", priority = PRIORITY, }) end ---Highlight each part of a git conflict i.e. the incoming changes vs the current/HEAD changes ---TODO: should extmarks be ephemeral? or is it less expensive to save them and only re-apply ---them when a buffer changes since otherwise we have to reparse the whole buffer constantly ---@param bufnr integer ---@param positions table ---@param lines string[] local function highlight_conflicts(bufnr, positions, lines) M.clear(bufnr) for _, position in ipairs(positions) do local current_start = position.current.range_start local current_end = position.current.range_end local incoming_start = position.incoming.range_start local incoming_end = position.incoming.range_end -- Add one since the index access in lines is 1 based local current_label = lines[current_start + 1] .. " (Current changes)" local incoming_label = lines[incoming_end + 1] .. " (Incoming changes)" local curr_label_id = draw_section_label(bufnr, CURRENT_LABEL_HL, current_label, current_start) local curr_id = hl_range(bufnr, CURRENT_HL, current_start, current_end + 1) local inc_id = hl_range(bufnr, INCOMING_HL, incoming_start, incoming_end + 1) local inc_label_id = draw_section_label(bufnr, INCOMING_LABEL_HL, incoming_label, incoming_end) position.marks = { current = { label = curr_label_id, content = curr_id }, incoming = { label = inc_label_id, content = inc_id }, } end end ---Iterate through the buffer line by line checking there is a matching conflict marker ---when we find a starting mark we collect the position details and add it to a list of positions ---@param lines string[] ---@return boolean ---@return ConflictPosition[] local function detect_conflicts(lines) local positions = {} local position, has_middle = nil, false for index, line in ipairs(lines) do local lnum = index - 1 if line:match(conflict_start) then position = { current = { range_start = lnum, content_start = lnum + 1 }, middle = {}, incoming = {}, } end if position ~= nil and line:match(conflict_middle) then has_middle = true position.current.range_end = lnum - 1 position.current.content_end = lnum - 1 position.middle.range_start = lnum position.middle.range_end = lnum + 1 position.incoming.range_start = lnum + 1 position.incoming.content_start = lnum + 1 end if position ~= nil and has_middle and line:match(conflict_end) then position.incoming.range_end = lnum position.incoming.content_end = lnum - 1 positions[#positions + 1] = position position, has_middle = nil, false end end return #positions > 0, positions end ---Helper function to find a conflict position based on a comparator function ---@param bufnr integer ---@param comparator fun(string, integer): boolean ---@param opts table? ---@return ConflictPosition? local function find_position(bufnr, comparator, opts) local match = visited_buffers[bufnr] if not match then return end local line = Utils.get_cursor_pos() line = line - 1 -- Convert to 0-based for position comparison if opts and opts.reverse then for i = #match.positions, 1, -1 do local position = match.positions[i] if comparator(line, position) then return position end end return nil end for _, position in ipairs(match.positions) do if comparator(line, position) then return position end end return nil end ---Retrieves a conflict marker position by checking the visited buffers for a supported range ---@param bufnr integer ---@return ConflictPosition? local function get_current_position(bufnr) return find_position( bufnr, function(line, position) return position.current.range_start <= line and position.incoming.range_end >= line end ) end ---@param position ConflictPosition? ---@param side ConflictSide local function set_cursor(position, side) if not position then return end local target = side == SIDES.OURS and position.current or position.incoming api.nvim_win_set_cursor(0, { target.range_start + 1, 0 }) vim.cmd("normal! zz") end local show_keybinding_hint_extmark_id = nil local function register_cursor_move_events(bufnr) local function show_keybinding_hint(lnum) if show_keybinding_hint_extmark_id then api.nvim_buf_del_extmark(bufnr, KEYBINDING_NAMESPACE, show_keybinding_hint_extmark_id) end local hint = string.format( "[<%s>: OURS, <%s>: THEIRS, <%s>: CURSOR, <%s>: ALL THEIRS, <%s>: PREV, <%s>: NEXT]", Config.mappings.diff.ours, Config.mappings.diff.theirs, Config.mappings.diff.cursor, Config.mappings.diff.all_theirs, Config.mappings.diff.prev, Config.mappings.diff.next ) show_keybinding_hint_extmark_id = api.nvim_buf_set_extmark(bufnr, KEYBINDING_NAMESPACE, lnum - 1, -1, { hl_group = "AvanteInlineHint", virt_text = { { hint, "AvanteInlineHint" } }, virt_text_pos = "right_align", priority = PRIORITY, }) end api.nvim_create_autocmd({ "CursorMoved", "CursorMovedI", "WinLeave" }, { buffer = bufnr, callback = function(event) local position = get_current_position(bufnr) if (event.event == "CursorMoved" or event.event == "CursorMovedI") and position then show_keybinding_hint(position.current.range_start + 1) M.override_timeoutlen(bufnr) else api.nvim_buf_clear_namespace(bufnr, KEYBINDING_NAMESPACE, 0, -1) M.restore_timeoutlen(bufnr) end end, }) end ---Get the conflict marker positions for a buffer if any and update the buffers state ---@param bufnr integer ---@param range_start? integer ---@param range_end? integer local function parse_buffer(bufnr, range_start, range_end) local lines = Utils.get_buf_lines(range_start or 0, range_end or -1, bufnr) local prev_conflicts = visited_buffers[bufnr].positions ~= nil and #visited_buffers[bufnr].positions > 0 local has_conflict, positions = detect_conflicts(lines) update_visited_buffers(bufnr, positions) if has_conflict then register_cursor_move_events(bufnr) highlight_conflicts(bufnr, positions, lines) else M.clear(bufnr) end if prev_conflicts ~= has_conflict or not vim.b[bufnr].avante_conflict_mappings_set then local pattern = has_conflict and "AvanteConflictDetected" or "AvanteConflictResolved" api.nvim_exec_autocmds("User", { pattern = pattern }) end end ---Process a buffer if the changed tick has changed ---@param bufnr integer ---@param range_start integer? ---@param range_end integer? function M.process(bufnr, range_start, range_end) bufnr = bufnr if visited_buffers[bufnr] and visited_buffers[bufnr].tick == vim.b[bufnr].changedtick then return end parse_buffer(bufnr, range_start, range_end) end -----------------------------------------------------------------------------// -- Mappings -----------------------------------------------------------------------------// ---@param bufnr integer given buffer id function H.setup_buffer_mappings(bufnr) ---@param desc string local function opts(desc) return { silent = true, buffer = bufnr, desc = "avante(conflict): " .. desc } end vim.keymap.set({ "n", "v" }, Config.mappings.diff.ours, function() M.choose("ours") end, opts("choose ours")) vim.keymap.set({ "n", "v" }, Config.mappings.diff.both, function() M.choose("both") end, opts("choose both")) vim.keymap.set({ "n", "v" }, Config.mappings.diff.theirs, function() M.choose("theirs") end, opts("choose theirs")) vim.keymap.set( { "n", "v" }, Config.mappings.diff.all_theirs, function() M.choose("all_theirs") end, opts("choose all theirs") ) vim.keymap.set("n", Config.mappings.diff.cursor, function() M.choose("cursor") end, opts("choose under cursor")) vim.keymap.set("n", Config.mappings.diff.prev, function() M.find_prev("ours") end, opts("previous conflict")) vim.keymap.set("n", Config.mappings.diff.next, function() M.find_next("ours") end, opts("next conflict")) vim.b[bufnr].avante_conflict_mappings_set = true end ---@param bufnr integer function H.clear_buffer_mappings(bufnr) if not bufnr or not vim.b[bufnr].avante_conflict_mappings_set then return end for _, diff_mapping in pairs(Config.mappings.diff) do pcall(vim.api.nvim_buf_del_keymap, bufnr, "n", diff_mapping) end vim.b[bufnr].avante_conflict_mappings_set = false M.restore_timeoutlen(bufnr) end ---@param bufnr integer function M.override_timeoutlen(bufnr) if vim.b[bufnr].avante_original_timeoutlen then return end if Config.diff.override_timeoutlen > 0 then vim.b[bufnr].avante_original_timeoutlen = vim.o.timeoutlen vim.o.timeoutlen = Config.diff.override_timeoutlen end end ---@param bufnr integer function M.restore_timeoutlen(bufnr) if vim.b[bufnr].avante_original_timeoutlen then vim.o.timeoutlen = vim.b[bufnr].avante_original_timeoutlen vim.b[bufnr].avante_original_timeoutlen = nil end end M.augroup = api.nvim_create_augroup(AUGROUP_NAME, { clear = true }) function M.setup() local previous_inlay_enabled = nil api.nvim_create_autocmd("User", { group = M.augroup, pattern = "AvanteConflictDetected", callback = function(ev) vim.diagnostic.enable(false, { bufnr = ev.buf }) if vim.lsp.inlay_hint then previous_inlay_enabled = vim.lsp.inlay_hint.is_enabled({ bufnr = ev.buf }) vim.lsp.inlay_hint.enable(false, { bufnr = ev.buf }) end H.setup_buffer_mappings(ev.buf) end, }) api.nvim_create_autocmd("User", { group = M.augroup, pattern = "AvanteConflictResolved", callback = function(ev) vim.diagnostic.enable(true, { bufnr = ev.buf }) if vim.lsp.inlay_hint and previous_inlay_enabled ~= nil then vim.lsp.inlay_hint.enable(previous_inlay_enabled, { bufnr = ev.buf }) previous_inlay_enabled = nil end H.clear_buffer_mappings(ev.buf) end, }) api.nvim_set_decoration_provider(NAMESPACE, { on_win = function(_, _, bufnr, _, _) if visited_buffers[bufnr] then M.process(bufnr) end end, }) end --- Add additional metadata to a quickfix entry if we have already visited the buffer and have that --- information ---@param item table ---@param items table[] ---@param visited_buf ConflictBufferCache local function quickfix_items_from_positions(item, items, visited_buf) if vim.tbl_isempty(visited_buf.positions) then return end for _, pos in ipairs(visited_buf.positions) do for key, value in pairs(pos) do if vim.tbl_contains({ name_map.ours, name_map.theirs, name_map.base }, key) and not vim.tbl_isempty(value) then local lnum = value.range_start + 1 local next_item = vim.deepcopy(item) next_item.text = string.format("%s change", key, lnum) next_item.lnum = lnum next_item.col = 0 table.insert(items, next_item) end end end end --- Convert the conflicts detected via get conflicted files into a list of quickfix entries. ---@param callback fun(files: table) function M.conflicts_to_qf_items(callback) local items = {} for filename, visited_buf in pairs(visited_buffers) do local item = { filename = filename, pattern = conflict_start, text = "git conflict", type = "E", valid = 1, } if visited_buf and next(visited_buf) then quickfix_items_from_positions(item, items, visited_buf) else table.insert(items, item) end end callback(items) end ---@param bufnr integer? function M.clear(bufnr) if bufnr and not api.nvim_buf_is_valid(bufnr) then return end bufnr = bufnr or 0 api.nvim_buf_clear_namespace(bufnr, NAMESPACE, 0, -1) api.nvim_buf_clear_namespace(bufnr, KEYBINDING_NAMESPACE, 0, -1) end ---@param side ConflictSide function M.find_next(side) local pos = find_position( 0, function(line, position) return position.current.range_start > line and position.incoming.range_end > line end ) set_cursor(pos, side) end ---@param side ConflictSide function M.find_prev(side) local pos = find_position( 0, function(line, position) return position.current.range_start <= line and position.incoming.range_end <= line end, { reverse = true } ) set_cursor(pos, side) end ---Select the changes to keep ---@param side ConflictSide function M.choose(side) local bufnr = api.nvim_get_current_buf() if vim.fn.mode() == "v" or vim.fn.mode() == "V" or vim.fn.mode() == "" then vim.cmd("noautocmd stopinsert") -- have to defer so that the < and > marks are set vim.defer_fn(function() local start = api.nvim_buf_get_mark(0, "<")[1] local finish = api.nvim_buf_get_mark(0, ">")[1] local position = find_position(bufnr, function(_, pos) local left = pos.current.range_start >= start - 1 local right = pos.incoming.range_end <= finish + 1 return left and right end) while position ~= nil do M.process_position(bufnr, side, position, false) position = find_position(bufnr, function(_, pos) local left = pos.current.range_start >= start - 1 local right = pos.incoming.range_end <= finish + 1 return left and right end) end end, 50) if Config.diff.autojump then M.find_next(side) vim.cmd([[normal! zz]]) end return end local position = get_current_position(bufnr) if not position then return end if side == SIDES.ALL_THEIRS then ---@diagnostic disable-next-line: unused-local local pos = find_position(bufnr, function(line, pos) return true end) while pos ~= nil do M.process_position(bufnr, "theirs", pos, false) ---@diagnostic disable-next-line: unused-local pos = find_position(bufnr, function(line, pos_) return true end) end else M.process_position(bufnr, side, position, true) end end ---@param side ConflictSide ---@param position ConflictPosition ---@param enable_autojump boolean function M.process_position(bufnr, side, position, enable_autojump) local lines = {} if vim.tbl_contains({ SIDES.OURS, SIDES.THEIRS }, side) then local data = position[name_map[side]] lines = Utils.get_buf_lines(data.content_start, data.content_end + 1) elseif side == SIDES.BOTH then local first = Utils.get_buf_lines(position.current.content_start, position.current.content_end + 1) local second = Utils.get_buf_lines(position.incoming.content_start, position.incoming.content_end + 1) lines = vim.list_extend(first, second) elseif side == SIDES.NONE then lines = {} elseif side == SIDES.CURSOR then local cursor_line = Utils.get_cursor_pos() for _, pos in ipairs({ SIDES.OURS, SIDES.THEIRS }) do local data = position[name_map[pos]] or {} if data.range_start and data.range_start + 1 <= cursor_line and data.range_end + 1 >= cursor_line then side = pos lines = Utils.get_buf_lines(data.content_start, data.content_end + 1) break end end if side == SIDES.CURSOR then return end else return end local pos_start = position.current.range_start < 0 and 0 or position.current.range_start local pos_end = position.incoming.range_end + 1 api.nvim_buf_set_lines(0, pos_start, pos_end, false, lines) api.nvim_buf_del_extmark(0, NAMESPACE, position.marks.incoming.label) api.nvim_buf_del_extmark(0, NAMESPACE, position.marks.current.label) parse_buffer(bufnr) if enable_autojump and Config.diff.autojump then M.find_next(side) vim.cmd([[normal! zz]]) end end function M.conflict_count(bufnr) if bufnr and not api.nvim_buf_is_valid(bufnr) then return 0 end bufnr = bufnr or 0 local name = api.nvim_buf_get_name(bufnr) if not visited_buffers[name] then return 0 end return #visited_buffers[name].positions end return M ================================================ FILE: lua/avante/extensions/init.lua ================================================ ---@class avante.extensions local M = {} setmetatable(M, { __index = function(t, k) ---@diagnostic disable-next-line: no-unknown t[k] = require("avante.extensions." .. k) return t[k] end, }) ================================================ FILE: lua/avante/extensions/nvim_tree.lua ================================================ local Api = require("avante.api") --- @class avante.extensions.nvim_tree local M = {} --- Adds the currently selected file in NvimTree to the selection via Api.add_selected_file. -- Notifies the user if not invoked within NvimTree or if errors occur. --- @return nil function M.add_file() if vim.bo.filetype ~= "NvimTree" then vim.notify("This action can only be used inside NvimTree.", vim.log.levels.WARN) return end local ok, nvim_tree_api = pcall(require, "nvim-tree.api") if not ok then vim.notify("nvim-tree needed", vim.log.levels.ERROR) return end local success, node = pcall(function() return nvim_tree_api.tree.get_node_under_cursor() end) if not success then vim.notify("Error getting node: " .. tostring(node), vim.log.levels.ERROR) return end local filepath = node.absolute_path Api.add_selected_file(filepath) end --- Removes the currently selected file in NvimTree from the selection via Api.remove_selected_file. -- Notifies the user if not invoked within NvimTree or if errors occur. --- @return nil function M.remove_file() if vim.bo.filetype ~= "NvimTree" then vim.notify("This action can only be used inside NvimTree.", vim.log.levels.WARN) return end local ok, nvim_tree_api = pcall(require, "nvim-tree.api") if not ok then vim.notify("nvim-tree needed", vim.log.levels.ERROR) return end local success, node = pcall(function() return nvim_tree_api.tree.get_node_under_cursor() end) if not success then vim.notify("Error getting node: " .. tostring(node), vim.log.levels.ERROR) return end local filepath = node.absolute_path Api.remove_selected_file(filepath) end return M ================================================ FILE: lua/avante/file_selector.lua ================================================ local Utils = require("avante.utils") local Config = require("avante.config") local Selector = require("avante.ui.selector") local PROMPT_TITLE = "(Avante) Add a file" --- @class FileSelector local FileSelector = {} --- @class FileSelector --- @field id integer --- @field selected_filepaths string[] Absolute paths --- @field event_handlers table ---@alias FileSelectorHandler fun(self: FileSelector, on_select: fun(filepaths: string[] | nil)): nil local function has_scheme(path) return path:find("^(?!term://)%w+://") ~= nil end function FileSelector:process_directory(absolute_path) if absolute_path:sub(-1) == Utils.path_sep then absolute_path = absolute_path:sub(1, -2) end local files = Utils.scan_directory({ directory = absolute_path, add_dirs = false }) for _, file in ipairs(files) do local abs_path = Utils.to_absolute_path(file) if not vim.tbl_contains(self.selected_filepaths, abs_path) then table.insert(self.selected_filepaths, abs_path) end end self:emit("update") end ---@param selected_paths string[] | nil ---@return nil function FileSelector:handle_path_selection(selected_paths) if not selected_paths then return end for _, selected_path in ipairs(selected_paths) do local absolute_path = Utils.to_absolute_path(selected_path) if vim.fn.isdirectory(absolute_path) == 1 then self:process_directory(absolute_path) else local abs_path = Utils.to_absolute_path(selected_path) if Config.file_selector.provider == "native" then table.insert(self.selected_filepaths, abs_path) else if not vim.tbl_contains(self.selected_filepaths, abs_path) then table.insert(self.selected_filepaths, abs_path) end end end end self:emit("update") end ---Scans a given directory and produces a list of files/directories with absolute paths ---@param excluded_paths_set? table Optional set of absolute paths to exclude ---@return { path: string, is_dir: boolean }[] local function get_project_filepaths(excluded_paths_set) excluded_paths_set = excluded_paths_set or {} local project_root = Utils.get_project_root() local files = Utils.scan_directory({ directory = project_root, add_dirs = true }) return vim .iter(files) :filter(function(path) return not excluded_paths_set[path] end) :map(function(path) local is_dir = vim.fn.isdirectory(path) == 1 return { path = path, is_dir = is_dir } end) :totable() end ---@param id integer ---@return FileSelector function FileSelector:new(id) return setmetatable({ id = id, selected_filepaths = {}, event_handlers = {}, }, { __index = self }) end function FileSelector:reset() self.selected_filepaths = {} self.event_handlers = {} self:emit("update") end function FileSelector:add_selected_file(filepath) if not filepath or filepath == "" or has_scheme(filepath) then return end if filepath:match("^oil:") then filepath = filepath:gsub("^oil:", "") end local absolute_path = Utils.to_absolute_path(filepath) if vim.fn.isdirectory(absolute_path) == 1 then self:process_directory(absolute_path) return end -- Avoid duplicates if not vim.tbl_contains(self.selected_filepaths, absolute_path) then table.insert(self.selected_filepaths, absolute_path) self:emit("update") end end function FileSelector:add_current_buffer() local current_buf = vim.api.nvim_get_current_buf() local filepath = vim.api.nvim_buf_get_name(current_buf) if filepath and filepath ~= "" and not has_scheme(filepath) then local absolute_path = Utils.to_absolute_path(filepath) for i, path in ipairs(self.selected_filepaths) do if path == absolute_path then table.remove(self.selected_filepaths, i) self:emit("update") return true end end self:add_selected_file(absolute_path) return true end return false end function FileSelector:on(event, callback) local handlers = self.event_handlers[event] if not handlers then handlers = {} self.event_handlers[event] = handlers end table.insert(handlers, callback) end function FileSelector:emit(event, ...) local handlers = self.event_handlers[event] if not handlers then return end for _, handler in ipairs(handlers) do handler(...) end end function FileSelector:off(event, callback) if not callback then self.event_handlers[event] = {} return end local handlers = self.event_handlers[event] if not handlers then return end for i, handler in ipairs(handlers) do if handler == callback then table.remove(handlers, i) break end end end function FileSelector:open() self:show_selector_ui() end function FileSelector:get_filepaths() if type(Config.file_selector.provider_opts.get_filepaths) == "function" then ---@type avante.file_selector.opts.IGetFilepathsParams local params = { cwd = Utils.get_project_root(), selected_filepaths = self.selected_filepaths, } return Config.file_selector.provider_opts.get_filepaths(params) end local selected_filepaths_set = {} for _, abs_path in ipairs(self.selected_filepaths) do selected_filepaths_set[abs_path] = true end local project_root = Utils.get_project_root() local file_info = get_project_filepaths(selected_filepaths_set) table.sort(file_info, function(a, b) -- Sort alphabetically with directories being first if a.is_dir and not b.is_dir then return true elseif not a.is_dir and b.is_dir then return false else return a.path < b.path end end) return vim .iter(file_info) :map(function(info) local rel_path = Utils.make_relative_path(info.path, project_root) if info.is_dir then rel_path = rel_path .. "/" end return rel_path end) :totable() end ---@return nil function FileSelector:show_selector_ui() local function handler(selected_paths) self:handle_path_selection(selected_paths) end vim.schedule(function() if Config.file_selector.provider ~= nil then Utils.warn("config.file_selector is deprecated, please use config.selector instead!") if type(Config.file_selector.provider) == "function" then local title = string.format("%s:", PROMPT_TITLE) ---@type string local filepaths = self:get_filepaths() ---@type string[] local params = { title = title, filepaths = filepaths, handler = handler } ---@type avante.file_selector.IParams Config.file_selector.provider(params) else ---@type avante.SelectorProvider local provider = "native" if Config.file_selector.provider == "native" then provider = "native" elseif Config.file_selector.provider == "fzf" then provider = "fzf_lua" elseif Config.file_selector.provider == "mini.pick" then provider = "mini_pick" elseif Config.file_selector.provider == "snacks" then provider = "snacks" elseif Config.file_selector.provider == "telescope" then provider = "telescope" elseif type(Config.file_selector.provider) == "function" then provider = Config.file_selector.provider end ---@cast provider avante.SelectorProvider local selector = Selector:new({ provider = provider, title = PROMPT_TITLE, items = vim.tbl_map(function(filepath) return { id = filepath, title = filepath } end, self:get_filepaths()), default_item_id = self.selected_filepaths[1], selected_item_ids = self.selected_filepaths, provider_opts = Config.file_selector.provider_opts, on_select = function(item_ids) self:handle_path_selection(item_ids) end, get_preview_content = function(item_id) local content = Utils.read_file_from_buf_or_disk(item_id) local filetype = Utils.get_filetype(item_id) return table.concat(content or {}, "\n"), filetype end, }) selector:open() end else local selector = Selector:new({ provider = Config.selector.provider, title = PROMPT_TITLE, items = vim.tbl_map(function(filepath) return { id = filepath, title = filepath } end, self:get_filepaths()), default_item_id = self.selected_filepaths[1], selected_item_ids = self.selected_filepaths, provider_opts = Config.selector.provider_opts, on_select = function(item_ids) self:handle_path_selection(item_ids) end, get_preview_content = function(item_id) local content = Utils.read_file_from_buf_or_disk(item_id) local filetype = Utils.get_filetype(item_id) return table.concat(content or {}, "\n"), filetype end, }) selector:open() end end) -- unlist the current buffer as vim.ui.select will be listed local winid = vim.api.nvim_get_current_win() local bufnr = vim.api.nvim_win_get_buf(winid) vim.api.nvim_set_option_value("buflisted", false, { buf = bufnr }) vim.api.nvim_set_option_value("bufhidden", "wipe", { buf = bufnr }) end ---@param idx integer ---@return boolean function FileSelector:remove_selected_filepaths_with_index(idx) if idx > 0 and idx <= #self.selected_filepaths then table.remove(self.selected_filepaths, idx) self:emit("update") return true end return false end function FileSelector:remove_selected_file(rel_path) local abs_path = Utils.to_absolute_path(rel_path) local idx = Utils.tbl_indexof(self.selected_filepaths, abs_path) if idx then self:remove_selected_filepaths_with_index(idx) end end ---@return { path: string, content: string, file_type: string }[] function FileSelector:get_selected_files_contents() local contents = {} for _, filepath in ipairs(self.selected_filepaths) do local lines, error = Utils.read_file_from_buf_or_disk(filepath) lines = lines or {} local filetype = Utils.get_filetype(filepath) if error ~= nil then Utils.error("error reading file: " .. error) else local content = table.concat(lines, "\n") table.insert(contents, { path = filepath, content = content, file_type = filetype }) end end return contents end function FileSelector:get_selected_filepaths() return vim.deepcopy(self.selected_filepaths) end ---@return nil function FileSelector:add_quickfix_files() local quickfix_files = vim .iter(vim.fn.getqflist({ items = 0 }).items) :filter(function(item) return item.bufnr ~= 0 end) :map(function(item) return Utils.to_absolute_path(vim.api.nvim_buf_get_name(item.bufnr)) end) :totable() for _, filepath in ipairs(quickfix_files) do self:add_selected_file(filepath) end end ---@return nil function FileSelector:add_buffer_files() local buffers = vim.api.nvim_list_bufs() for _, bufnr in ipairs(buffers) do -- Skip invalid or unlisted buffers if vim.api.nvim_buf_is_valid(bufnr) and vim.bo[bufnr].buflisted then local filepath = vim.api.nvim_buf_get_name(bufnr) -- Skip empty paths and special buffers (like terminals) if filepath ~= "" and not has_scheme(filepath) then local absolute_path = Utils.to_absolute_path(filepath) self:add_selected_file(absolute_path) end end end end return FileSelector ================================================ FILE: lua/avante/health.lua ================================================ local M = {} local H = require("vim.health") local Utils = require("avante.utils") local Config = require("avante.config") function M.check() H.start("avante.nvim") -- Required dependencies with their module names local required_plugins = { ["plenary.nvim"] = { path = "nvim-lua/plenary.nvim", module = "plenary", }, ["nui.nvim"] = { path = "MunifTanjim/nui.nvim", module = "nui.popup", }, } for name, plugin in pairs(required_plugins) do if Utils.has(name) or Utils.has(plugin.module) then H.ok(string.format("Found required plugin: %s", plugin.path)) else H.error(string.format("Missing required plugin: %s", plugin.path)) end end -- Optional dependencies if Utils.icons_enabled() then H.ok("Found icons plugin (nvim-web-devicons or mini.icons)") else H.warn("No icons plugin found (nvim-web-devicons or mini.icons). Icons will not be displayed") end -- Check input UI provider local input_provider = Config.input and Config.input.provider or "native" if input_provider == "dressing" then if Utils.has("dressing.nvim") or Utils.has("dressing") then H.ok("Found configured input provider: dressing.nvim") else H.error("Input provider is set to 'dressing' but dressing.nvim is not installed") end elseif input_provider == "snacks" then if Utils.has("snacks.nvim") or Utils.has("snacks") then H.ok("Found configured input provider: snacks.nvim") else H.error("Input provider is set to 'snacks' but snacks.nvim is not installed") end else H.ok("Using native input provider (no additional dependencies required)") end -- Check Copilot if configured if Config.provider and Config.provider == "copilot" then if Utils.has("copilot.lua") or Utils.has("copilot.vim") or Utils.has("copilot") then H.ok("Found Copilot plugin") else H.error("Copilot provider is configured but neither copilot.lua nor copilot.vim is installed") end end -- Check TreeSitter dependencies M.check_treesitter() end -- Check TreeSitter functionality and parsers function M.check_treesitter() H.start("TreeSitter Dependencies") -- List of important parsers for avante.nvim local essential_parsers = { "markdown", } local missing_parsers = {} ---@type string[] for _, parser_name in ipairs(essential_parsers) do local loaded_parser = vim.treesitter.language.add(parser_name) if not loaded_parser then missing_parsers[#missing_parsers + 1] = parser_name end end if #missing_parsers == 0 then H.ok("All essential TreeSitter parsers are installed") else H.warn( string.format( "Missing recommended parsers: %s. Install with :TSInstall %s", table.concat(missing_parsers, ", "), table.concat(missing_parsers, " ") ) ) end -- Check TreeSitter highlight local _, highlighter = pcall(require, "vim.treesitter.highlighter") if not highlighter then H.warn("TreeSitter highlighter not available. Syntax highlighting might be limited") else H.ok("TreeSitter highlighter is available") end end return M ================================================ FILE: lua/avante/highlights.lua ================================================ local api = vim.api local Config = require("avante.config") local Utils = require("avante.utils") local bit = require("bit") local rshift, band = bit.rshift, bit.band local Highlights = { TITLE = { name = "AvanteTitle", fg = "#1e222a", bg = "#98c379" }, REVERSED_TITLE = { name = "AvanteReversedTitle", fg = "#98c379", bg_link = "NormalFloat" }, SUBTITLE = { name = "AvanteSubtitle", fg = "#1e222a", bg = "#56b6c2" }, REVERSED_SUBTITLE = { name = "AvanteReversedSubtitle", fg = "#56b6c2", bg_link = "NormalFloat" }, THIRD_TITLE = { name = "AvanteThirdTitle", fg = "#ABB2BF", bg = "#353B45" }, REVERSED_THIRD_TITLE = { name = "AvanteReversedThirdTitle", fg = "#353B45", bg_link = "NormalFloat" }, SUGGESTION = { name = "AvanteSuggestion", link = "Comment" }, ANNOTATION = { name = "AvanteAnnotation", link = "Comment" }, POPUP_HINT = { name = "AvantePopupHint", link = "NormalFloat" }, INLINE_HINT = { name = "AvanteInlineHint", link = "Keyword" }, TO_BE_DELETED = { name = "AvanteToBeDeleted", bg = "#ffcccc", strikethrough = true }, TO_BE_DELETED_WITHOUT_STRIKETHROUGH = { name = "AvanteToBeDeletedWOStrikethrough", bg = "#562C30" }, CONFIRM_TITLE = { name = "AvanteConfirmTitle", fg = "#1e222a", bg = "#e06c75" }, BUTTON_DEFAULT = { name = "AvanteButtonDefault", fg = "#1e222a", bg = "#ABB2BF" }, BUTTON_DEFAULT_HOVER = { name = "AvanteButtonDefaultHover", fg = "#1e222a", bg = "#a9cf8a" }, BUTTON_PRIMARY = { name = "AvanteButtonPrimary", fg = "#1e222a", bg = "#ABB2BF" }, BUTTON_PRIMARY_HOVER = { name = "AvanteButtonPrimaryHover", fg = "#1e222a", bg = "#56b6c2" }, BUTTON_DANGER = { name = "AvanteButtonDanger", fg = "#1e222a", bg = "#ABB2BF" }, BUTTON_DANGER_HOVER = { name = "AvanteButtonDangerHover", fg = "#1e222a", bg = "#e06c75" }, AVANTE_PROMPT_INPUT = { name = "AvantePromptInput" }, AVANTE_PROMPT_INPUT_BORDER = { name = "AvantePromptInputBorder", link = "NormalFloat" }, AVANTE_SIDEBAR_WIN_SEPARATOR = { name = "AvanteSidebarWinSeparator", fg_link_bg = "NormalFloat", bg_link = "NormalFloat", }, AVANTE_SIDEBAR_WIN_HORIZONTAL_SEPARATOR = { name = "AvanteSidebarWinHorizontalSeparator", fg_link = "WinSeparator", bg_link = "NormalFloat", }, AVANTE_SIDEBAR_NORMAL = { name = "AvanteSidebarNormal", link = "NormalFloat" }, AVANTE_COMMENT_FG = { name = "AvanteCommentFg", fg_link = "Comment" }, AVANTE_REVERSED_NORMAL = { name = "AvanteReversedNormal", fg_link_bg = "Normal", bg_link_fg = "Normal" }, AVANTE_STATE_SPINNER_GENERATING = { name = "AvanteStateSpinnerGenerating", fg = "#1e222a", bg = "#ab9df2" }, AVANTE_STATE_SPINNER_TOOL_CALLING = { name = "AvanteStateSpinnerToolCalling", fg = "#1e222a", bg = "#56b6c2" }, AVANTE_STATE_SPINNER_FAILED = { name = "AvanteStateSpinnerFailed", fg = "#1e222a", bg = "#e06c75" }, AVANTE_STATE_SPINNER_SUCCEEDED = { name = "AvanteStateSpinnerSucceeded", fg = "#1e222a", bg = "#98c379" }, AVANTE_STATE_SPINNER_SEARCHING = { name = "AvanteStateSpinnerSearching", fg = "#1e222a", bg = "#c678dd" }, AVANTE_STATE_SPINNER_THINKING = { name = "AvanteStateSpinnerThinking", fg = "#1e222a", bg = "#c678dd" }, AVANTE_STATE_SPINNER_COMPACTING = { name = "AvanteStateSpinnerCompacting", fg = "#1e222a", bg = "#c678dd" }, AVANTE_TASK_RUNNING = { name = "AvanteTaskRunning", fg = "#c678dd", bg_link = "Normal" }, AVANTE_TASK_COMPLETED = { name = "AvanteTaskCompleted", fg = "#98c379", bg_link = "Normal" }, AVANTE_TASK_FAILED = { name = "AvanteTaskFailed", fg = "#e06c75", bg_link = "Normal" }, AVANTE_THINKING = { name = "AvanteThinking", fg = "#c678dd", bg_link = "Normal" }, -- Gradient logo highlights AVANTE_LOGO_LINE_1 = { name = "AvanteLogoLine1", fg = "#f5f5f5" }, AVANTE_LOGO_LINE_2 = { name = "AvanteLogoLine2", fg = "#e8e8e8" }, AVANTE_LOGO_LINE_3 = { name = "AvanteLogoLine3", fg = "#dbdbdb" }, AVANTE_LOGO_LINE_4 = { name = "AvanteLogoLine4", fg = "#cfcfcf" }, AVANTE_LOGO_LINE_5 = { name = "AvanteLogoLine5", fg = "#c2c2c2" }, AVANTE_LOGO_LINE_6 = { name = "AvanteLogoLine6", fg = "#b5b5b5" }, AVANTE_LOGO_LINE_7 = { name = "AvanteLogoLine7", fg = "#a8a8a8" }, AVANTE_LOGO_LINE_8 = { name = "AvanteLogoLine8", fg = "#9b9b9b" }, AVANTE_LOGO_LINE_9 = { name = "AvanteLogoLine9", fg = "#8e8e8e" }, AVANTE_LOGO_LINE_10 = { name = "AvanteLogoLine10", fg = "#818181" }, AVANTE_LOGO_LINE_11 = { name = "AvanteLogoLine11", fg = "#747474" }, AVANTE_LOGO_LINE_12 = { name = "AvanteLogoLine12", fg = "#676767" }, AVANTE_LOGO_LINE_13 = { name = "AvanteLogoLine13", fg = "#5a5a5a" }, AVANTE_LOGO_LINE_14 = { name = "AvanteLogoLine14", fg = "#4d4d4d" }, } Highlights.conflict = { CURRENT = { name = "AvanteConflictCurrent", bg = "#562C30", bold = true }, CURRENT_LABEL = { name = "AvanteConflictCurrentLabel", shade_link = "AvanteConflictCurrent", shade = 30 }, INCOMING = { name = "AvanteConflictIncoming", bg = 3229523, bold = true }, -- #314753 INCOMING_LABEL = { name = "AvanteConflictIncomingLabel", shade_link = "AvanteConflictIncoming", shade = 30 }, } --- helper local H = {} local M = {} local function has_set_colors(hl_group) return next(Utils.get_hl(hl_group)) ~= nil end local first_setup = true local already_set_highlights = {} function M.setup() if Config.behaviour.auto_set_highlight_group then vim .iter(Highlights) :filter(function(k, _) -- return all uppercase key with underscore or fully uppercase key return k:match("^%u+_") or k:match("^%u+$") end) :each(function(_, hl) if first_setup and has_set_colors(hl.name) then already_set_highlights[hl.name] = true end if not already_set_highlights[hl.name] then local bg = hl.bg local fg = hl.fg if hl.bg_link ~= nil then bg = Utils.get_hl(hl.bg_link).bg end if hl.fg_link ~= nil then fg = Utils.get_hl(hl.fg_link).fg end if hl.bg_link_fg ~= nil then bg = Utils.get_hl(hl.bg_link_fg).fg end if hl.fg_link_bg ~= nil then fg = Utils.get_hl(hl.fg_link_bg).bg end api.nvim_set_hl( 0, hl.name, { fg = fg or nil, bg = bg or nil, link = hl.link or nil, strikethrough = hl.strikethrough } ) end end) end if first_setup then vim.iter(Highlights.conflict):each(function(_, hl) if hl.name and has_set_colors(hl.name) then already_set_highlights[hl.name] = true end end) end first_setup = false M.setup_conflict_highlights() end function M.setup_conflict_highlights() local custom_hls = Config.highlights.diff ---@return number | nil local function get_bg(hl_name) return Utils.get_hl(hl_name).bg end local function get_bold(hl_name) return Utils.get_hl(hl_name).bold end vim.iter(Highlights.conflict):each(function(key, hl) --- set none shade linked highlights first if hl.shade_link ~= nil and hl.shade ~= nil then return end if already_set_highlights[hl.name] then return end local bg = hl.bg local bold = hl.bold local custom_hl_name = custom_hls[key:lower()] if custom_hl_name ~= nil then bg = get_bg(custom_hl_name) or hl.bg bold = get_bold(custom_hl_name) or hl.bold end api.nvim_set_hl(0, hl.name, { bg = bg, default = true, bold = bold }) end) vim.iter(Highlights.conflict):each(function(key, hl) --- only set shade linked highlights if hl.shade_link == nil or hl.shade == nil then return end if already_set_highlights[hl.name] then return end local bg local bold = hl.bold local custom_hl_name = custom_hls[key:lower()] if custom_hl_name ~= nil then bg = get_bg(custom_hl_name) bold = get_bold(custom_hl_name) or hl.bold else local link_bg = get_bg(hl.shade_link) if link_bg == nil then Utils.warn(string.format("highlights %s don't have bg, use fallback", hl.shade_link)) link_bg = 3229523 end bg = H.shade_color(link_bg, hl.shade) end api.nvim_set_hl(0, hl.name, { bg = bg, default = true, bold = bold }) end) end setmetatable(M, { __index = function(t, k) if Highlights[k] ~= nil then return Highlights[k].name elseif Highlights.conflict[k] ~= nil then return Highlights.conflict[k].name end return t[k] end, }) --- Returns a table containing the RGB values encoded inside 24 least --- significant bits of the number @rgb_24bit --- ---@param rgb_24bit number 24-bit RGB value ---@return {r: integer, g: integer, b: integer} with keys 'r', 'g', 'b' in [0,255] function H.decode_24bit_rgb(rgb_24bit) if vim.fn.has("nvim-0.11") == 1 then vim.validate("rgb_24bit", rgb_24bit, "number", true) else vim.validate({ rgb_24bit = { rgb_24bit, "number", true } }) end local r = band(rshift(rgb_24bit, 16), 255) local g = band(rshift(rgb_24bit, 8), 255) local b = band(rgb_24bit, 255) return { r = r, g = g, b = b } end ---@param attr integer ---@param percent integer function H.alter(attr, percent) return math.floor(attr * (100 + percent) / 100) end ---@source https://stackoverflow.com/q/5560248 ---@see https://stackoverflow.com/a/37797380 ---Lighten a specified hex color ---@param color number ---@param percent number ---@return string function H.shade_color(color, percent) percent = vim.opt.background:get() == "light" and percent / 5 or percent local rgb = H.decode_24bit_rgb(color) if not rgb.r or not rgb.g or not rgb.b then return "NONE" end local r, g, b = H.alter(rgb.r, percent), H.alter(rgb.g, percent), H.alter(rgb.b, percent) r, g, b = math.min(r, 255), math.min(g, 255), math.min(b, 255) return string.format("#%02x%02x%02x", r, g, b) end return M ================================================ FILE: lua/avante/history/helpers.lua ================================================ local Utils = require("avante.utils") local M = {} ---If message is a text message return the text. ---@param message avante.HistoryMessage ---@return string | nil function M.get_text_data(message) local content = message.message.content if type(content) == "table" then assert(#content == 1, "more than one entry in message content") local item = content[1] if type(item) == "string" then return item elseif type(item) == "table" and item.type == "text" then return item.content end elseif type(content) == "string" then return content end end ---If message is a "tool use" message returns information about the tool invocation. ---@param message avante.HistoryMessage ---@return AvanteLLMToolUse | nil function M.get_tool_use_data(message) local content = message.message.content if type(content) == "table" then assert(#content == 1, "more than one entry in message content") local item = content[1] if item.type == "tool_use" then ---@cast item AvanteLLMToolUse return item end end end ---If message is a "tool result" message returns results of the tool invocation. ---@param message avante.HistoryMessage ---@return AvanteLLMToolResult | nil function M.get_tool_result_data(message) local content = message.message.content if type(content) == "table" then assert(#content == 1, "more than one entry in message content") local item = content[1] if item.type == "tool_result" then ---@cast item AvanteLLMToolResult return item end end end ---Attempts to locate result of a tool execution given tool invocation ID ---@param id string ---@param messages avante.HistoryMessage[] ---@return AvanteLLMToolResult | nil function M.get_tool_result(id, messages) for idx = #messages, 1, -1 do local msg = messages[idx] local result = M.get_tool_result_data(msg) if result and result.tool_use_id == id then return result end end end ---Given a tool invocation ID locate corresponding tool use message ---@param id string ---@param messages avante.HistoryMessage[] ---@return avante.HistoryMessage | nil function M.get_tool_use_message(id, messages) for idx = #messages, 1, -1 do local msg = messages[idx] local use = M.get_tool_use_data(msg) if use and use.id == id then return msg end end end ---Given a tool invocation ID locate corresponding tool result message ---@param id string ---@param messages avante.HistoryMessage[] ---@return avante.HistoryMessage | nil function M.get_tool_result_message(id, messages) for idx = #messages, 1, -1 do local msg = messages[idx] local result = M.get_tool_result_data(msg) if result and result.tool_use_id == id then return msg end end end ---@param message avante.HistoryMessage ---@return boolean function M.is_thinking_message(message) local content = message.message.content return type(content) == "table" and (content[1].type == "thinking" or content[1].type == "redacted_thinking") end ---@param message avante.HistoryMessage ---@return boolean function M.is_tool_result_message(message) return M.get_tool_result_data(message) ~= nil end ---@param message avante.HistoryMessage ---@return boolean function M.is_tool_use_message(message) return M.get_tool_use_data(message) ~= nil end return M ================================================ FILE: lua/avante/history/init.lua ================================================ local Helpers = require("avante.history.helpers") local Message = require("avante.history.message") local Utils = require("avante.utils") local M = {} M.Helpers = Helpers M.Message = Message ---@param history avante.ChatHistory ---@return avante.HistoryMessage[] function M.get_history_messages(history) if history.messages then return history.messages end local messages = {} for _, entry in ipairs(history.entries or {}) do if entry.request and entry.request ~= "" then local message = Message:new("user", entry.request, { timestamp = entry.timestamp, is_user_submission = true, visible = entry.visible, selected_filepaths = entry.selected_filepaths, selected_code = entry.selected_code, }) table.insert(messages, message) end if entry.response and entry.response ~= "" then local message = Message:new("assistant", entry.response, { timestamp = entry.timestamp, visible = entry.visible, }) table.insert(messages, message) end end history.messages = messages return messages end ---Represents information about tool use: invocation, result, affected file (for "view" or "edit" tools). ---@class HistoryToolInfo ---@field kind "edit" | "view" | "other" ---@field use AvanteLLMToolUse ---@field result? AvanteLLMToolResult ---@field result_message? avante.HistoryMessage Complete result message ---@field path? string Uniform (normalized) path of the affected file ---@class HistoryFileInfo ---@field last_tool_id? string ID of the tool with most up-to-date state of the file ---@field edit_tool_id? string ID of the last tool done edit on the file ---Collects information about all uses of tools in the history: their invocations, results, and affected files. ---@param messages avante.HistoryMessage[] ---@return table ---@return table local function collect_tool_info(messages) ---@type table Maps tool ID to tool information local tools = {} ---@type table Maps file path to file information local files = {} -- Collect invocations of all tools, and also build a list of viewed or edited files. for _, message in ipairs(messages) do local use = Helpers.get_tool_use_data(message) if use then if use.name == "view" or Utils.is_edit_tool_use(use) then if use.input.path then local path = Utils.uniform_path(use.input.path) if use.id then tools[use.id] = { kind = use.name == "view" and "view" or "edit", use = use, path = path } end end else if use.id then tools[use.id] = { kind = "other", use = use } end end goto continue end local result = Helpers.get_tool_result_data(message) if result then -- We assume that "result" entries always come after corresponding "use" entries. local info = tools[result.tool_use_id] if info then info.result = result info.result_message = message if info.path then local f = files[info.path] if not f then f = {} files[info.path] = f end f.last_tool_id = result.tool_use_id if info.kind == "edit" and not (result.is_error or result.is_user_declined) then f.edit_tool_id = result.tool_use_id end end end end ::continue:: end return tools, files end ---Converts a tool invocation (use + result) into a simple request/response pair of text messages ---@param tool_info HistoryToolInfo ---@return avante.HistoryMessage[] local function convert_tool_to_text(tool_info) return { Message:new_assistant_synthetic( string.format("Tool use %s(%s)", tool_info.use.name, vim.json.encode(tool_info.use.input)) ), Message:new_user_synthetic({ type = "text", text = string.format( "Tool use [%s] is successful: %s", tool_info.use.name, tostring(not tool_info.result.is_error) ), }), } end ---Generates a fake file "content" telling LLM to look further for up-to-date data ---@param path string ---@return string local function stale_view_content(path) return string.format("The file %s has been updated. Please use the latest `view` tool result!", path) end ---Updates the result of "view" tool invocation with latest contents of a buffer or file, ---or a stub message if this result will be superseded by another one. ---@param tool_info HistoryToolInfo ---@param stale_view boolean local function update_view_result(tool_info, stale_view) local use = tool_info.use local result = tool_info.result if stale_view then result.content = stale_view_content(tool_info.path) else local view_result, view_error = require("avante.llm_tools.view").func( { path = tool_info.path, start_line = use.input.start_line, end_line = use.input.end_line }, {} ) result.content = view_error and ("Error: " .. view_error) or view_result result.is_error = view_error ~= nil end end ---Generates synthetic "view" tool invocation to tell LLM to refresh its view of a file after editing ---@param tool_use AvanteLLMToolUse ---@param path any ---@param stale_view any ---@return avante.HistoryMessage[] local function generate_view_messages(tool_use, path, stale_view) local view_result, view_error if stale_view then view_result = stale_view_content(path) else view_result, view_error = require("avante.llm_tools.view").func({ path = path }, {}) end if view_error then view_result = "Error: " .. view_error end local view_tool_use_id = Utils.generate_call_tool_id() local view_tool_name = "view" local view_tool_input = { path = path } if tool_use.name == "str_replace_editor" and tool_use.input.command == "str_replace" then view_tool_name = "str_replace_editor" view_tool_input.command = "view" elseif tool_use.name == "str_replace_based_edit_tool" and tool_use.input.command == "str_replace" then view_tool_name = "str_replace_based_edit_tool" view_tool_input.command = "view" end return { Message:new_assistant_synthetic(string.format("Viewing file %s to get the latest content", path)), Message:new_assistant_synthetic({ type = "tool_use", id = view_tool_use_id, name = view_tool_name, input = view_tool_input, }), Message:new_user_synthetic({ type = "tool_result", tool_use_id = view_tool_use_id, content = view_result, is_error = view_error ~= nil, is_user_declined = false, }), } end ---Generates "diagnostic" for a file after it has been edited to help catching errors ---@param path string ---@return avante.HistoryMessage[] local function generate_diagnostic_messages(path) local get_diagnostics_tool_use_id = Utils.generate_call_tool_id() local diagnostics = Utils.lsp.get_diagnostics_from_filepath(path) return { Message:new_assistant_synthetic( string.format("The file %s has been modified, let me check if there are any errors in the changes.", path) ), Message:new_assistant_synthetic({ type = "tool_use", id = get_diagnostics_tool_use_id, name = "get_diagnostics", input = { path = path }, }), Message:new_user_synthetic({ type = "tool_result", tool_use_id = get_diagnostics_tool_use_id, content = vim.json.encode(diagnostics), is_error = false, is_user_declined = false, }), } end ---Iterate through history messages and generate a new list containing updated history ---that has up-to-date file contents and potentially updated diagnostic for modified ---files. ---@param messages avante.HistoryMessage[] ---@param tools HistoryToolInfo[] ---@param files HistoryFileInfo[] ---@param add_diagnostic boolean Whether to generate and add diagnostic info to "edit" invocations ---@param tools_to_text integer Number of tool invocations to be converted to simple text ---@return avante.HistoryMessage[] local function refresh_history(messages, tools, files, add_diagnostic, tools_to_text) ---@type avante.HistoryMessage[] local updated_messages = {} local tool_count = 0 for _, message in ipairs(messages) do local use = Helpers.get_tool_use_data(message) if use then -- This is a tool invocation message. We will be handling both use and result together. local tool_info = tools[use.id] if not tool_info then goto continue end if not tool_info.result then goto continue end if tool_count < tools_to_text then local text_msgs = convert_tool_to_text(tool_info) Utils.debug("Converted", use.name, "invocation to", #text_msgs, "messages") updated_messages = vim.list_extend(updated_messages, text_msgs) else table.insert(updated_messages, message) table.insert(updated_messages, tool_info.result_message) tool_count = tool_count + 1 if tool_info.kind == "view" then local path = tool_info.path assert(path, "encountered 'view' tool invocation without path") update_view_result(tool_info, use.id ~= files[tool_info.path].last_tool_id) end end if tool_info.kind == "edit" then local path = tool_info.path assert(path, "encountered 'edit' tool invocation without path") local file_info = files[path] -- If this is the last operation for this file, generate synthetic "view" -- invocation to provide the up-to-date file contents. if not tool_info.result.is_error then local view_msgs = generate_view_messages(use, path, use.id == file_info.last_tool_id) Utils.debug("Added", #view_msgs, "'view' tool messages for", path) updated_messages = vim.list_extend(updated_messages, view_msgs) tool_count = tool_count + 1 end if add_diagnostic and use.id == file_info.edit_tool_id then local diag_msgs = generate_diagnostic_messages(path) Utils.debug("Added", #diag_msgs, "'diagnostics' tool messages for", path) updated_messages = vim.list_extend(updated_messages, diag_msgs) tool_count = tool_count + 1 end end elseif not Helpers.get_tool_result_data(message) then -- Skip the tool result messages (since we process them together with their "use"s. -- All other (non-tool-related) messages we simply keep. table.insert(updated_messages, message) end ::continue:: end return updated_messages end ---Analyzes the history looking for tool invocations, drops incomplete invocations, ---and updates complete ones with the latest data available. ---@param messages avante.HistoryMessage[] ---@param max_tool_use integer | nil Maximum number of tool invocations to keep ---@param add_diagnostic boolean Mix in LSP diagnostic info for affected files ---@return avante.HistoryMessage[] M.update_tool_invocation_history = function(messages, max_tool_use, add_diagnostic) local tools, files = collect_tool_info(messages) -- Figure number of tool invocations that should be converted to simple "text" -- messages to reduce prompt costs. local tools_to_text = 0 if max_tool_use then local n_edits = vim.iter(files):fold( 0, ---@param count integer ---@param file_info HistoryFileInfo function(count, file_info) if file_info.edit_tool_id then count = count + 1 end return count end ) -- Each valid "edit" invocation will result in synthetic "view" and also -- in "diagnostic" if it is requested by the caller. local expected = #tools + n_edits + (add_diagnostic and n_edits or 0) tools_to_text = expected - max_tool_use end return refresh_history(messages, tools, files, add_diagnostic, tools_to_text) end ---Scans message history backwards, looking for tool invocations that have not been executed yet ---@param messages avante.HistoryMessage[] ---@return AvantePartialLLMToolUse[] ---@return avante.HistoryMessage[] function M.get_pending_tools(messages) local last_turn_id = nil if #messages > 0 then last_turn_id = messages[#messages].turn_id end local pending_tool_uses = {} ---@type AvantePartialLLMToolUse[] local pending_tool_uses_messages = {} ---@type avante.HistoryMessage[] local tool_result_seen = {} for idx = #messages, 1, -1 do local message = messages[idx] if last_turn_id and message.turn_id ~= last_turn_id then break end local use = Helpers.get_tool_use_data(message) if use then if not tool_result_seen[use.id] then local partial_tool_use = { name = use.name, id = use.id, input = use.input, state = message.state, } table.insert(pending_tool_uses, 1, partial_tool_use) table.insert(pending_tool_uses_messages, 1, message) end goto continue end local result = Helpers.get_tool_result_data(message) if result then tool_result_seen[result.tool_use_id] = true end ::continue:: end return pending_tool_uses, pending_tool_uses_messages end return M ================================================ FILE: lua/avante/history/message.lua ================================================ local Utils = require("avante.utils") ---@class avante.HistoryMessage local M = {} M.__index = M ---@class avante.HistoryMessage.Opts ---@field uuid? string ---@field turn_id? string ---@field state? avante.HistoryMessageState ---@field displayed_content? string ---@field original_content? AvanteLLMMessageContent ---@field selected_code? AvanteSelectedCode ---@field selected_filepaths? string[] ---@field is_calling? boolean ---@field is_dummy? boolean ---@field is_user_submission? boolean ---@field just_for_display? boolean ---@field visible? boolean --- ---@param role "user" | "assistant" ---@param content AvanteLLMMessageContentItem ---@param opts? avante.HistoryMessage.Opts ---@return avante.HistoryMessage function M:new(role, content, opts) ---@type AvanteLLMMessage local message = { role = role, content = type(content) == "string" and content or { content } } local obj = { message = message, uuid = Utils.uuid(), state = "generated", timestamp = Utils.get_timestamp(), is_user_submission = false, visible = true, } obj = vim.tbl_extend("force", obj, opts or {}) return setmetatable(obj, M) end ---Creates a new instance of synthetic (dummy) history message ---@param role "assistant" | "user" ---@param item AvanteLLMMessageContentItem ---@return avante.HistoryMessage function M:new_synthetic(role, item) return M:new(role, item, { is_dummy = true }) end ---Creates a new instance of synthetic (dummy) history message attributed to the assistant ---@param item AvanteLLMMessageContentItem ---@return avante.HistoryMessage function M:new_assistant_synthetic(item) return M:new_synthetic("assistant", item) end ---Creates a new instance of synthetic (dummy) history message attributed to the user ---@param item AvanteLLMMessageContentItem ---@return avante.HistoryMessage function M:new_user_synthetic(item) return M:new_synthetic("user", item) end ---Updates content of a message as long as it is a simple text (or empty). ---@param new_content string function M:update_content(new_content) assert(type(self.message.content) == "string", "can only update content of simple string messages") self.message.content = new_content end return M ================================================ FILE: lua/avante/history/render.lua ================================================ local Helpers = require("avante.history.helpers") local Line = require("avante.ui.line") local Utils = require("avante.utils") local Highlights = require("avante.highlights") local M = {} ---@diagnostic disable-next-line: deprecated local islist = vim.islist or vim.tbl_islist ---Converts text into format suitable for UI ---@param text string ---@param decoration string | nil ---@param filter? fun(text_line: string, idx: integer, len: integer): boolean ---@return avante.ui.Line[] local function text_to_lines(text, decoration, filter) local text_lines = vim.split(text, "\n") local lines = {} for idx, text_line in ipairs(text_lines) do if filter and not filter(text_line, idx, #text_lines) then goto continue end if decoration then table.insert(lines, Line:new({ { decoration }, { text_line } })) else table.insert(lines, Line:new({ { text_line } })) end ::continue:: end return lines end ---Converts text into format suitable for UI ---@param text string ---@param decoration string | nil ---@param truncate boolean | nil ---@return avante.ui.Line[] local function text_to_truncated_lines(text, decoration, truncate) local text_lines = vim.split(text, "\n") local lines = {} for _, text_line in ipairs(text_lines) do if truncate and #lines > 3 then table.insert( lines, Line:new({ { decoration }, { string.format("... (Result truncated, remaining %d lines not shown)", #text_lines - #lines + 1), Highlights.AVANTE_COMMENT_FG, }, }) ) break end table.insert(lines, Line:new({ { decoration }, { text_line } })) end return lines end ---@param lines avante.ui.Line[] ---@param decoration string | nil ---@param truncate boolean | nil ---@return avante.ui.Line[] local function lines_to_truncated_lines(lines, decoration, truncate) local truncated_lines = {} for idx, line in ipairs(lines) do if truncate and #truncated_lines > 3 then table.insert( truncated_lines, Line:new({ { decoration }, { string.format("... (Result truncated, remaining %d lines not shown)", #lines - idx + 1), Highlights.AVANTE_COMMENT_FG, }, }) ) break end table.insert(truncated_lines, line) end return truncated_lines end ---Converts "thinking" item into format suitable for UI ---@param item AvanteLLMMessageContentItem ---@return avante.ui.Line[] local function thinking_to_lines(item) local text = item.thinking or item.data or "" local text_lines = vim.split(text, "\n") --- trim prefix empty lines while #text_lines > 0 and text_lines[1] == "" do table.remove(text_lines, 1) end --- trim suffix empty lines while #text_lines > 0 and text_lines[#text_lines] == "" do table.remove(text_lines, #text_lines) end local ui_lines = {} table.insert(ui_lines, Line:new({ { Utils.icon("🤔 ") .. "Thought content:" } })) table.insert(ui_lines, Line:new({ { "" } })) for _, text_line in ipairs(text_lines) do table.insert(ui_lines, Line:new({ { "> " .. text_line } })) end return ui_lines end ---Converts logs generated by a tool during execution into format suitable for UI ---@param tool_name string ---@param logs string[] ---@return avante.ui.Line[] function M.tool_logs_to_lines(tool_name, logs) local ui_lines = {} local num_logs = #logs for log_idx = 1, num_logs do local log_lines = vim.split(logs[log_idx]:gsub("^%[" .. tool_name .. "%]: ", "", 1), "\n") local num_lines = #log_lines for line_idx = 1, num_lines do local decoration = "│ " table.insert(ui_lines, Line:new({ { decoration }, { " " .. log_lines[line_idx] } })) end end return ui_lines end local STATE_TO_HL = { generating = "AvanteStateSpinnerToolCalling", failed = "AvanteStateSpinnerFailed", succeeded = "AvanteStateSpinnerSucceeded", } function M.get_diff_lines(old_str, new_str, decoration, truncate) local lines = {} local line_count = 0 local old_lines = vim.split(old_str, "\n") local new_lines = vim.split(new_str, "\n") ---@diagnostic disable-next-line: assign-type-mismatch, missing-fields local patch = vim.diff(old_str, new_str, { ---@type integer[][] algorithm = "histogram", result_type = "indices", ctxlen = vim.o.scrolloff, }) local prev_start_a = 0 local truncated_lines = 0 for _, hunk in ipairs(patch) do local start_a, count_a, start_b, count_b = unpack(hunk) local no_change_lines = vim.list_slice(old_lines, prev_start_a, start_a - 1) if truncate then local last_three_no_change_lines = vim.list_slice(no_change_lines, #no_change_lines - 3) truncated_lines = truncated_lines + #no_change_lines - #last_three_no_change_lines if #no_change_lines > 4 then table.insert(lines, Line:new({ { decoration }, { "...", Highlights.AVANTE_COMMENT_FG } })) end no_change_lines = last_three_no_change_lines end for idx, line in ipairs(no_change_lines) do if truncate and line_count > 10 then truncated_lines = truncated_lines + #no_change_lines - idx break end line_count = line_count + 1 table.insert(lines, Line:new({ { decoration }, { line } })) end prev_start_a = start_a + count_a if count_a > 0 then local delete_lines = vim.list_slice(old_lines, start_a, start_a + count_a - 1) for idx, line in ipairs(delete_lines) do if truncate and line_count > 10 then truncated_lines = truncated_lines + #delete_lines - idx break end line_count = line_count + 1 table.insert(lines, Line:new({ { decoration }, { line, Highlights.TO_BE_DELETED_WITHOUT_STRIKETHROUGH } })) end end if count_b > 0 then local create_lines = vim.list_slice(new_lines, start_b, start_b + count_b - 1) for idx, line in ipairs(create_lines) do if truncate and line_count > 10 then truncated_lines = truncated_lines + #create_lines - idx break end line_count = line_count + 1 table.insert(lines, Line:new({ { decoration }, { line, Highlights.INCOMING } })) end end end if prev_start_a < #old_lines then -- Append remaining old_lines local no_change_lines = vim.list_slice(old_lines, prev_start_a, #old_lines) local first_three_no_change_lines = vim.list_slice(no_change_lines, 1, 3) for idx, line in ipairs(first_three_no_change_lines) do if truncate and line_count > 10 then truncated_lines = truncated_lines + #first_three_no_change_lines - idx break end line_count = line_count + 1 table.insert(lines, Line:new({ { decoration }, { line } })) end end if truncate and truncated_lines > 0 then table.insert( lines, Line:new({ { decoration }, { string.format("... (Result truncated, remaining %d lines not shown)", truncated_lines), Highlights.AVANTE_COMMENT_FG, }, }) ) end return lines end ---@param content any ---@param decoration string | nil ---@param truncate boolean | nil function M.get_content_lines(content, decoration, truncate) local lines = {} local content_obj = content if type(content) == "string" then local ok, content_obj_ = pcall(vim.json.decode, content) if ok then content_obj = content_obj_ end end if type(content_obj) == "table" then if islist(content_obj) then local all_lines = {} for _, content_item in ipairs(content_obj) do if type(content_item) == "string" then local lines_ = text_to_lines(content_item, decoration) all_lines = vim.list_extend(all_lines, lines_) end end local lines_ = lines_to_truncated_lines(all_lines, decoration, truncate) lines = vim.list_extend(lines, lines_) end if type(content_obj.content) == "string" then local lines_ = text_to_truncated_lines(content_obj.content, decoration, truncate) lines = vim.list_extend(lines, lines_) end if islist(content_obj.content) then local all_lines = {} for _, content_item in ipairs(content_obj.content) do if type(content_item) == "string" then local lines_ = text_to_lines(content_item, decoration) all_lines = vim.list_extend(all_lines, lines_) end end local lines_ = lines_to_truncated_lines(all_lines, decoration, truncate) lines = vim.list_extend(lines, lines_) end if islist(content_obj.matches) then local all_lines = {} for _, content_item in ipairs(content_obj.matches) do if type(content_item) == "string" then local lines_ = text_to_lines(content_item, decoration) all_lines = vim.list_extend(all_lines, lines_) end end local lines_ = lines_to_truncated_lines(all_lines, decoration, truncate) lines = vim.list_extend(lines, lines_) end end if type(content_obj) == "string" then local lines_ = text_to_lines(content_obj, decoration) local line_count = 0 for idx, line in ipairs(lines_) do if truncate and line_count > 3 then table.insert( lines, Line:new({ { decoration }, { string.format("... (Result truncated, remaining %d lines not shown)", #lines_ - idx + 1), Highlights.AVANTE_COMMENT_FG, }, }) ) break end line_count = line_count + 1 table.insert(lines, line) end end if type(content_obj) == "number" then table.insert(lines, Line:new({ { decoration }, { tostring(content_obj) } })) end if islist(content) then for _, content_item in ipairs(content) do local line_count = 0 if content_item.type == "content" then if content_item.content.type == "text" then local lines_ = text_to_lines(content_item.content.text, decoration, function(text_line, idx, len) if idx == 1 and text_line:match("^%s*```%s*$") then return false end if idx == len and text_line:match("^%s*```%s*$") then return false end return true end) for idx, line in ipairs(lines_) do if truncate and line_count > 3 then table.insert( lines, Line:new({ { decoration }, { string.format("... (Result truncated, remaining %d lines not shown)", #lines_ - idx + 1), Highlights.AVANTE_COMMENT_FG, }, }) ) break end line_count = line_count + 1 table.insert(lines, line) end end elseif content_item.type == "diff" and content_item.oldText ~= nil and content_item.newText ~= nil and content_item.oldText ~= vim.NIL and content_item.newText ~= vim.NIL then local relative_path = Utils.relative_path(content_item.path) table.insert(lines, Line:new({ { decoration }, { "Path: " .. relative_path } })) local lines_ = M.get_diff_lines(content_item.oldText, content_item.newText, decoration, truncate) lines = vim.list_extend(lines, lines_) end end end return lines end ---@param message avante.HistoryMessage ---@return string tool_name ---@return string | nil error function M.get_tool_display_name(message) local content = message.message.content if type(content) ~= "table" then return "", "expected message content to be a table" end ---@cast content AvanteLLMMessageContentItem[] if not islist(content) then return "", "expected message content to be a list" end local item = message.message.content[1] local native_tool_name = item.name if native_tool_name == "other" and message.acp_tool_call then native_tool_name = message.acp_tool_call.title or "Other" end if message.acp_tool_call and message.acp_tool_call.title then native_tool_name = message.acp_tool_call.title end local tool_name = native_tool_name if message.displayed_tool_name then tool_name = message.displayed_tool_name else local param if item.input and type(item.input) == "table" then local path if type(item.input.path) == "string" then path = item.input.path end if type(item.input.rel_path) == "string" then path = item.input.rel_path end if type(item.input.filepath) == "string" then path = item.input.filepath end if type(item.input.file_path) == "string" then path = item.input.file_path end if type(item.input.query) == "string" then param = item.input.query end if type(item.input.pattern) == "string" then param = item.input.pattern end if type(item.input.command) == "string" then param = item.input.command local pieces = vim.split(param, "\n") if #pieces > 1 then param = pieces[1] .. "..." end end if native_tool_name == "execute" and not param then if message.acp_tool_call and message.acp_tool_call.title then param = message.acp_tool_call.title end end if not param and path then local relative_path = Utils.relative_path(path) param = relative_path end end if not param and message.acp_tool_call then if message.acp_tool_call.locations then for _, location in ipairs(message.acp_tool_call.locations) do if location.path then local relative_path = Utils.relative_path(location.path) param = relative_path break end end end end if not param and message.acp_tool_call and message.acp_tool_call.rawInput and message.acp_tool_call.rawInput.command then param = message.acp_tool_call.rawInput.command pcall(function() local project_root = Utils.root.get() param = param:gsub(project_root .. "/?", "") end) end if param then tool_name = native_tool_name .. "(" .. vim.inspect(param) .. ")" end end ---@cast tool_name string return tool_name, nil end ---Converts a tool invocation into format suitable for UI ---@param item AvanteLLMMessageContentItem ---@param message avante.HistoryMessage ---@param messages avante.HistoryMessage[] ---@param expanded boolean | nil ---@return avante.ui.Line[] local function tool_to_lines(item, message, messages, expanded) -- local logs = message.tool_use_logs local lines = {} local tool_name, error = M.get_tool_display_name(message) if error then table.insert(lines, Line:new({ { "❌ " }, { error } })) return lines end local rest_input_text_lines = {} local result = Helpers.get_tool_result(item.id, messages) local state if not result then state = "generating" elseif result.is_error then state = "failed" else state = "succeeded" end table.insert( lines, Line:new({ { "╭─ " }, { " " .. tool_name .. " ", STATE_TO_HL[state] }, { " " .. state }, }) ) -- if logs then vim.list_extend(lines, tool_logs_to_lines(item.name, logs)) end local decoration = "│ " if rest_input_text_lines and #rest_input_text_lines > 0 then local lines_ = text_to_lines(table.concat(rest_input_text_lines, "\n"), decoration) local line_count = 0 for idx, line in ipairs(lines_) do if not expanded and line_count > 3 then table.insert( lines, Line:new({ { decoration }, { string.format("... (Input truncated, remaining %d lines not shown)", #lines_ - idx + 1), Highlights.AVANTE_COMMENT_FG, }, }) ) break end line_count = line_count + 1 table.insert(lines, line) end table.insert(lines, Line:new({ { decoration }, { "" } })) end local add_diff_lines = false if item.input and type(item.input) == "table" then if type(item.input.old_str) == "string" and type(item.input.new_str) == "string" then local diff_lines = M.get_diff_lines(item.input.old_str, item.input.new_str, decoration, not expanded) add_diff_lines = true vim.list_extend(lines, diff_lines) end end if not add_diff_lines and message.acp_tool_call and message.acp_tool_call.rawInput and message.acp_tool_call.rawInput.oldString then local diff_lines = M.get_diff_lines( message.acp_tool_call.rawInput.oldString, message.acp_tool_call.rawInput.newString, decoration, not expanded ) vim.list_extend(lines, diff_lines) end if message.acp_tool_call and message.acp_tool_call.rawOutput and message.acp_tool_call.rawOutput.metadata and message.acp_tool_call.rawOutput.metadata.preview then local preview = message.acp_tool_call.rawOutput.metadata.preview if preview then local content_lines = M.get_content_lines(preview, decoration, not expanded) vim.list_extend(lines, content_lines) end else if message.acp_tool_call and message.acp_tool_call.content then local content = message.acp_tool_call.content if content then local content_lines = M.get_content_lines(content, decoration, not expanded) vim.list_extend(lines, content_lines) end else if result and result.content then local result_content = result.content if result_content then local content_lines = M.get_content_lines(result_content, decoration, not expanded) vim.list_extend(lines, content_lines) end end end end if #lines <= 1 then if state == "generating" then table.insert(lines, Line:new({ { decoration }, { "...", Highlights.AVANTE_COMMENT_FG } })) else table.insert(lines, Line:new({ { decoration }, { "completed" } })) end end --- remove last empty lines while #lines > 0 and lines[#lines].sections[2] and lines[#lines].sections[2][1] == "" do table.remove(lines, #lines) end local last_line = lines[#lines] last_line.sections[1][1] = "╰─ " return lines end ---Converts a message item into representation suitable for UI ---@param item AvanteLLMMessageContentItem ---@param message avante.HistoryMessage ---@param messages avante.HistoryMessage[] ---@param expanded boolean | nil ---@return avante.ui.Line[] local function message_content_item_to_lines(item, message, messages, expanded) if type(item) == "string" then return text_to_lines(item) elseif type(item) == "table" then if item.type == "thinking" or item.type == "redacted_thinking" then return thinking_to_lines(item) elseif item.type == "text" then return text_to_lines(item.text) elseif item.type == "image" then return { Line:new({ { "![image](" .. item.source.media_type .. ": " .. item.source.data .. ")" } }) } elseif item.type == "tool_use" and item.name then local ok, llm_tool = pcall(require, "avante.llm_tools." .. item.name) if ok then local tool_result_message = Helpers.get_tool_result_message(item.id, messages) ---@cast llm_tool AvanteLLMTool if llm_tool.on_render then return llm_tool.on_render(item.input, { logs = message.tool_use_logs, state = message.state, store = message.tool_use_store, result_message = tool_result_message, }) end end local lines = tool_to_lines(item, message, messages, expanded) if message.tool_use_log_lines then lines = vim.list_extend(lines, message.tool_use_log_lines) end return lines end end return {} end ---Converts a message into representation suitable for UI ---@param message avante.HistoryMessage ---@param messages avante.HistoryMessage[] ---@param expanded boolean | nil ---@return avante.ui.Line[] function M.message_to_lines(message, messages, expanded) if message.displayed_content then return text_to_lines(message.displayed_content) end local content = message.message.content if type(content) == "string" then return text_to_lines(content) end if islist(content) then local lines = {} for _, item in ipairs(content) do local item_lines = message_content_item_to_lines(item, message, messages, expanded) lines = vim.list_extend(lines, item_lines) end return lines end return {} end ---Converts a message item into text representation ---@param item AvanteLLMMessageContentItem ---@param message avante.HistoryMessage ---@param messages avante.HistoryMessage[] ---@return string local function message_content_item_to_text(item, message, messages) local lines = message_content_item_to_lines(item, message, messages) return vim.iter(lines):map(function(line) return tostring(line) end):join("\n") end ---Converts a message into text representation ---@param message avante.HistoryMessage ---@param messages avante.HistoryMessage[] ---@return string function M.message_to_text(message, messages) local content = message.message.content if type(content) == "string" then return content end if islist(content) then return vim .iter(content) :map(function(item) return message_content_item_to_text(item, message, messages) end) :filter(function(text) return text ~= "" end) :join("\n") end return "" end return M ================================================ FILE: lua/avante/history_selector.lua ================================================ local History = require("avante.history") local Utils = require("avante.utils") local Path = require("avante.path") local Config = require("avante.config") local Selector = require("avante.ui.selector") ---@class avante.HistorySelector local M = {} ---@param history avante.ChatHistory ---@return table? local function to_selector_item(history) local messages = History.get_history_messages(history) local timestamp = #messages > 0 and messages[#messages].timestamp or history.timestamp local name = history.title .. " - " .. timestamp .. " (" .. #messages .. ")" name = name:gsub("\n", "\\n") return { name = name, filename = history.filename, } end ---@param bufnr integer ---@param cb fun(filename: string) function M.open(bufnr, cb) local selector_items = {} local histories = Path.history.list(bufnr) for _, history in ipairs(histories) do table.insert(selector_items, to_selector_item(history)) end if #selector_items == 0 then Utils.warn("No history items found.") return end local current_selector -- To be able to close it from the keymap current_selector = Selector:new({ provider = Config.selector.provider, -- This should be 'native' for the current setup title = "Avante History (Select, then choose action)", -- Updated title items = vim .iter(selector_items) :map( function(item) return { id = item.filename, title = item.name, } end ) :totable(), on_select = function(item_ids) if not item_ids then return end if #item_ids == 0 then return end cb(item_ids[1]) end, get_preview_content = function(item_id) local history = Path.history.load(vim.api.nvim_get_current_buf(), item_id) local Sidebar = require("avante.sidebar") local content = Sidebar.render_history_content(history) return content, "markdown" end, on_delete_item = function(item_id_to_delete) if not item_id_to_delete then Utils.warn("No item ID provided for deletion.") return end Path.history.delete(bufnr, item_id_to_delete) -- bufnr from M.open's scope end, on_open = function() M.open(bufnr, cb) end, }) current_selector:open() end return M ================================================ FILE: lua/avante/html2md.lua ================================================ ---@class AvanteHtml2Md ---@field fetch_md fun(url: string): string local _html2md_lib = nil local M = {} ---@return AvanteHtml2Md|nil function M._init_html2md_lib() if _html2md_lib ~= nil then return _html2md_lib end local ok, core = pcall(require, "avante_html2md") if not ok then return nil end _html2md_lib = core return _html2md_lib end function M.setup() vim.defer_fn(M._init_html2md_lib, 1000) end function M.fetch_md(url) local html2md_lib = M._init_html2md_lib() if not html2md_lib then return nil, "Failed to load avante_html2md" end local ok, res = pcall(html2md_lib.fetch_md, url) if not ok then return nil, res end return res, nil end return M ================================================ FILE: lua/avante/init.lua ================================================ local api = vim.api local Utils = require("avante.utils") local Sidebar = require("avante.sidebar") local Selection = require("avante.selection") local Suggestion = require("avante.suggestion") local Config = require("avante.config") local Diff = require("avante.diff") local RagService = require("avante.rag_service") ---@class Avante local M = { ---@type avante.Sidebar[] we use this to track chat command across tabs sidebars = {}, ---@type avante.Selection[] selections = {}, ---@type avante.Suggestion[] suggestions = {}, ---@type {sidebar?: avante.Sidebar, selection?: avante.Selection, suggestion?: avante.Suggestion} current = { sidebar = nil, selection = nil, suggestion = nil }, ---@type table Global ACP client registry for cleanup on exit acp_clients = {}, } M.did_setup = false -- ACP Client Management Functions ---Register an ACP client for cleanup on exit ---@param client_id string Unique identifier for the client ---@param client any ACP client instance function M.register_acp_client(client_id, client) M.acp_clients[client_id] = client Utils.debug("Registered ACP client: " .. client_id) end ---Unregister an ACP client ---@param client_id string Unique identifier for the client function M.unregister_acp_client(client_id) M.acp_clients[client_id] = nil Utils.debug("Unregistered ACP client: " .. client_id) end ---Cleanup all registered ACP clients function M.cleanup_all_acp_clients() Utils.debug("Cleaning up all ACP clients...") for client_id, client in pairs(M.acp_clients) do if client and client.stop then Utils.debug("Stopping ACP client: " .. client_id) pcall(function() client:stop() end) end end M.acp_clients = {} Utils.debug("All ACP clients cleaned up") end local H = {} function H.load_path() local ok, LazyConfig = pcall(require, "lazy.core.config") if ok then Utils.debug("LazyConfig loaded") local name = "avante.nvim" local function load_path() require("avante_lib").load() end if LazyConfig.plugins[name] and LazyConfig.plugins[name]._.loaded then vim.schedule(load_path) else api.nvim_create_autocmd("User", { pattern = "LazyLoad", callback = function(event) if event.data == name then load_path() return true end end, }) end api.nvim_create_autocmd("User", { pattern = "VeryLazy", callback = load_path, }) else require("avante_lib").load() end end function H.keymaps() vim.keymap.set({ "n", "v" }, "(AvanteAsk)", function() require("avante.api").ask() end, { noremap = true }) vim.keymap.set( { "n", "v" }, "(AvanteAskNew)", function() require("avante.api").ask({ new_chat = true }) end, { noremap = true } ) vim.keymap.set( { "n", "v" }, "(AvanteChat)", function() require("avante.api").ask({ ask = false }) end, { noremap = true } ) vim.keymap.set("v", "(AvanteEdit)", function() require("avante.api").edit() end, { noremap = true }) vim.keymap.set("n", "(AvanteRefresh)", function() require("avante.api").refresh() end, { noremap = true }) vim.keymap.set("n", "(AvanteFocus)", function() require("avante.api").focus() end, { noremap = true }) vim.keymap.set("n", "(AvanteBuild)", function() require("avante.api").build() end, { noremap = true }) vim.keymap.set("n", "(AvanteToggle)", function() M.toggle() end, { noremap = true }) vim.keymap.set("n", "(AvanteToggleDebug)", function() M.toggle.debug() end) vim.keymap.set("n", "(AvanteToggleSelection)", function() M.toggle.selection() end) vim.keymap.set("n", "(AvanteToggleSuggestion)", function() M.toggle.suggestion() end) vim.keymap.set({ "n", "v" }, "(AvanteConflictOurs)", function() Diff.choose("ours") end) vim.keymap.set({ "n", "v" }, "(AvanteConflictBoth)", function() Diff.choose("both") end) vim.keymap.set({ "n", "v" }, "(AvanteConflictTheirs)", function() Diff.choose("theirs") end) vim.keymap.set({ "n", "v" }, "(AvanteConflictAllTheirs)", function() Diff.choose("all_theirs") end) vim.keymap.set({ "n", "v" }, "(AvanteConflictCursor)", function() Diff.choose("cursor") end) vim.keymap.set("n", "(AvanteConflictNextConflict)", function() Diff.find_next("ours") end) vim.keymap.set("n", "(AvanteConflictPrevConflict)", function() Diff.find_prev("ours") end) vim.keymap.set("n", "(AvanteSelectModel)", function() require("avante.api").select_model() end) if Config.behaviour.auto_set_keymaps then Utils.safe_keymap_set( { "n", "v" }, Config.mappings.ask, function() require("avante.api").ask() end, { desc = "avante: ask" } ) Utils.safe_keymap_set( { "n", "v" }, Config.mappings.zen_mode, function() require("avante.api").zen_mode() end, { desc = "avante: toggle Zen Mode" } ) Utils.safe_keymap_set( { "n", "v" }, Config.mappings.new_ask, function() require("avante.api").ask({ new_chat = true }) end, { desc = "avante: create new ask" } ) Utils.safe_keymap_set( "v", Config.mappings.edit, function() require("avante.api").edit() end, { desc = "avante: edit" } ) Utils.safe_keymap_set( "n", Config.mappings.stop, function() require("avante.api").stop() end, { desc = "avante: stop" } ) Utils.safe_keymap_set( "n", Config.mappings.refresh, function() require("avante.api").refresh() end, { desc = "avante: refresh" } ) Utils.safe_keymap_set( "n", Config.mappings.focus, function() require("avante.api").focus() end, { desc = "avante: focus" } ) Utils.safe_keymap_set("n", Config.mappings.toggle.default, function() M.toggle() end, { desc = "avante: toggle" }) Utils.safe_keymap_set( "n", Config.mappings.toggle.debug, function() M.toggle.debug() end, { desc = "avante: toggle debug" } ) Utils.safe_keymap_set( "n", Config.mappings.toggle.selection, function() M.toggle.hint() end, { desc = "avante: toggle selection" } ) Utils.safe_keymap_set( "n", Config.mappings.toggle.suggestion, function() M.toggle.suggestion() end, { desc = "avante: toggle suggestion" } ) Utils.safe_keymap_set("n", Config.mappings.toggle.repomap, function() require("avante.repo_map").show() end, { desc = "avante: display repo map", noremap = true, silent = true, }) Utils.safe_keymap_set( "n", Config.mappings.select_model, function() require("avante.api").select_model() end, { desc = "avante: select model" } ) Utils.safe_keymap_set( "n", Config.mappings.select_history, function() require("avante.api").select_history() end, { desc = "avante: select history" } ) Utils.safe_keymap_set( "n", Config.mappings.files.add_all_buffers, function() require("avante.api").add_buffer_files() end, { desc = "avante: add all open buffers" } ) end if Config.behaviour.auto_suggestions then Utils.safe_keymap_set("i", Config.mappings.suggestion.accept, function() local _, _, sg = M.get() sg:accept() end, { desc = "avante: accept suggestion", noremap = true, silent = true, }) Utils.safe_keymap_set("i", Config.mappings.suggestion.dismiss, function() local _, _, sg = M.get() if sg:is_visible() then sg:dismiss() end end, { desc = "avante: dismiss suggestion", noremap = true, silent = true, }) Utils.safe_keymap_set("i", Config.mappings.suggestion.next, function() local _, _, sg = M.get() sg:next() end, { desc = "avante: next suggestion", noremap = true, silent = true, }) Utils.safe_keymap_set("i", Config.mappings.suggestion.prev, function() local _, _, sg = M.get() sg:prev() end, { desc = "avante: previous suggestion", noremap = true, silent = true, }) end end ---@class ApiCaller ---@operator call(...): any function H.api(fun) return setmetatable({ api = true }, { __call = function(...) return fun(...) end, }) --[[@as ApiCaller]] end function H.signs() vim.fn.sign_define("AvanteInputPromptSign", { text = Config.windows.input.prefix }) end H.augroup = api.nvim_create_augroup("avante_autocmds", { clear = true }) function H.autocmds() api.nvim_create_autocmd("TabEnter", { group = H.augroup, pattern = "*", once = true, callback = function(ev) local tab = tonumber(ev.file) M._init(tab or api.nvim_get_current_tabpage()) if Config.selection.enabled and not M.current.selection.did_setup then M.current.selection:setup_autocmds() end end, }) api.nvim_create_autocmd("VimResized", { group = H.augroup, callback = function() local sidebar = M.get() if not sidebar then return end if not sidebar:is_open() then return end sidebar:resize() end, }) api.nvim_create_autocmd("QuitPre", { group = H.augroup, callback = function() local current_buf = vim.api.nvim_get_current_buf() if Utils.is_sidebar_buffer(current_buf) then return end local non_sidebar_wins = 0 local sidebar_wins = {} for _, win in ipairs(vim.api.nvim_list_wins()) do if vim.api.nvim_win_is_valid(win) then local win_buf = vim.api.nvim_win_get_buf(win) if Utils.is_sidebar_buffer(win_buf) then table.insert(sidebar_wins, win) else non_sidebar_wins = non_sidebar_wins + 1 end end end if non_sidebar_wins <= 1 then for _, win in ipairs(sidebar_wins) do pcall(vim.api.nvim_win_close, win, false) end end end, nested = true, }) api.nvim_create_autocmd("TabClosed", { group = H.augroup, pattern = "*", callback = function(ev) local tab = tonumber(ev.file) local s = M.sidebars[tab] local sl = M.selections[tab] if s then s:reset() end if sl then sl:delete_autocmds() end if tab ~= nil then M.sidebars[tab] = nil end end, }) -- Fix Issue #2749: Cleanup ACP processes on Neovim exit api.nvim_create_autocmd("VimLeavePre", { group = H.augroup, desc = "Cleanup all ACP processes before Neovim exits", callback = function() Utils.debug("VimLeavePre: Starting ACP cleanup...") -- Cancel any inflight requests first local ok, Llm = pcall(require, "avante.llm") if ok then pcall(function() Llm.cancel_inflight_request() end) end -- Cleanup all registered ACP clients M.cleanup_all_acp_clients() Utils.debug("VimLeavePre: ACP cleanup completed") end, }) vim.schedule(function() M._init(api.nvim_get_current_tabpage()) if Config.selection.enabled then M.current.selection:setup_autocmds() end end) local function setup_colors() Utils.debug("Setting up avante colors") require("avante.highlights").setup() end api.nvim_create_autocmd("ColorSchemePre", { group = H.augroup, callback = function() vim.schedule(function() setup_colors() end) end, }) api.nvim_create_autocmd("ColorScheme", { group = H.augroup, callback = function() vim.schedule(function() setup_colors() end) end, }) -- automatically setup Avante filetype to markdown vim.treesitter.language.register("markdown", "Avante") vim.filetype.add({ extension = { ["avanterules"] = "jinja", }, pattern = { ["%.avanterules%.[%w_.-]+"] = "jinja", }, }) end ---@param current boolean? false to disable setting current, otherwise use this to track across tabs. ---@return avante.Sidebar, avante.Selection, avante.Suggestion function M.get(current) local tab = api.nvim_get_current_tabpage() local sidebar = M.sidebars[tab] local selection = M.selections[tab] local suggestion = M.suggestions[tab] if current ~= false then M.current.sidebar = sidebar M.current.selection = selection M.current.suggestion = suggestion end return sidebar, selection, suggestion end ---@param id integer function M._init(id) local sidebar = M.sidebars[id] local selection = M.selections[id] local suggestion = M.suggestions[id] if not sidebar then sidebar = Sidebar:new(id) M.sidebars[id] = sidebar end if not selection then selection = Selection:new(id) M.selections[id] = selection end if not suggestion then suggestion = Suggestion:new(id) M.suggestions[id] = suggestion end M.current = { sidebar = sidebar, selection = selection, suggestion = suggestion } return M end M.toggle = { api = true } ---@param opts? AskOptions function M.toggle_sidebar(opts) opts = opts or {} if opts.ask == nil then opts.ask = true end local sidebar = M.get() if not sidebar then M._init(api.nvim_get_current_tabpage()) ---@cast opts SidebarOpenOptions M.current.sidebar:open(opts) return true end return sidebar:toggle(opts) end function M.is_sidebar_open() local sidebar = M.get() if not sidebar then return false end return sidebar:is_open() end ---@param opts? AskOptions function M.open_sidebar(opts) opts = opts or {} if opts.ask == nil then opts.ask = true end local sidebar = M.get() if not sidebar then M._init(api.nvim_get_current_tabpage()) end ---@cast opts SidebarOpenOptions M.current.sidebar:open(opts) end function M.close_sidebar() local sidebar = M.get() if not sidebar then return end sidebar:close() end M.toggle.debug = H.api(Utils.toggle_wrap({ name = "debug", get = function() return Config.debug end, set = function(state) Config.override({ debug = state }) end, })) M.toggle.selection = H.api(Utils.toggle_wrap({ name = "selection", get = function() return Config.selection.enabled end, set = function(state) Config.override({ selection = { enabled = state } }) end, })) M.toggle.suggestion = H.api(Utils.toggle_wrap({ name = "suggestion", get = function() return Config.behaviour.auto_suggestions end, set = function(state) Config.override({ behaviour = { auto_suggestions = state } }) local _, _, sg = M.get() if state ~= false then if sg then sg:setup_autocmds() end H.keymaps() else if sg then sg:delete_autocmds() end end end, })) setmetatable(M.toggle, { __index = M.toggle, __call = function() M.toggle_sidebar() end, }) M.slash_commands_id = nil ---@param opts? avante.Config function M.setup(opts) ---PERF: we can still allow running require("avante").setup() multiple times to override config if users wish to ---but most of the other functionality will only be called once from lazy.nvim Config.setup(opts) if M.did_setup then return end H.load_path() require("avante.html2md").setup() require("avante.repo_map").setup() require("avante.path").setup() require("avante.highlights").setup() require("avante.diff").setup() require("avante.providers").setup() require("avante.clipboard").setup() -- setup helpers H.autocmds() H.keymaps() H.signs() M.did_setup = true local function run_rag_service() local started_at = os.time() local add_resource_with_delay local function add_resource() local is_ready = RagService.is_ready() if not is_ready then local elapsed = os.time() - started_at if elapsed > 1000 * 60 * 15 then Utils.warn("Rag Service is not ready, giving up") return end add_resource_with_delay() return end vim.defer_fn(function() Utils.info("Adding project root to Rag Service ...") local uri = "file://" .. Utils.get_project_root() if uri:sub(-1) ~= "/" then uri = uri .. "/" end RagService.add_resource(uri) end, 5000) end add_resource_with_delay = function() vim.defer_fn(function() add_resource() end, 5000) end vim.schedule(function() Utils.info("Starting Rag Service ...") RagService.launch_rag_service(add_resource_with_delay) end) end if Config.rag_service.enabled then run_rag_service() end local has_cmp, cmp = pcall(require, "cmp") if has_cmp then M.slash_commands_id = cmp.register_source("avante_commands", require("cmp_avante.commands"):new()) cmp.register_source("avante_mentions", require("cmp_avante.mentions"):new(Utils.get_chat_mentions)) cmp.register_source("avante_prompt_mentions", require("cmp_avante.mentions"):new(Utils.get_mentions)) cmp.register_source("avante_shortcuts", require("cmp_avante.shortcuts"):new()) cmp.setup.filetype({ "AvanteInput" }, { enabled = true, sources = { { name = "avante_commands" }, { name = "avante_mentions" }, { name = "avante_shortcuts" }, { name = "avante_files" }, }, }) cmp.setup.filetype({ "AvantePromptInput" }, { enabled = true, sources = { { name = "avante_prompt_mentions" }, }, }) end end return M ================================================ FILE: lua/avante/libs/ReAct_parser.lua ================================================ local M = {} -- Helper function to parse a parameter tag like value -- Returns {name = string, value = string, next_pos = number} or nil if incomplete local function parse_parameter(text, start_pos) local i = start_pos local len = #text -- Skip whitespace while i <= len and string.match(string.sub(text, i, i), "%s") do i = i + 1 end if i > len or string.sub(text, i, i) ~= "<" then return nil end -- Find parameter name local param_name_start = i + 1 local param_name_end = string.find(text, ">", param_name_start) if not param_name_end then return nil -- Incomplete parameter tag end local param_name = string.sub(text, param_name_start, param_name_end - 1) i = param_name_end + 1 -- Find parameter value (everything until closing tag) local param_close_tag = "" local param_value_start = i local param_close_pos = string.find(text, param_close_tag, i, true) if not param_close_pos then -- Incomplete parameter value, return what we have local param_value = string.sub(text, param_value_start) return { name = param_name, value = param_value, next_pos = len + 1, } end local param_value = string.sub(text, param_value_start, param_close_pos - 1) i = param_close_pos + #param_close_tag return { name = param_name, value = param_value, next_pos = i, } end -- Helper function to parse tool use content starting after -- Returns {content = ToolUseContent, next_pos = number} or nil if incomplete local function parse_tool_use(text, start_pos) local i = start_pos local len = #text -- Skip whitespace while i <= len and string.match(string.sub(text, i, i), "%s") do i = i + 1 end if i > len then return nil -- No content after end -- Check if we have opening tag for tool name if string.sub(text, i, i) ~= "<" then return nil -- Invalid format end -- Find tool name local tool_name_start = i + 1 local tool_name_end = string.find(text, ">", tool_name_start) if not tool_name_end then return nil -- Incomplete tool name tag end local tool_name = string.sub(text, tool_name_start, tool_name_end - 1) i = tool_name_end + 1 -- Parse tool parameters local tool_input = {} local partial = false -- Look for tool closing tag or local tool_close_tag = "" local tool_use_close_tag = "" while i <= len do -- Skip whitespace before checking for closing tags while i <= len and string.match(string.sub(text, i, i), "%s") do i = i + 1 end if i > len then partial = true break end -- Check for tool closing tag first local tool_close_pos = string.find(text, tool_close_tag, i, true) local tool_use_close_pos = string.find(text, tool_use_close_tag, i, true) if tool_close_pos and tool_close_pos == i then -- Found tool closing tag i = tool_close_pos + #tool_close_tag -- Skip whitespace while i <= len and string.match(string.sub(text, i, i), "%s") do i = i + 1 end -- Check for if i <= len and string.find(text, tool_use_close_tag, i, true) == i then i = i + #tool_use_close_tag partial = false else partial = true end break elseif tool_use_close_pos and tool_use_close_pos == i then -- Found without tool closing tag (malformed, but handle it) i = tool_use_close_pos + #tool_use_close_tag partial = false break else -- Parse parameter tag local param_result = parse_parameter(text, i) if param_result then tool_input[param_result.name] = param_result.value i = param_result.next_pos else -- Incomplete parameter, mark as partial partial = true break end end end -- If we reached end of text without proper closing, it's partial if i > len then partial = true end return { content = { type = "tool_use", tool_name = tool_name, tool_input = tool_input, partial = partial, }, next_pos = i, } end --- Parse the text into a list of TextContent and ToolUseContent --- The text is a string. --- For example: --- parse("Hello, world!") --- returns --- { --- { --- type = "text", --- text = "Hello, world!", --- partial = false, --- }, --- } --- --- parse("Hello, world! I am a tool.path/to/file.txtfoo") --- returns --- { --- { --- type = "text", --- text = "Hello, world! I am a tool.", --- partial = false, --- }, --- { --- type = "tool_use", --- tool_name = "write", --- tool_input = { --- path = "path/to/file.txt", --- content = "foo", --- }, --- partial = false, --- }, --- } --- --- parse("Hello, world! I am a tool.path/to/file.txtfooI am another tool.path/to/file.txtbarhello") --- returns --- { --- { --- type = "text", --- text = "Hello, world! I am a tool.", --- partial = false, --- }, --- { --- type = "tool_use", --- tool_name = "write", --- tool_input = { --- path = "path/to/file.txt", --- content = "foo", --- }, --- partial = false, --- }, --- { --- type = "text", --- text = "I am another tool.", --- partial = false, --- }, --- { --- type = "tool_use", --- tool_name = "write", --- tool_input = { --- path = "path/to/file.txt", --- content = "bar", --- }, --- partial = false, --- }, --- { --- type = "text", --- text = "hello", --- partial = false, --- }, --- } --- --- parse("Hello, world! I am a tool.") --- returns --- { --- { --- type = "text", --- text = "Hello, world! I am a tool.", --- partial = false, --- }, --- { --- type = "tool_use", --- tool_name = "write", --- tool_input = {}, --- partial = true, --- }, --- } --- --- parse("Hello, world! I am a tool.path/to/file.txt") --- returns --- { --- { --- type = "text", --- text = "Hello, world! I am a tool.", --- partial = false, --- }, --- { --- type = "tool_use", --- tool_name = "write", --- tool_input = { --- path = "path/to/file.txt", --- }, --- partial = true, --- }, --- } --- --- parse("Hello, world! I am a tool.path/to/file.txtfoo bar") --- returns --- { --- { --- type = "text", --- text = "Hello, world! I am a tool.", --- partial = false, --- }, --- { --- type = "tool_use", --- tool_name = "write", --- tool_input = { --- path = "path/to/file.txt", --- content = "foo bar", --- }, --- partial = true, --- }, --- } --- --- parse("Hello, world! I am a tool.path/to/file.txtfoo bar") --- returns --- { --- { --- type = "text", --- text = "Hello, world! I am a tool.path/to/file.txtfoo bar", --- partial = false, --- } --- } --- --- parse("Hello, world! I am a tool.path/to/file.txt") --- returns --- { --- { --- type = "text", --- text = "Hello, world! I am a tool.", --- partial = false, --- }, --- { --- type = "tool_use", --- tool_name = "write", --- tool_input = { --- path = "path/to/file.txt", --- content = "", --- }, --- partial = false, --- }, --- } --- --- parse("Hello, world! I am a tool.path/to/file.txt
)} ======= setShowLogs(false)} title="Project PRD Logs" size="xl">
{logs.split('\n').join('\n\n')}
{logsLoading && }
{ if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'end' }); } }} />
{logs.length > 0 && (
)} +++++++ REPLACE ]] local fixed_diff = Utils.fix_diff(diff) assert.equals(diff, fixed_diff) end) it("should not break normal multiple diff", function() local diff = [[------- SEARCH setShowLogs(false)} title="Project PRD Logs" size="xl">
{logs.split('\n').join('\n\n')}
{logsLoading && }
{logs.length > 0 && (
)}
======= setShowLogs(false)} title="Project PRD Logs" size="xl">
{logs.split('\n').join('\n\n')}
{logsLoading && }
{ if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'end' }); } }} />
{logs.length > 0 && (
)} +++++++ REPLACE ------- SEARCH setShowLogs(false)} title="Project PRD Logs" size="xl">
======= setShowLogs(false)} title="Project PRD Logs" size="xl aaa">
+++++++ REPLACE ]] local fixed_diff = Utils.fix_diff(diff) assert.equals(diff, fixed_diff) end) it("should fix duplicated REPLACE delimiters", function() local diff = [[------- SEARCH setShowLogs(false)} title="Project PRD Logs" size="xl">
{logs.split('\n').join('\n\n')}
{logsLoading && }
{logs.length > 0 && (
)}
------- REPLACE setShowLogs(false)} title="Project PRD Logs" size="xl">
{logs.split('\n').join('\n\n')}
{logsLoading && }
{ if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'end' }); } }} />
{logs.length > 0 && (
)} ------- REPLACE ]] local expected_diff = [[------- SEARCH setShowLogs(false)} title="Project PRD Logs" size="xl">
{logs.split('\n').join('\n\n')}
{logsLoading && }
{logs.length > 0 && (
)}
======= setShowLogs(false)} title="Project PRD Logs" size="xl">
{logs.split('\n').join('\n\n')}
{logsLoading && }
{ if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'end' }); } }} />
{logs.length > 0 && (
)} +++++++ REPLACE ]] local fixed_diff = Utils.fix_diff(diff) assert.equals(expected_diff, fixed_diff) end) it("should fix the delimiter is on the same line as the content", function() local diff = [[------- // Fetch initial stages when project changes useEffect(() => { if (!subscribedProject) return; const fetchStages = async () => { try { const response = await fetch(`/api/projects/${subscribedProject}/stages`); if (response.ok) { const stagesData = await response.json(); setStages(stagesData); } } catch (error) { console.error('Failed to fetch stages:', error); } }; fetchStages(); }, [subscribedProject, forceUpdateCounter]); ======= // Fetch initial stages when project changes useEffect(() => { if (!subscribedProject) return; const fetchStages = async () => { try { // Use the correct API endpoint for stages by project UUID const response = await fetch(`/api/stages?project_uuid=${subscribedProject}`); if (response.ok) { const stagesData = await response.json(); setStages(stagesData); } } catch (error) { console.error('Failed to fetch stages:', error); } }; fetchStages(); }, [subscribedProject, forceUpdateCounter]); +++++++ REPLACE ]] local expected_diff = [[------- SEARCH // Fetch initial stages when project changes useEffect(() => { if (!subscribedProject) return; const fetchStages = async () => { try { const response = await fetch(`/api/projects/${subscribedProject}/stages`); if (response.ok) { const stagesData = await response.json(); setStages(stagesData); } } catch (error) { console.error('Failed to fetch stages:', error); } }; fetchStages(); }, [subscribedProject, forceUpdateCounter]); ======= // Fetch initial stages when project changes useEffect(() => { if (!subscribedProject) return; const fetchStages = async () => { try { // Use the correct API endpoint for stages by project UUID const response = await fetch(`/api/stages?project_uuid=${subscribedProject}`); if (response.ok) { const stagesData = await response.json(); setStages(stagesData); } } catch (error) { console.error('Failed to fetch stages:', error); } }; fetchStages(); }, [subscribedProject, forceUpdateCounter]); +++++++ REPLACE ]] local fixed_diff = Utils.fix_diff(diff) assert.equals(expected_diff, fixed_diff) end) it("should fix unified diff", function() local diff = [[--- lua/avante/sidebar.lua +++ lua/avante/sidebar.lua @@ -3099,7 +3099,7 @@ function Sidebar:create_todos_container() local history = Path.history.load(self.code.bufnr) if not history or not history.todos or #history.todos == 0 then - if self.containers.todos then self.containers.todos:unmount() end + if self.containers.todos and Utils.is_valid_container(self.containers.todos) then self.containers.todos:unmount() end self.containers.todos = nil self:adjust_layout() return @@ -3121,7 +3121,7 @@ }), position = "bottom", size = { - height = 3, + height = math.min(3, math.max(1, vim.o.lines - 5)), }, }) self.containers.todos:mount() @@ -3151,11 +3151,15 @@ self:render_header( self.containers.todos.winid, todos_buf, - Utils.icon(" ") .. "Todos" .. " (" .. done_count .. "/" .. total_count .. ")", + Utils.icon(" ") .. "Todos" .. " (" .. done_count .. "/" .. total_count .. ")", Highlights.SUBTITLE, Highlights.REVERSED_SUBTITLE ) - self:adjust_layout() + + local ok, err = pcall(function() + self:adjust_layout() + end) + if not ok then Utils.debug("Failed to adjust layout after todos creation:", err) end end function Sidebar:adjust_layout() ]] local expected_diff = [[------- SEARCH function Sidebar:create_todos_container() local history = Path.history.load(self.code.bufnr) if not history or not history.todos or #history.todos == 0 then if self.containers.todos then self.containers.todos:unmount() end self.containers.todos = nil self:adjust_layout() return ======= function Sidebar:create_todos_container() local history = Path.history.load(self.code.bufnr) if not history or not history.todos or #history.todos == 0 then if self.containers.todos and Utils.is_valid_container(self.containers.todos) then self.containers.todos:unmount() end self.containers.todos = nil self:adjust_layout() return +++++++ REPLACE ------- SEARCH }), position = "bottom", size = { height = 3, }, }) self.containers.todos:mount() ======= }), position = "bottom", size = { height = math.min(3, math.max(1, vim.o.lines - 5)), }, }) self.containers.todos:mount() +++++++ REPLACE ------- SEARCH self:render_header( self.containers.todos.winid, todos_buf, Utils.icon(" ") .. "Todos" .. " (" .. done_count .. "/" .. total_count .. ")", Highlights.SUBTITLE, Highlights.REVERSED_SUBTITLE ) self:adjust_layout() end function Sidebar:adjust_layout() ======= self:render_header( self.containers.todos.winid, todos_buf, Utils.icon(" ") .. "Todos" .. " (" .. done_count .. "/" .. total_count .. ")", Highlights.SUBTITLE, Highlights.REVERSED_SUBTITLE ) local ok, err = pcall(function() self:adjust_layout() end) if not ok then Utils.debug("Failed to adjust layout after todos creation:", err) end end function Sidebar:adjust_layout() +++++++ REPLACE]] local fixed_diff = Utils.fix_diff(diff) assert.equals(expected_diff, fixed_diff) end) it("should fix unified diff 2", function() local diff = [[ @@ -3099,7 +3099,7 @@ function Sidebar:create_todos_container() local history = Path.history.load(self.code.bufnr) if not history or not history.todos or #history.todos == 0 then - if self.containers.todos then self.containers.todos:unmount() end + if self.containers.todos and Utils.is_valid_container(self.containers.todos) then self.containers.todos:unmount() end self.containers.todos = nil self:adjust_layout() return @@ -3121,7 +3121,7 @@ }), position = "bottom", size = { - height = 3, + height = math.min(3, math.max(1, vim.o.lines - 5)), }, }) self.containers.todos:mount() @@ -3151,11 +3151,15 @@ self:render_header( self.containers.todos.winid, todos_buf, - Utils.icon(" ") .. "Todos" .. " (" .. done_count .. "/" .. total_count .. ")", + Utils.icon(" ") .. "Todos" .. " (" .. done_count .. "/" .. total_count .. ")", Highlights.SUBTITLE, Highlights.REVERSED_SUBTITLE ) - self:adjust_layout() + + local ok, err = pcall(function() + self:adjust_layout() + end) + if not ok then Utils.debug("Failed to adjust layout after todos creation:", err) end end function Sidebar:adjust_layout() ]] local expected_diff = [[------- SEARCH function Sidebar:create_todos_container() local history = Path.history.load(self.code.bufnr) if not history or not history.todos or #history.todos == 0 then if self.containers.todos then self.containers.todos:unmount() end self.containers.todos = nil self:adjust_layout() return ======= function Sidebar:create_todos_container() local history = Path.history.load(self.code.bufnr) if not history or not history.todos or #history.todos == 0 then if self.containers.todos and Utils.is_valid_container(self.containers.todos) then self.containers.todos:unmount() end self.containers.todos = nil self:adjust_layout() return +++++++ REPLACE ------- SEARCH }), position = "bottom", size = { height = 3, }, }) self.containers.todos:mount() ======= }), position = "bottom", size = { height = math.min(3, math.max(1, vim.o.lines - 5)), }, }) self.containers.todos:mount() +++++++ REPLACE ------- SEARCH self:render_header( self.containers.todos.winid, todos_buf, Utils.icon(" ") .. "Todos" .. " (" .. done_count .. "/" .. total_count .. ")", Highlights.SUBTITLE, Highlights.REVERSED_SUBTITLE ) self:adjust_layout() end function Sidebar:adjust_layout() ======= self:render_header( self.containers.todos.winid, todos_buf, Utils.icon(" ") .. "Todos" .. " (" .. done_count .. "/" .. total_count .. ")", Highlights.SUBTITLE, Highlights.REVERSED_SUBTITLE ) local ok, err = pcall(function() self:adjust_layout() end) if not ok then Utils.debug("Failed to adjust layout after todos creation:", err) end end function Sidebar:adjust_layout() +++++++ REPLACE]] local fixed_diff = Utils.fix_diff(diff) assert.equals(expected_diff, fixed_diff) end) it("should fix duplicated replace blocks", function() local diff = [[------- SEARCH useEffect(() => { if (!isExpanded || !textContentRef.current) { setShowFixedCollapseButton(false); return; } const observer = new IntersectionObserver( ([entry]) => { setShowFixedCollapseButton(!entry.isIntersecting); }, { root: null, rootMargin: '0px', threshold: 1.0, } ); const collapseButton = collapseButtonRef.current; if (collapseButton) { observer.observe(collapseButton); } return () => { if (collapseButton) { observer.unobserve(collapseButton); } }; }, [isExpanded, textContentRef.current]); ======= useEffect(() => { if (!isExpanded || !textContentRef.current) { setShowFixedCollapseButton(false); return; } // Check initial visibility of the collapse button const checkInitialVisibility = () => { const collapseButton = collapseButtonRef.current; if (collapseButton) { const rect = collapseButton.getBoundingClientRect(); const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight; setShowFixedCollapseButton(!isVisible); } }; // Small delay to ensure DOM is updated after expansion const timeoutId = setTimeout(checkInitialVisibility, 100); const observer = new IntersectionObserver( ([entry]) => { setShowFixedCollapseButton(!entry.isIntersecting); }, { root: null, rootMargin: '0px', threshold: [0, 1.0], // Check both when it starts to leave and when fully visible } ); const collapseButton = collapseButtonRef.current; if (collapseButton) { observer.observe(collapseButton); } return () => { clearTimeout(timeoutId); if (collapseButton) { observer.unobserve(collapseButton); } }; }, [isExpanded, textContentRef.current]); ======= useEffect(() => { if (!isExpanded || !textContentRef.current) { setShowFixedCollapseButton(false); return; } // Check initial visibility of the collapse button const checkInitialVisibility = () => { const collapseButton = collapseButtonRef.current; if (collapseButton) { const rect = collapseButton.getBoundingClientRect(); const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight; setShowFixedCollapseButton(!isVisible); } }; // Small delay to ensure DOM is updated after expansion const timeoutId = setTimeout(checkInitialVisibility, 100); const observer = new IntersectionObserver( ([entry]) => { setShowFixedCollapseButton(!entry.isIntersecting); }, { root: null, rootMargin: '0px', threshold: [0, 1.0], // Check both when it starts to leave and when fully visible } ); const collapseButton = collapseButtonRef.current; if (collapseButton) { observer.observe(collapseButton); } return () => { clearTimeout(timeoutId); if (collapseButton) { observer.unobserve(collapseButton); } }; }, [isExpanded, textContentRef.current]); +++++++ REPLACE ]] local expected_diff = [[------- SEARCH useEffect(() => { if (!isExpanded || !textContentRef.current) { setShowFixedCollapseButton(false); return; } const observer = new IntersectionObserver( ([entry]) => { setShowFixedCollapseButton(!entry.isIntersecting); }, { root: null, rootMargin: '0px', threshold: 1.0, } ); const collapseButton = collapseButtonRef.current; if (collapseButton) { observer.observe(collapseButton); } return () => { if (collapseButton) { observer.unobserve(collapseButton); } }; }, [isExpanded, textContentRef.current]); ======= useEffect(() => { if (!isExpanded || !textContentRef.current) { setShowFixedCollapseButton(false); return; } // Check initial visibility of the collapse button const checkInitialVisibility = () => { const collapseButton = collapseButtonRef.current; if (collapseButton) { const rect = collapseButton.getBoundingClientRect(); const isVisible = rect.top >= 0 && rect.bottom <= window.innerHeight; setShowFixedCollapseButton(!isVisible); } }; // Small delay to ensure DOM is updated after expansion const timeoutId = setTimeout(checkInitialVisibility, 100); const observer = new IntersectionObserver( ([entry]) => { setShowFixedCollapseButton(!entry.isIntersecting); }, { root: null, rootMargin: '0px', threshold: [0, 1.0], // Check both when it starts to leave and when fully visible } ); const collapseButton = collapseButtonRef.current; if (collapseButton) { observer.observe(collapseButton); } return () => { clearTimeout(timeoutId); if (collapseButton) { observer.unobserve(collapseButton); } }; }, [isExpanded, textContentRef.current]); +++++++ REPLACE]] local fixed_diff = Utils.fix_diff(diff) assert.equals(expected_diff, fixed_diff) end) end) ================================================ FILE: tests/utils/get_parent_path_spec.lua ================================================ local utils = require("avante.utils") describe("get_parent_path", function() -- Define path separator for our tests, using the same logic as in the utils module local path_sep = jit.os:find("Windows") ~= nil and "\\" or "/" it("should return the parent directory of a file path", function() local filepath = "foo" .. path_sep .. "bar" .. path_sep .. "baz.txt" local expected = "foo" .. path_sep .. "bar" assert.are.equal(expected, utils.get_parent_path(filepath)) end) it("should return the parent directory of a directory path", function() local dirpath = "foo" .. path_sep .. "bar" .. path_sep .. "baz" local expected = "foo" .. path_sep .. "bar" assert.are.equal(expected, utils.get_parent_path(dirpath)) end) it("should handle trailing separators", function() local dirpath = "foo" .. path_sep .. "bar" .. path_sep .. "baz" .. path_sep local expected = "foo" .. path_sep .. "bar" assert.are.equal(expected, utils.get_parent_path(dirpath)) end) it("should return '.' for a single file or directory", function() assert.are.equal(".", utils.get_parent_path("foo.txt")) assert.are.equal(".", utils.get_parent_path("dir")) end) it("should handle paths with multiple levels", function() local filepath = "a" .. path_sep .. "b" .. path_sep .. "c" .. path_sep .. "d" .. path_sep .. "file.txt" local expected = "a" .. path_sep .. "b" .. path_sep .. "c" .. path_sep .. "d" assert.are.equal(expected, utils.get_parent_path(filepath)) end) it("should return empty string for root directory", function() -- Root directory on Unix-like systems if path_sep == "/" then assert.are.equal("/", utils.get_parent_path("/foo")) else -- Windows uses drive letters, so parent of "C:\foo" is "C:" local winpath = "C:" .. path_sep .. "foo" assert.are.equal("C:", utils.get_parent_path(winpath)) end end) it("should return empty string for an empty string", function() assert.are.equal("", utils.get_parent_path("")) end) it("should throw an error for nil input", function() assert.has_error(function() utils.get_parent_path(nil) end, "filepath cannot be nil") end) it("should handle paths with spaces", function() local filepath = "path with spaces" .. path_sep .. "file name.txt" local expected = "path with spaces" assert.are.equal(expected, utils.get_parent_path(filepath)) end) it("should handle special characters in paths", function() local filepath = "folder-name!" .. path_sep .. "file_#$%&.txt" local expected = "folder-name!" assert.are.equal(expected, utils.get_parent_path(filepath)) end) it("should handle absolute paths", function() if path_sep == "/" then -- Unix-like paths local filepath = path_sep .. "home" .. path_sep .. "user" .. path_sep .. "file.txt" local expected = path_sep .. "home" .. path_sep .. "user" assert.are.equal(expected, utils.get_parent_path(filepath)) -- Root directory edge case assert.are.equal("", utils.get_parent_path(path_sep)) else -- Windows paths local filepath = "C:" .. path_sep .. "Users" .. path_sep .. "user" .. path_sep .. "file.txt" local expected = "C:" .. path_sep .. "Users" .. path_sep .. "user" assert.are.equal(expected, utils.get_parent_path(filepath)) end end) end) ================================================ FILE: tests/utils/init_spec.lua ================================================ local Utils = require("avante.utils") describe("Utils", function() describe("trim", function() it("should trim prefix", function() assert.equals("test", Utils.trim("prefix_test", { prefix = "prefix_" })) end) it("should trim suffix", function() assert.equals("test", Utils.trim("test_suffix", { suffix = "_suffix" })) end) it( "should trim both prefix and suffix", function() assert.equals("test", Utils.trim("prefix_test_suffix", { prefix = "prefix_", suffix = "_suffix" })) end ) it( "should return original string if no match", function() assert.equals("test", Utils.trim("test", { prefix = "xxx", suffix = "yyy" })) end ) end) describe("url_join", function() it("should join url parts correctly", function() assert.equals("http://example.com/path", Utils.url_join("http://example.com", "path")) assert.equals("http://example.com/path", Utils.url_join("http://example.com/", "/path")) assert.equals("http://example.com/path/to", Utils.url_join("http://example.com", "path", "to")) assert.equals("http://example.com/path", Utils.url_join("http://example.com/", "/path/")) end) it("should handle empty parts", function() assert.equals("http://example.com", Utils.url_join("http://example.com", "")) assert.equals("http://example.com", Utils.url_join("http://example.com", nil)) end) end) describe("is_type", function() it("should check basic types correctly", function() assert.is_true(Utils.is_type("string", "test")) assert.is_true(Utils.is_type("number", 123)) assert.is_true(Utils.is_type("boolean", true)) assert.is_true(Utils.is_type("table", {})) assert.is_true(Utils.is_type("function", function() end)) assert.is_true(Utils.is_type("nil", nil)) end) it("should check list type correctly", function() assert.is_true(Utils.is_type("list", { 1, 2, 3 })) assert.is_false(Utils.is_type("list", { a = 1, b = 2 })) end) it("should check map type correctly", function() assert.is_true(Utils.is_type("map", { a = 1, b = 2 })) assert.is_false(Utils.is_type("map", { 1, 2, 3 })) end) end) describe("get_indentation", function() it("should get correct indentation", function() assert.equals(" ", Utils.get_indentation(" test")) assert.equals("\t", Utils.get_indentation("\ttest")) assert.equals("", Utils.get_indentation("test")) end) it("should handle empty or nil input", function() assert.equals("", Utils.get_indentation("")) assert.equals("", Utils.get_indentation(nil)) end) end) describe("trime_space", function() it("should remove indentation correctly", function() assert.equals("test", Utils.trim_space(" test")) assert.equals("test", Utils.trim_space("\ttest")) assert.equals("test", Utils.trim_space("test")) end) it("should handle empty or nil input", function() assert.equals("", Utils.trim_space("")) assert.equals(nil, Utils.trim_space(nil)) end) end) describe("is_first_letter_uppercase", function() it("should detect uppercase first letter", function() assert.is_true(Utils.is_first_letter_uppercase("Test")) assert.is_true(Utils.is_first_letter_uppercase("ABC")) end) it("should detect lowercase first letter", function() assert.is_false(Utils.is_first_letter_uppercase("test")) assert.is_false(Utils.is_first_letter_uppercase("abc")) end) end) describe("extract_mentions", function() it("should extract @codebase mention", function() local result = Utils.extract_mentions("test @codebase") assert.equals("test ", result.new_content) assert.is_true(result.enable_project_context) assert.is_false(result.enable_diagnostics) end) it("should extract @diagnostics mention", function() local result = Utils.extract_mentions("test @diagnostics") assert.equals("test @diagnostics", result.new_content) assert.is_false(result.enable_project_context) assert.is_true(result.enable_diagnostics) end) it("should handle multiple mentions", function() local result = Utils.extract_mentions("test @codebase @diagnostics") assert.equals("test @diagnostics", result.new_content) assert.is_true(result.enable_project_context) assert.is_true(result.enable_diagnostics) end) end) describe("get_mentions", function() it("should return valid mentions", function() local mentions = Utils.get_mentions() assert.equals("codebase", mentions[1].command) assert.equals("diagnostics", mentions[2].command) end) end) describe("trim_think_content", function() it("should remove think content", function() local input = "this should be removed Hello World" assert.equals(" Hello World", Utils.trim_think_content(input)) end) it("The think tag that is not in the prefix should not be deleted.", function() local input = "Hello this should not be removed World" assert.equals("Hello this should not be removed World", Utils.trim_think_content(input)) end) it("should handle multiple think blocks", function() local input = "firstmiddlesecond" assert.equals("middlesecond", Utils.trim_think_content(input)) end) it("should handle empty think blocks", function() local input = "testtest" assert.equals("testtest", Utils.trim_think_content(input)) end) it("should handle empty think blocks", function() local input = "testtest" assert.equals("testtest", Utils.trim_think_content(input)) end) it("should handle input without think blocks", function() local input = "just normal text" assert.equals("just normal text", Utils.trim_think_content(input)) end) end) describe("debounce", function() it("should debounce function calls", function() local count = 0 local debounced = Utils.debounce(function() count = count + 1 end, 100) -- Call multiple times in quick succession debounced() debounced() debounced() -- Should not have executed yet assert.equals(0, count) -- Wait for debounce timeout vim.wait(200, function() return false end) -- Should have executed once assert.equals(1, count) end) it("should cancel previous timer on new calls", function() local count = 0 local debounced = Utils.debounce(function(c) count = c end, 100) -- First call debounced(1) -- Wait partial time vim.wait(50, function() return false end) -- Second call should cancel first debounced(233) -- Count should still be 0 assert.equals(0, count) -- Wait for timeout vim.wait(200, function() return false end) -- Should only execute the latest once assert.equals(233, count) end) it("should pass arguments correctly", function() local result local debounced = Utils.debounce(function(x, y) result = x + y end, 100) debounced(2, 3) -- Wait for timeout vim.wait(200, function() return false end) assert.equals(5, result) end) end) describe("fuzzy_match", function() it("should match exact lines", function() local lines = { "test", "test2", "test3", "test4" } local start_line, end_line = Utils.fuzzy_match(lines, { "test2", "test3" }) assert.equals(2, start_line) assert.equals(3, end_line) end) it("should match lines with suffix", function() local lines = { "test", "test2", "test3", "test4" } local start_line, end_line = Utils.fuzzy_match(lines, { "test2 \t", "test3" }) assert.equals(2, start_line) assert.equals(3, end_line) end) it("should match lines with space", function() local lines = { "test", "test2", "test3", "test4" } local start_line, end_line = Utils.fuzzy_match(lines, { "test2 ", " test3" }) assert.equals(2, start_line) assert.equals(3, end_line) end) end) end) ================================================ FILE: tests/utils/join_paths_spec.lua ================================================ local assert = require("luassert") local utils = require("avante.utils") describe("join_paths", function() it("should join multiple path segments with proper separator", function() local result = utils.join_paths("path", "to", "file.lua") assert.equals("path" .. utils.path_sep .. "to" .. utils.path_sep .. "file.lua", result) end) it("should handle empty path segments", function() local result = utils.join_paths("", "to", "file.lua") assert.equals("to" .. utils.path_sep .. "file.lua", result) end) it("should handle nil path segments", function() local result = utils.join_paths(nil, "to", "file.lua") assert.equals("to" .. utils.path_sep .. "file.lua", result) end) it("should handle empty path segments", function() local result = utils.join_paths("path", "", "file.lua") assert.equals("path" .. utils.path_sep .. "file.lua", result) end) it("should use absolute path when encountered", function() local absolute_path = utils.is_win() and "C:\\absolute\\path" or "/absolute/path" local result = utils.join_paths("relative", "path", absolute_path) assert.equals(absolute_path, result) end) it("should handle paths with trailing separators", function() local path_with_sep = "path" .. utils.path_sep local result = utils.join_paths(path_with_sep, "file.lua") assert.equals("path" .. utils.path_sep .. "file.lua", result) end) it("should handle no paths provided", function() local result = utils.join_paths() assert.equals(".", result) end) it("should return first path when only one path provided", function() local result = utils.join_paths("path") assert.equals("path", result) end) it("should handle path with mixed separators", function() -- This test is more relevant on Windows where both / and \ are valid separators local mixed_path = utils.is_win() and "path\\to/file" or "path/to/file" local result = utils.join_paths("base", mixed_path) -- The function should use utils.path_sep for joining assert.equals("base" .. utils.path_sep .. mixed_path, result) end) end) ================================================ FILE: tests/utils/make_relative_path_spec.lua ================================================ local assert = require("luassert") local utils = require("avante.utils") describe("make_relative_path", function() it("should remove base directory from filepath", function() local test_filepath = "/path/to/project/src/file.lua" local test_base_dir = "/path/to/project" local result = utils.make_relative_path(test_filepath, test_base_dir) assert.equals("src/file.lua", result) end) it("should handle trailing dot-slash in base_dir", function() local test_filepath = "/path/to/project/src/file.lua" local test_base_dir = "/path/to/project/." local result = utils.make_relative_path(test_filepath, test_base_dir) assert.equals("src/file.lua", result) end) it("should handle trailing dot-slash in filepath", function() local test_filepath = "/path/to/project/src/." local test_base_dir = "/path/to/project" local result = utils.make_relative_path(test_filepath, test_base_dir) assert.equals("src", result) end) it("should handle both having trailing dot-slash", function() local test_filepath = "/path/to/project/src/." local test_base_dir = "/path/to/project/." local result = utils.make_relative_path(test_filepath, test_base_dir) assert.equals("src", result) end) it("should return the filepath when base_dir is not a prefix", function() local test_filepath = "/path/to/project/src/file.lua" local test_base_dir = "/different/path" local result = utils.make_relative_path(test_filepath, test_base_dir) assert.equals("/path/to/project/src/file.lua", result) end) it("should handle identical paths", function() local test_filepath = "/path/to/project" local test_base_dir = "/path/to/project" local result = utils.make_relative_path(test_filepath, test_base_dir) assert.equals(".", result) end) it("should handle empty strings", function() local result = utils.make_relative_path("", "") assert.equals(".", result) end) it("should preserve trailing slash in filepath", function() local test_filepath = "/path/to/project/src/" local test_base_dir = "/path/to/project" local result = utils.make_relative_path(test_filepath, test_base_dir) assert.equals("src/", result) end) end) ================================================ FILE: tests/utils/streaming_json_parser_spec.lua ================================================ local StreamingJSONParser = require("avante.utils.streaming_json_parser") describe("StreamingJSONParser", function() local parser before_each(function() parser = StreamingJSONParser:new() end) describe("initialization", function() it("should create a new parser with empty state", function() assert.is_not_nil(parser) assert.equals("", parser.buffer) assert.is_not_nil(parser.state) assert.is_false(parser.state.inString) assert.is_false(parser.state.escaping) assert.is_table(parser.state.stack) assert.equals(0, #parser.state.stack) assert.is_nil(parser.state.result) assert.is_nil(parser.state.currentKey) assert.is_nil(parser.state.current) assert.is_table(parser.state.parentKeys) end) end) describe("parse", function() it("should parse a complete simple JSON object", function() local result, complete = parser:parse('{"key": "value"}') assert.is_true(complete) assert.is_table(result) assert.equals("value", result.key) end) it("should parse breaklines", function() local result, complete = parser:parse('{"key": "value\nv"}') assert.is_true(complete) assert.is_table(result) assert.equals("value\nv", result.key) end) it("should parse a complete simple JSON array", function() local result, complete = parser:parse("[1, 2, 3]") assert.is_true(complete) assert.is_table(result) assert.equals(1, result[1]) assert.equals(2, result[2]) assert.equals(3, result[3]) end) it("should handle streaming JSON in multiple chunks", function() local result1, complete1 = parser:parse('{"name": "John') assert.is_false(complete1) assert.is_table(result1) assert.equals("John", result1.name) local result2, complete2 = parser:parse('", "age": 30}') assert.is_true(complete2) assert.is_table(result2) assert.equals("John", result2.name) assert.equals(30, result2.age) end) it("should handle streaming string field", function() local result1, complete1 = parser:parse('{"name": {"first": "John') assert.is_false(complete1) assert.is_table(result1) assert.equals("John", result1.name.first) end) it("should parse nested objects", function() local json = [[{ "person": { "name": "John", "age": 30, "address": { "city": "New York", "zip": "10001" } } }]] local result, complete = parser:parse(json) assert.is_true(complete) assert.is_table(result) assert.is_table(result.person) assert.equals("John", result.person.name) assert.equals(30, result.person.age) assert.is_table(result.person.address) assert.equals("New York", result.person.address.city) assert.equals("10001", result.person.address.zip) end) it("should parse nested arrays", function() local json = [[{ "matrix": [ [1, 2, 3], [4, 5, 6], [7, 8, 9] ] }]] local result, complete = parser:parse(json) assert.is_true(complete) assert.is_table(result) assert.is_table(result.matrix) assert.equals(3, #result.matrix) assert.equals(1, result.matrix[1][1]) assert.equals(5, result.matrix[2][2]) assert.equals(9, result.matrix[3][3]) end) it("should handle boolean values", function() local result, complete = parser:parse('{"success": true, "failed": false}') assert.is_true(complete) assert.is_table(result) assert.is_true(result.success) assert.is_false(result.failed) end) it("should handle null values", function() local result, complete = parser:parse('{"value": null}') assert.is_true(complete) assert.is_table(result) assert.is_nil(result.value) end) it("should handle escaped characters in strings", function() local result, complete = parser:parse('{"text": "line1\\nline2\\t\\"quoted\\""}') assert.is_true(complete) assert.is_table(result) assert.equals('line1\nline2\t"quoted"', result.text) end) it("should handle numbers correctly", function() local result, complete = parser:parse('{"integer": 42, "float": 3.14, "negative": -10, "exponent": 1.2e3}') assert.is_true(complete) assert.is_table(result) assert.equals(42, result.integer) assert.equals(3.14, result.float) assert.equals(-10, result.negative) assert.equals(1200, result.exponent) end) it("should handle streaming complex JSON", function() local chunks = { '{"data": [{"id": 1, "info": {"name":', ' "Product A", "active": true}}, {"id": 2, ', '"info": {"name": "Product B", "active": false', '}}], "total": 2}', } local complete = false local result for _, chunk in ipairs(chunks) do result, complete = parser:parse(chunk) end assert.is_true(complete) assert.is_table(result) assert.equals(2, #result.data) assert.equals(1, result.data[1].id) assert.equals("Product A", result.data[1].info.name) assert.is_true(result.data[1].info.active) assert.equals(2, result.data[2].id) assert.equals("Product B", result.data[2].info.name) assert.is_false(result.data[2].info.active) assert.equals(2, result.total) end) it("should reset the parser state correctly", function() parser:parse('{"key": "value"}') parser:reset() assert.equals("", parser.buffer) assert.is_false(parser.state.inString) assert.is_false(parser.state.escaping) assert.is_table(parser.state.stack) assert.equals(0, #parser.state.stack) assert.is_nil(parser.state.result) assert.is_nil(parser.state.currentKey) assert.is_nil(parser.state.current) assert.is_table(parser.state.parentKeys) end) it("should return partial results for incomplete JSON", function() parser:reset() local result, complete = parser:parse('{"stream": [1, 2,') assert.is_false(complete) assert.is_table(result) assert.is_table(result.stream) assert.equals(1, result.stream[1]) assert.equals(2, result.stream[2]) -- We need exactly one item in the stack (the array) assert.equals(2, #parser.state.stack) end) it("should handle whitespace correctly", function() parser:reset() local result, complete = parser:parse('{"key1": "value1", "key2": 42}') assert.is_true(complete) assert.is_table(result) assert.equals("value1", result.key1) assert.equals(42, result.key2) end) it("should provide access to partial results during streaming", function() parser:parse('{"name": "John", "items": [') local partial = parser:getCurrentPartial() assert.is_table(partial) assert.equals("John", partial.name) assert.is_table(partial.items) parser:parse("1, 2]") local result, complete = parser:parse("}") assert.is_true(complete) assert.equals("John", result.name) assert.equals(1, result.items[1]) assert.equals(2, result.items[2]) end) end) end)