Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| feeb8b31b3 | |||
| 5c20f57eb4 | |||
| 75de3198cd | |||
| 69c8061b8e | |||
| 9687b352d5 | |||
| 4416626acf | |||
| 565e3658b5 | |||
| f8ce473877 | |||
| 0d83c6ba4d | |||
| d93fed165f | |||
|
|
5fa7d7d347 | ||
| 35b808ca1e | |||
| 3a1472670b | |||
| fe118e0885 |
129
.github/workflows/lua.yaml
vendored
129
.github/workflows/lua.yaml
vendored
@@ -2,85 +2,78 @@ name: Lua CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
branches: [master]
|
||||
paths:
|
||||
- "**/*.lua"
|
||||
- "lua/**/*.lua"
|
||||
- "plugin/**/*.lua"
|
||||
- ".stylua.toml"
|
||||
- ".luacheckrc"
|
||||
- .github/workflows/lua.yaml
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
branches: [master]
|
||||
paths:
|
||||
- "**/*.lua"
|
||||
- "lua/**/*.lua"
|
||||
- "plugin/**/*.lua"
|
||||
- ".stylua.toml"
|
||||
- ".luacheckrc"
|
||||
- .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
|
||||
format:
|
||||
name: StyLua auto-format
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
nvim_version: [ stable ]
|
||||
luals_version: [ 3.13.6 ]
|
||||
if: github.event_name == 'push'
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: rhysd/action-setup-vim@v1
|
||||
- name: Run StyLua
|
||||
uses: JohnnyMorganz/stylua-action@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
version: latest
|
||||
args: lua/ plugin/
|
||||
|
||||
- name: Commit formatting changes
|
||||
uses: stefanzweifel/git-auto-commit-action@v5
|
||||
with:
|
||||
commit_message: "style: auto-format with stylua"
|
||||
file_pattern: "lua/**/*.lua plugin/**/*.lua"
|
||||
|
||||
lint:
|
||||
name: Luacheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: leafo/gh-actions-lua@v11
|
||||
with:
|
||||
luaVersion: "5.1"
|
||||
|
||||
- uses: leafo/gh-actions-luarocks@v5
|
||||
|
||||
- name: Install luacheck
|
||||
run: luarocks install luacheck
|
||||
|
||||
- name: Run luacheck
|
||||
run: luacheck lua/ plugin/
|
||||
|
||||
health:
|
||||
name: Plugin load check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Neovim
|
||||
uses: rhysd/action-setup-vim@v1
|
||||
with:
|
||||
neovim: true
|
||||
version: ${{ matrix.nvim_version }}
|
||||
version: stable
|
||||
|
||||
- name: Typecheck
|
||||
env:
|
||||
VIMRUNTIME: /home/runner/nvim-${{ matrix.nvim_version }}/share/nvim/runtime
|
||||
- name: Verify plugin loads
|
||||
run: |
|
||||
make lua-typecheck
|
||||
nvim --headless -u NONE \
|
||||
-c "set rtp+=." \
|
||||
-c "lua require('codetyper')" \
|
||||
-c "qa!" 2>&1
|
||||
|
||||
38
.github/workflows/pre-commit.yaml
vendored
38
.github/workflows/pre-commit.yaml
vendored
@@ -1,38 +0,0 @@
|
||||
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()
|
||||
218
.github/workflows/release.yaml
vendored
218
.github/workflows/release.yaml
vendored
@@ -2,184 +2,64 @@ name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: [v\d+\.\d+\.\d+]
|
||||
tags:
|
||||
- "v[0-9]+.[0-9]+.[0-9]+*"
|
||||
|
||||
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 }}"
|
||||
|
||||
release:
|
||||
name: Create GitHub Release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # ratchet:actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
uses: battila7/get-version-action@d97fbc34ceb64d1f5d95f4dfd6dce33521ccccf5 # ratchet:battila7/get-version-action@v2
|
||||
- name: Get version from tag
|
||||
id: version
|
||||
run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Get tag message
|
||||
id: tag
|
||||
- name: Extract changelog for this version
|
||||
id: changelog
|
||||
run: |
|
||||
git fetch --depth=1 origin +refs/tags/*:refs/tags/*
|
||||
echo "message<<EOF" >> $GITHUB_OUTPUT
|
||||
echo "$(git tag -l --format='%(contents)' ${{ steps.get_version.outputs.version }})" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
version="${{ steps.version.outputs.version }}"
|
||||
# Strip leading 'v' for matching in CHANGELOG.md
|
||||
semver="${version#v}"
|
||||
|
||||
# Extract the section for this version from CHANGELOG.md
|
||||
body=$(awk -v ver="$semver" '
|
||||
/^## \[/ {
|
||||
if (found) exit
|
||||
if (index($0, ver)) found=1
|
||||
next
|
||||
}
|
||||
found { print }
|
||||
' CHANGELOG.md)
|
||||
|
||||
if [ -z "$body" ]; then
|
||||
body="Release $version"
|
||||
fi
|
||||
|
||||
# Write to file to avoid escaping issues
|
||||
echo "$body" > /tmp/release_body.md
|
||||
|
||||
- name: Generate help tags
|
||||
uses: rhysd/action-setup-vim@v1
|
||||
with:
|
||||
neovim: true
|
||||
version: stable
|
||||
|
||||
- name: Verify help tags
|
||||
run: nvim --headless -c "helptags doc/" -c "qa" 2>/dev/null || true
|
||||
|
||||
- name: Create Release
|
||||
id: create-release
|
||||
uses: ncipollo/release-action@2c591bcc8ecdcd2db72b97d6147f871fcd833ba5 # ratchet:ncipollo/release-action@v1
|
||||
uses: 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
|
||||
})
|
||||
name: "codetyper.nvim ${{ steps.version.outputs.version }}"
|
||||
tag: ${{ steps.version.outputs.version }}
|
||||
bodyFile: /tmp/release_body.md
|
||||
draft: false
|
||||
prerelease: ${{ contains(steps.version.outputs.version, '-') }}
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
31
.github/workflows/rust.yaml
vendored
31
.github/workflows/rust.yaml
vendored
@@ -1,31 +0,0 @@
|
||||
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
|
||||
49
.luacheckrc
Normal file
49
.luacheckrc
Normal file
@@ -0,0 +1,49 @@
|
||||
std = "luajit"
|
||||
|
||||
globals = {
|
||||
"vim",
|
||||
"_",
|
||||
}
|
||||
|
||||
read_globals = {
|
||||
"describe",
|
||||
"it",
|
||||
"before_each",
|
||||
"after_each",
|
||||
"assert",
|
||||
}
|
||||
|
||||
max_line_length = false
|
||||
|
||||
ignore = {
|
||||
"211", -- unused function
|
||||
"212", -- unused argument
|
||||
"213", -- unused loop variable
|
||||
"311", -- value assigned is unused
|
||||
"312", -- value of argument is unused
|
||||
"314", -- value of field is overwritten before use
|
||||
"411", -- variable redefines
|
||||
"421", -- shadowing local variable
|
||||
"431", -- shadowing upvalue
|
||||
"432", -- shadowing upvalue argument
|
||||
"511", -- unreachable code
|
||||
"542", -- empty if branch
|
||||
"631", -- max_line_length
|
||||
}
|
||||
|
||||
files["lua/codetyper/adapters/nvim/autocmds.lua"] = {
|
||||
ignore = { "111", "113", "131", "231", "241" }, -- TODO: fix undefined refs and dead stores
|
||||
}
|
||||
|
||||
files["lua/codetyper/adapters/nvim/ui/context_modal.lua"] = {
|
||||
ignore = { "113" }, -- TODO: fix undefined run_project_inspect
|
||||
}
|
||||
|
||||
files["lua/codetyper/core/scheduler/loop.lua"] = {
|
||||
ignore = { "241" }, -- mutated but never accessed
|
||||
}
|
||||
|
||||
exclude_files = {
|
||||
".luarocks",
|
||||
".luacache",
|
||||
}
|
||||
6
.stylua.toml
Normal file
6
.stylua.toml
Normal file
@@ -0,0 +1,6 @@
|
||||
column_width = 120
|
||||
line_endings = "Unix"
|
||||
indent_type = "Spaces"
|
||||
indent_width = 2
|
||||
quote_style = "AutoPreferDouble"
|
||||
call_parentheses = "Always"
|
||||
61
CHANGELOG.md
61
CHANGELOG.md
@@ -7,6 +7,63 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.2] - 2025-03-24
|
||||
|
||||
### Changed
|
||||
|
||||
- **Major module refactoring** — Migrated monolithic files to one-function-per-file architecture
|
||||
- `parser.lua` — Extracted 11 functions into `parser/` folder; deleted barrel file and 3 unused files (`get_prompt_at_cursor`, `detect_prompt_type`, `has_unclosed_prompts`)
|
||||
- `cmp/init.lua` — Extracted completion getters into individual files; moved shared source methods to `utils/cmp_source.lua`
|
||||
- `context_modal.lua` — Migrated handlers, utils, and state into `context_modal/` folder; deleted barrel file
|
||||
- `diff_review.lua` — Moved diff entry state functions to `utils/get_config.lua`; extracted remaining functions into `diff_review/` folder; deleted barrel file
|
||||
- `logs.lua` — Extracted 26 log functions into `logs/` folder plus 2 utility files (`get_timestamp`, `estimate_tokens`); deleted barrel file
|
||||
- `logs_panel.lua` — Extracted 10 panel functions into `logs_panel/` folder; deleted barrel file
|
||||
- `thinking.lua` — Extracted 10 functions into `thinking/` folder; deleted barrel file
|
||||
- `throbber.lua` — Extracted class, constructor, and methods into `throbber/` folder; deleted barrel file
|
||||
- `commands.lua` — Extracted 14 command functions into `commands/` folder; deleted barrel file
|
||||
- `autocmds.lua` — Extracted 22 functions, 4 data files, and state into `autocmds/` folder; deleted barrel file and 2 unused files (`clear`, `clear_auto_indexed`)
|
||||
- All external consumers updated to import functions directly from pure files
|
||||
- Renamed single-character and ambiguous variables to descriptive names across all refactored files
|
||||
|
||||
### Added
|
||||
|
||||
- `SECURITY.md` — Security policy and vulnerability reporting guidelines
|
||||
|
||||
## [1.0.1] - 2026-03-19
|
||||
|
||||
### Added
|
||||
|
||||
- **Version command** — `:Coder version` shows plugin version
|
||||
- **CI workflows** — Lua CI with StyLua auto-format, Luacheck, and plugin load check
|
||||
- **Release workflow** — tag-based GitHub Releases with changelog extraction
|
||||
- **`.stylua.toml`** — 2-space indentation formatting config
|
||||
- **`.luacheckrc`** — Luacheck config with proper globals and per-file ignores
|
||||
|
||||
### Changed
|
||||
|
||||
- Switched code style from tabs to 2-space indentation across all Lua files
|
||||
- Updated all documentation (`README.md`, `CHANGELOG.md`, `CONTRIBUTING.md`, `llms.txt`,
|
||||
`doc/codetyper.txt`, `doc/tags`) to match current project state
|
||||
- Removed stale references to Claude, OpenAI, Gemini, Split View, Ask Panel, and Agent Mode
|
||||
- `:Coder` with no arguments now defaults to `version` instead of `toggle`
|
||||
- Simplified Makefile — removed nonexistent test targets, added `docs` and `format-check`
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed 7 files missing `local M = {}` declaration (`params/agents/bash.lua`, `edit.lua`,
|
||||
`grep.lua`, `prompts/agents/bash.lua`, `edit.lua`, `grep.lua`, `write.lua`)
|
||||
- Fixed `cmp/init.lua` literal newline in string pattern (replaced with `\n`)
|
||||
- Fixed `prompts/agents/loop.lua` raw markdown outside string literal
|
||||
- Removed avante.nvim workflow files (Rust CI, pre-commit with Python deps) that were
|
||||
causing CI failures
|
||||
|
||||
### Removed
|
||||
|
||||
- Deleted `.github/workflows/rust.yaml` (not applicable — no Rust code)
|
||||
- Deleted `.github/workflows/pre-commit.yaml` (referenced nonexistent Python deps)
|
||||
|
||||
---
|
||||
|
||||
## [1.0.0] - 2026-03-18
|
||||
|
||||
### Added
|
||||
@@ -226,7 +283,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Fixed** — Bug fixes
|
||||
- **Security** — Vulnerability fixes
|
||||
|
||||
[Unreleased]: https://github.com/cargdev/codetyper.nvim/compare/v1.0.0...HEAD
|
||||
[Unreleased]: https://github.com/cargdev/codetyper.nvim/compare/v1.0.2...HEAD
|
||||
[1.0.2]: https://github.com/cargdev/codetyper.nvim/compare/v1.0.1...v1.0.2
|
||||
[1.0.1]: https://github.com/cargdev/codetyper.nvim/compare/v1.0.0...v1.0.1
|
||||
[1.0.0]: https://github.com/cargdev/codetyper.nvim/compare/v0.6.0...v1.0.0
|
||||
[0.6.0]: https://github.com/cargdev/codetyper.nvim/compare/v0.5.0...v0.6.0
|
||||
[0.5.0]: https://github.com/cargdev/codetyper.nvim/compare/v0.4.0...v0.5.0
|
||||
|
||||
21
SECURITY.md
Normal file
21
SECURITY.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
Use this section to tell people about which versions of your project are
|
||||
currently being supported with security updates.
|
||||
|
||||
| Version | Supported |
|
||||
| ------- | ------------------ |
|
||||
| 5.1.x | :white_check_mark: |
|
||||
| 5.0.x | :x: |
|
||||
| 4.0.x | :white_check_mark: |
|
||||
| < 4.0 | :x: |
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
Use this section to tell people how to report a vulnerability.
|
||||
|
||||
Tell them where to go, how often they can expect to get an update on a
|
||||
reported vulnerability, what to expect if the vulnerability is accepted or
|
||||
declined, etc.
|
||||
File diff suppressed because it is too large
Load Diff
252
lua/codetyper/adapters/nvim/autocmds/auto_index_file.lua
Normal file
252
lua/codetyper/adapters/nvim/autocmds/auto_index_file.lua
Normal file
@@ -0,0 +1,252 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
local autocmds_state = require("codetyper.adapters.nvim.autocmds.state")
|
||||
local is_supported_extension = require("codetyper.adapters.nvim.autocmds.is_supported_extension")
|
||||
local should_ignore_for_coder = require("codetyper.adapters.nvim.autocmds.should_ignore_for_coder")
|
||||
|
||||
--- Auto-index a file by creating/opening its coder companion
|
||||
---@param bufnr number Buffer number
|
||||
local function auto_index_file(bufnr)
|
||||
if not vim.api.nvim_buf_is_valid(bufnr) then
|
||||
return
|
||||
end
|
||||
|
||||
if autocmds_state.auto_indexed_buffers[bufnr] then
|
||||
return
|
||||
end
|
||||
|
||||
local filepath = vim.api.nvim_buf_get_name(bufnr)
|
||||
if not filepath or filepath == "" then
|
||||
return
|
||||
end
|
||||
|
||||
if utils.is_coder_file(filepath) then
|
||||
return
|
||||
end
|
||||
|
||||
local buftype = vim.bo[bufnr].buftype
|
||||
if buftype ~= "" then
|
||||
return
|
||||
end
|
||||
|
||||
local ext = vim.fn.fnamemodify(filepath, ":e")
|
||||
if ext == "" or not is_supported_extension(ext) then
|
||||
return
|
||||
end
|
||||
|
||||
if should_ignore_for_coder(filepath) then
|
||||
return
|
||||
end
|
||||
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
if config and config.auto_index == false then
|
||||
return
|
||||
end
|
||||
|
||||
autocmds_state.auto_indexed_buffers[bufnr] = true
|
||||
|
||||
local coder_path = utils.get_coder_path(filepath)
|
||||
|
||||
local coder_exists = utils.file_exists(coder_path)
|
||||
|
||||
if not coder_exists then
|
||||
local filename = vim.fn.fnamemodify(filepath, ":t")
|
||||
local file_ext = vim.fn.fnamemodify(filepath, ":e")
|
||||
|
||||
local comment_prefix = "--"
|
||||
local comment_block_start = "--[["
|
||||
local comment_block_end = "]]"
|
||||
if
|
||||
file_ext == "ts"
|
||||
or file_ext == "tsx"
|
||||
or file_ext == "js"
|
||||
or file_ext == "jsx"
|
||||
or file_ext == "java"
|
||||
or file_ext == "c"
|
||||
or file_ext == "cpp"
|
||||
or file_ext == "cs"
|
||||
or file_ext == "go"
|
||||
or file_ext == "rs"
|
||||
then
|
||||
comment_prefix = "//"
|
||||
comment_block_start = "/*"
|
||||
comment_block_end = "*/"
|
||||
elseif file_ext == "py" or file_ext == "rb" or file_ext == "yaml" or file_ext == "yml" then
|
||||
comment_prefix = "#"
|
||||
comment_block_start = '"""'
|
||||
comment_block_end = '"""'
|
||||
end
|
||||
|
||||
local content = ""
|
||||
pcall(function()
|
||||
local lines = vim.fn.readfile(filepath)
|
||||
if lines then
|
||||
content = table.concat(lines, "\n")
|
||||
end
|
||||
end)
|
||||
|
||||
local functions = extract_functions(content, file_ext)
|
||||
local classes = extract_classes(content, file_ext)
|
||||
local imports = extract_imports(content, file_ext)
|
||||
|
||||
local pseudo_code = {}
|
||||
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ═══════════════════════════════════════════════════════════"
|
||||
)
|
||||
table.insert(pseudo_code, comment_prefix .. " CODER COMPANION: " .. filename)
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ═══════════════════════════════════════════════════════════"
|
||||
)
|
||||
table.insert(pseudo_code, comment_prefix .. " This file describes the business logic and behavior of " .. filename)
|
||||
table.insert(pseudo_code, comment_prefix .. " Edit this pseudo-code to guide code generation.")
|
||||
table.insert(pseudo_code, comment_prefix .. "")
|
||||
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
table.insert(pseudo_code, comment_prefix .. " MODULE PURPOSE:")
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
table.insert(pseudo_code, comment_prefix .. " TODO: Describe what this module/file is responsible for")
|
||||
table.insert(pseudo_code, comment_prefix .. ' Example: "Handles user authentication and session management"')
|
||||
table.insert(pseudo_code, comment_prefix .. "")
|
||||
|
||||
if #imports > 0 then
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
table.insert(pseudo_code, comment_prefix .. " DEPENDENCIES:")
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
for _, imp in ipairs(imports) do
|
||||
table.insert(pseudo_code, comment_prefix .. " • " .. imp)
|
||||
end
|
||||
table.insert(pseudo_code, comment_prefix .. "")
|
||||
end
|
||||
|
||||
if #classes > 0 then
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
table.insert(pseudo_code, comment_prefix .. " CLASSES:")
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
for _, class in ipairs(classes) do
|
||||
table.insert(pseudo_code, comment_prefix .. "")
|
||||
table.insert(pseudo_code, comment_prefix .. " class " .. class.name .. ":")
|
||||
table.insert(pseudo_code, comment_prefix .. " PURPOSE: TODO - describe what this class represents")
|
||||
table.insert(pseudo_code, comment_prefix .. " RESPONSIBILITIES:")
|
||||
table.insert(pseudo_code, comment_prefix .. " - TODO: list main responsibilities")
|
||||
end
|
||||
table.insert(pseudo_code, comment_prefix .. "")
|
||||
end
|
||||
|
||||
if #functions > 0 then
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
table.insert(pseudo_code, comment_prefix .. " FUNCTIONS:")
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
for _, func in ipairs(functions) do
|
||||
table.insert(pseudo_code, comment_prefix .. "")
|
||||
table.insert(pseudo_code, comment_prefix .. " " .. func.name .. "():")
|
||||
table.insert(pseudo_code, comment_prefix .. " PURPOSE: TODO - what does this function do?")
|
||||
table.insert(pseudo_code, comment_prefix .. " INPUTS: TODO - describe parameters")
|
||||
table.insert(pseudo_code, comment_prefix .. " OUTPUTS: TODO - describe return value")
|
||||
table.insert(pseudo_code, comment_prefix .. " BEHAVIOR:")
|
||||
table.insert(pseudo_code, comment_prefix .. " - TODO: describe step-by-step logic")
|
||||
end
|
||||
table.insert(pseudo_code, comment_prefix .. "")
|
||||
end
|
||||
|
||||
if #functions == 0 and #classes == 0 then
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
table.insert(pseudo_code, comment_prefix .. " PLANNED STRUCTURE:")
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
table.insert(pseudo_code, comment_prefix .. " TODO: Describe what you want to build in this file")
|
||||
table.insert(pseudo_code, comment_prefix .. "")
|
||||
table.insert(pseudo_code, comment_prefix .. " Example pseudo-code:")
|
||||
|
||||
table.insert(pseudo_code, comment_prefix .. " Create a module that:")
|
||||
table.insert(pseudo_code, comment_prefix .. " 1. Exports a main function")
|
||||
table.insert(pseudo_code, comment_prefix .. " 2. Handles errors gracefully")
|
||||
table.insert(pseudo_code, comment_prefix .. " 3. Returns structured data")
|
||||
table.insert(pseudo_code, comment_prefix .. "")
|
||||
end
|
||||
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
table.insert(pseudo_code, comment_prefix .. " BUSINESS RULES:")
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ─────────────────────────────────────────────────────────────"
|
||||
)
|
||||
table.insert(pseudo_code, comment_prefix .. " TODO: Document any business rules, constraints, or requirements")
|
||||
table.insert(pseudo_code, comment_prefix .. " Example:")
|
||||
table.insert(pseudo_code, comment_prefix .. " - Users must be authenticated before accessing this feature")
|
||||
table.insert(pseudo_code, comment_prefix .. " - Data must be validated before saving")
|
||||
table.insert(pseudo_code, comment_prefix .. " - Errors should be logged but not exposed to users")
|
||||
table.insert(pseudo_code, comment_prefix .. "")
|
||||
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ═══════════════════════════════════════════════════════════"
|
||||
)
|
||||
table.insert(
|
||||
pseudo_code,
|
||||
comment_prefix
|
||||
.. " ═══════════════════════════════════════════════════════════"
|
||||
)
|
||||
table.insert(pseudo_code, "")
|
||||
|
||||
utils.write_file(coder_path, table.concat(pseudo_code, "\n"))
|
||||
end
|
||||
|
||||
local coder_filename = vim.fn.fnamemodify(coder_path, ":t")
|
||||
if coder_exists then
|
||||
utils.notify("Coder companion available: " .. coder_filename, vim.log.levels.DEBUG)
|
||||
else
|
||||
utils.notify("Created coder companion: " .. coder_filename, vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
|
||||
return auto_index_file
|
||||
32
lua/codetyper/adapters/nvim/autocmds/check_all_prompts.lua
Normal file
32
lua/codetyper/adapters/nvim/autocmds/check_all_prompts.lua
Normal file
@@ -0,0 +1,32 @@
|
||||
local process_single_prompt = require("codetyper.adapters.nvim.autocmds.process_single_prompt")
|
||||
|
||||
--- Check and process all closed prompts in the buffer
|
||||
local function check_all_prompts()
|
||||
local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer")
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local current_file = vim.fn.expand("%:p")
|
||||
|
||||
if current_file == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local prompts = find_prompts_in_buffer(bufnr)
|
||||
|
||||
if #prompts == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local codetyper = require("codetyper")
|
||||
local ct_config = codetyper.get_config()
|
||||
local scheduler_enabled = ct_config and ct_config.scheduler and ct_config.scheduler.enabled
|
||||
|
||||
if not scheduler_enabled then
|
||||
return
|
||||
end
|
||||
|
||||
for _, prompt in ipairs(prompts) do
|
||||
process_single_prompt(bufnr, prompt, current_file)
|
||||
end
|
||||
end
|
||||
|
||||
return check_all_prompts
|
||||
@@ -0,0 +1,35 @@
|
||||
local processed_prompts = require("codetyper.constants.constants").processed_prompts
|
||||
local get_prompt_key = require("codetyper.adapters.nvim.autocmds.get_prompt_key")
|
||||
local check_all_prompts = require("codetyper.adapters.nvim.autocmds.check_all_prompts")
|
||||
|
||||
--- Check all prompts with preference check
|
||||
--- Only processes if there are unprocessed prompts and auto_process is enabled
|
||||
local function check_all_prompts_with_preference()
|
||||
local preferences = require("codetyper.config.preferences")
|
||||
local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local prompts = find_prompts_in_buffer(bufnr)
|
||||
if #prompts == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local has_unprocessed = false
|
||||
for _, prompt in ipairs(prompts) do
|
||||
local prompt_key = get_prompt_key(bufnr, prompt)
|
||||
if not processed_prompts[prompt_key] then
|
||||
has_unprocessed = true
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if not has_unprocessed then
|
||||
return
|
||||
end
|
||||
|
||||
if auto_process then
|
||||
check_all_prompts()
|
||||
end
|
||||
end
|
||||
|
||||
return check_all_prompts_with_preference
|
||||
187
lua/codetyper/adapters/nvim/autocmds/check_for_closed_prompt.lua
Normal file
187
lua/codetyper/adapters/nvim/autocmds/check_for_closed_prompt.lua
Normal file
@@ -0,0 +1,187 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
local processed_prompts = require("codetyper.constants.constants").processed_prompts
|
||||
local is_processing = require("codetyper.constants.constants").is_processing
|
||||
local get_prompt_key = require("codetyper.adapters.nvim.autocmds.get_prompt_key")
|
||||
local read_attached_files = require("codetyper.adapters.nvim.autocmds.read_attached_files")
|
||||
local create_injection_marks = require("codetyper.adapters.nvim.autocmds.create_injection_marks")
|
||||
|
||||
--- Check if the buffer has a newly closed prompt and auto-process
|
||||
function check_for_closed_prompt()
|
||||
if is_processing then
|
||||
return
|
||||
end
|
||||
is_processing = true
|
||||
|
||||
local has_closing_tag = require("codetyper.parser.has_closing_tag")
|
||||
local get_last_prompt = require("codetyper.parser.get_last_prompt")
|
||||
local clean_prompt = require("codetyper.parser.clean_prompt")
|
||||
local strip_file_references = require("codetyper.parser.strip_file_references")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local current_file = vim.fn.expand("%:p")
|
||||
|
||||
if current_file == "" then
|
||||
is_processing = false
|
||||
return
|
||||
end
|
||||
|
||||
local cursor = vim.api.nvim_win_get_cursor(0)
|
||||
local line = cursor[1]
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, line - 1, line, false)
|
||||
|
||||
if #lines == 0 then
|
||||
is_processing = false
|
||||
return
|
||||
end
|
||||
|
||||
local current_line = lines[1]
|
||||
|
||||
if has_closing_tag(current_line, config.patterns.close_tag) then
|
||||
local prompt = get_last_prompt(bufnr)
|
||||
if prompt and prompt.content and prompt.content ~= "" then
|
||||
local prompt_key = get_prompt_key(bufnr, prompt)
|
||||
|
||||
if processed_prompts[prompt_key] then
|
||||
is_processing = false
|
||||
return
|
||||
end
|
||||
|
||||
processed_prompts[prompt_key] = true
|
||||
|
||||
local codetyper = require("codetyper")
|
||||
local ct_config = codetyper.get_config()
|
||||
local scheduler_enabled = ct_config and ct_config.scheduler and ct_config.scheduler.enabled
|
||||
|
||||
if scheduler_enabled then
|
||||
vim.schedule(function()
|
||||
local queue = require("codetyper.core.events.queue")
|
||||
local patch_mod = require("codetyper.core.diff.patch")
|
||||
local intent_mod = require("codetyper.core.intent")
|
||||
local scope_mod = require("codetyper.core.scope")
|
||||
|
||||
local snapshot = patch_mod.snapshot_buffer(bufnr, {
|
||||
start_line = prompt.start_line,
|
||||
end_line = prompt.end_line,
|
||||
})
|
||||
|
||||
local target_path
|
||||
if utils.is_coder_file(current_file) then
|
||||
target_path = utils.get_target_path(current_file)
|
||||
else
|
||||
target_path = current_file
|
||||
end
|
||||
|
||||
local attached_files = read_attached_files(prompt.content, current_file)
|
||||
|
||||
local cleaned = clean_prompt(strip_file_references(prompt.content))
|
||||
|
||||
local is_from_coder_file = utils.is_coder_file(current_file)
|
||||
|
||||
local target_bufnr = vim.fn.bufnr(target_path)
|
||||
local scope = nil
|
||||
local scope_text = nil
|
||||
local scope_range = nil
|
||||
|
||||
if not is_from_coder_file then
|
||||
if target_bufnr == -1 then
|
||||
target_bufnr = bufnr
|
||||
end
|
||||
scope = scope_mod.resolve_scope(target_bufnr, prompt.start_line, 1)
|
||||
if scope and scope.type ~= "file" then
|
||||
scope_text = scope.text
|
||||
scope_range = {
|
||||
start_line = scope.range.start_row,
|
||||
end_line = scope.range.end_row,
|
||||
}
|
||||
end
|
||||
else
|
||||
if target_bufnr == -1 then
|
||||
target_bufnr = vim.fn.bufadd(target_path)
|
||||
if target_bufnr ~= 0 then
|
||||
vim.fn.bufload(target_bufnr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local intent = intent_mod.detect(cleaned)
|
||||
|
||||
if not is_from_coder_file and scope and (scope.type == "function" or scope.type == "method") then
|
||||
if intent.type == "add" or intent.action == "insert" or intent.action == "append" then
|
||||
intent = {
|
||||
type = "complete",
|
||||
scope_hint = "function",
|
||||
confidence = intent.confidence,
|
||||
action = "replace",
|
||||
keywords = intent.keywords,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
if is_from_coder_file and (intent.action == "replace" or intent.type == "complete") then
|
||||
intent = {
|
||||
type = intent.type == "complete" and "add" or intent.type,
|
||||
confidence = intent.confidence,
|
||||
action = "append",
|
||||
keywords = intent.keywords,
|
||||
}
|
||||
end
|
||||
|
||||
local priority = 2
|
||||
if intent.type == "fix" or intent.type == "complete" then
|
||||
priority = 1
|
||||
elseif intent.type == "test" or intent.type == "document" then
|
||||
priority = 3
|
||||
end
|
||||
|
||||
local raw_start = (prompt.injection_range and prompt.injection_range.start_line) or prompt.start_line or 1
|
||||
local raw_end = (prompt.injection_range and prompt.injection_range.end_line) or prompt.end_line or 1
|
||||
local target_line_count = vim.api.nvim_buf_line_count(target_bufnr)
|
||||
target_line_count = math.max(1, target_line_count)
|
||||
local range_start = math.max(1, math.min(raw_start, target_line_count))
|
||||
local range_end = math.max(1, math.min(raw_end, target_line_count))
|
||||
if range_end < range_start then
|
||||
range_end = range_start
|
||||
end
|
||||
local event_range = { start_line = range_start, end_line = range_end }
|
||||
|
||||
local range_for_marks = scope_range or event_range
|
||||
local injection_marks = create_injection_marks(target_bufnr, range_for_marks)
|
||||
|
||||
queue.enqueue({
|
||||
id = queue.generate_id(),
|
||||
bufnr = bufnr,
|
||||
range = event_range,
|
||||
timestamp = os.clock(),
|
||||
changedtick = snapshot.changedtick,
|
||||
content_hash = snapshot.content_hash,
|
||||
prompt_content = cleaned,
|
||||
target_path = target_path,
|
||||
priority = priority,
|
||||
status = "pending",
|
||||
attempt_count = 0,
|
||||
intent = intent,
|
||||
scope = scope,
|
||||
scope_text = scope_text,
|
||||
scope_range = scope_range,
|
||||
attached_files = attached_files,
|
||||
injection_marks = injection_marks,
|
||||
})
|
||||
|
||||
local scope_info = scope
|
||||
and scope.type ~= "file"
|
||||
and string.format(" [%s: %s]", scope.type, scope.name or "anonymous")
|
||||
or ""
|
||||
utils.notify(string.format("Prompt queued: %s%s", intent.type, scope_info), vim.log.levels.INFO)
|
||||
end)
|
||||
else
|
||||
utils.notify("Processing prompt...", vim.log.levels.INFO)
|
||||
vim.schedule(function()
|
||||
vim.cmd("CoderProcess")
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
is_processing = false
|
||||
end
|
||||
|
||||
return check_for_closed_prompt
|
||||
@@ -0,0 +1,19 @@
|
||||
local check_for_closed_prompt = require("codetyper.adapters.nvim.autocmds.check_for_closed_prompt")
|
||||
|
||||
--- Check for closed prompt with preference check
|
||||
--- If auto_process is enabled, process; otherwise do nothing (manual mode)
|
||||
local function check_for_closed_prompt_with_preference()
|
||||
local find_prompts_in_buffer = require("codetyper.parser.find_prompts_in_buffer")
|
||||
|
||||
local bufnr = vim.api.nvim_get_current_buf()
|
||||
local prompts = find_prompts_in_buffer(bufnr)
|
||||
if #prompts == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
if auto_process then
|
||||
check_for_closed_prompt()
|
||||
end
|
||||
end
|
||||
|
||||
return check_for_closed_prompt_with_preference
|
||||
@@ -0,0 +1,9 @@
|
||||
local autocmds_state = require("codetyper.adapters.nvim.autocmds.state")
|
||||
|
||||
--- Clear auto-opened tracking for a buffer
|
||||
---@param bufnr number Buffer number
|
||||
local function clear_auto_opened(bufnr)
|
||||
autocmds_state.auto_opened_buffers[bufnr] = nil
|
||||
end
|
||||
|
||||
return clear_auto_opened
|
||||
@@ -0,0 +1,31 @@
|
||||
--- Create extmarks for injection range so position survives user edits
|
||||
---@param target_bufnr number Target buffer (where code will be injected)
|
||||
---@param range { start_line: number, end_line: number } Range to mark (1-based)
|
||||
---@return table|nil injection_marks { start_mark, end_mark } or nil if buffer invalid
|
||||
local function create_injection_marks(target_bufnr, range)
|
||||
if not range or target_bufnr == -1 or not vim.api.nvim_buf_is_valid(target_bufnr) then
|
||||
return nil
|
||||
end
|
||||
local line_count = vim.api.nvim_buf_line_count(target_bufnr)
|
||||
if line_count == 0 then
|
||||
return nil
|
||||
end
|
||||
local start_line = math.max(1, math.min(range.start_line, line_count))
|
||||
local end_line = math.max(1, math.min(range.end_line, line_count))
|
||||
if start_line > end_line then
|
||||
end_line = start_line
|
||||
end
|
||||
local marks = require("codetyper.core.marks")
|
||||
local end_line_content = vim.api.nvim_buf_get_lines(target_bufnr, end_line - 1, end_line, false)
|
||||
local end_col_0 = 0
|
||||
if end_line_content and end_line_content[1] then
|
||||
end_col_0 = #end_line_content[1]
|
||||
end
|
||||
local start_mark, end_mark = marks.mark_range(target_bufnr, start_line, end_line, end_col_0)
|
||||
if not start_mark.id or not end_mark.id then
|
||||
return nil
|
||||
end
|
||||
return { start_mark = start_mark, end_mark = end_mark }
|
||||
end
|
||||
|
||||
return create_injection_marks
|
||||
9
lua/codetyper/adapters/nvim/autocmds/get_prompt_key.lua
Normal file
9
lua/codetyper/adapters/nvim/autocmds/get_prompt_key.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
--- Generate a unique key for a prompt
|
||||
---@param bufnr number Buffer number
|
||||
---@param prompt table Prompt object
|
||||
---@return string Unique key
|
||||
local function get_prompt_key(bufnr, prompt)
|
||||
return string.format("%d:%d:%d:%s", bufnr, prompt.start_line, prompt.end_line, prompt.content:sub(1, 50))
|
||||
end
|
||||
|
||||
return get_prompt_key
|
||||
25
lua/codetyper/adapters/nvim/autocmds/ignored_directories.lua
Normal file
25
lua/codetyper/adapters/nvim/autocmds/ignored_directories.lua
Normal file
@@ -0,0 +1,25 @@
|
||||
local ignored_directories = {
|
||||
".git",
|
||||
".codetyper",
|
||||
".claude",
|
||||
".vscode",
|
||||
".idea",
|
||||
"node_modules",
|
||||
"vendor",
|
||||
"dist",
|
||||
"build",
|
||||
"target",
|
||||
"__pycache__",
|
||||
".cache",
|
||||
".npm",
|
||||
".yarn",
|
||||
"coverage",
|
||||
".next",
|
||||
".nuxt",
|
||||
".svelte-kit",
|
||||
"out",
|
||||
"bin",
|
||||
"obj",
|
||||
}
|
||||
|
||||
return ignored_directories
|
||||
48
lua/codetyper/adapters/nvim/autocmds/ignored_files.lua
Normal file
48
lua/codetyper/adapters/nvim/autocmds/ignored_files.lua
Normal file
@@ -0,0 +1,48 @@
|
||||
local ignored_files = {
|
||||
".gitignore",
|
||||
".gitattributes",
|
||||
".gitmodules",
|
||||
"package-lock.json",
|
||||
"yarn.lock",
|
||||
"pnpm-lock.yaml",
|
||||
"Cargo.lock",
|
||||
"Gemfile.lock",
|
||||
"poetry.lock",
|
||||
"composer.lock",
|
||||
".env",
|
||||
".env.local",
|
||||
".env.development",
|
||||
".env.production",
|
||||
".eslintrc",
|
||||
".eslintrc.json",
|
||||
".prettierrc",
|
||||
".prettierrc.json",
|
||||
".editorconfig",
|
||||
".dockerignore",
|
||||
"Dockerfile",
|
||||
"docker-compose.yml",
|
||||
"docker-compose.yaml",
|
||||
".npmrc",
|
||||
".yarnrc",
|
||||
".nvmrc",
|
||||
"tsconfig.json",
|
||||
"jsconfig.json",
|
||||
"babel.config.js",
|
||||
"webpack.config.js",
|
||||
"vite.config.js",
|
||||
"rollup.config.js",
|
||||
"jest.config.js",
|
||||
"vitest.config.js",
|
||||
".stylelintrc",
|
||||
"tailwind.config.js",
|
||||
"postcss.config.js",
|
||||
"README.md",
|
||||
"CHANGELOG.md",
|
||||
"LICENSE",
|
||||
"LICENSE.md",
|
||||
"CONTRIBUTING.md",
|
||||
"Makefile",
|
||||
"CMakeLists.txt",
|
||||
}
|
||||
|
||||
return ignored_files
|
||||
@@ -0,0 +1,18 @@
|
||||
local ignored_directories = require("codetyper.adapters.nvim.autocmds.ignored_directories")
|
||||
|
||||
--- Check if a file path contains an ignored directory
|
||||
---@param filepath string Full file path
|
||||
---@return boolean
|
||||
local function is_in_ignored_directory(filepath)
|
||||
for _, dir in ipairs(ignored_directories) do
|
||||
if filepath:match("/" .. dir .. "/") or filepath:match("/" .. dir .. "$") then
|
||||
return true
|
||||
end
|
||||
if filepath:match("^" .. dir .. "/") then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return is_in_ignored_directory
|
||||
@@ -0,0 +1,15 @@
|
||||
local supported_extensions = require("codetyper.adapters.nvim.autocmds.supported_extensions")
|
||||
|
||||
--- Check if extension is supported for auto-indexing
|
||||
---@param ext string File extension
|
||||
---@return boolean
|
||||
local function is_supported_extension(ext)
|
||||
for _, supported in ipairs(supported_extensions) do
|
||||
if ext == supported then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return is_supported_extension
|
||||
178
lua/codetyper/adapters/nvim/autocmds/process_single_prompt.lua
Normal file
178
lua/codetyper/adapters/nvim/autocmds/process_single_prompt.lua
Normal file
@@ -0,0 +1,178 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
local processed_prompts = require("codetyper.constants.constants").processed_prompts
|
||||
local get_prompt_key = require("codetyper.adapters.nvim.autocmds.get_prompt_key")
|
||||
local read_attached_files = require("codetyper.adapters.nvim.autocmds.read_attached_files")
|
||||
local create_injection_marks = require("codetyper.adapters.nvim.autocmds.create_injection_marks")
|
||||
|
||||
--- Process a single prompt through the scheduler
|
||||
---@param bufnr number Buffer number
|
||||
---@param prompt table Prompt object with start_line, end_line, content
|
||||
---@param current_file string Current file path
|
||||
---@param skip_processed_check? boolean Skip the processed check (for manual mode)
|
||||
local function process_single_prompt(bufnr, prompt, current_file, skip_processed_check)
|
||||
local clean_prompt = require("codetyper.parser.clean_prompt")
|
||||
local strip_file_references = require("codetyper.parser.strip_file_references")
|
||||
local scheduler = require("codetyper.core.scheduler.scheduler")
|
||||
|
||||
if not prompt.content or prompt.content == "" then
|
||||
return
|
||||
end
|
||||
|
||||
if not scheduler.status().running then
|
||||
scheduler.start()
|
||||
end
|
||||
|
||||
local prompt_key = get_prompt_key(bufnr, prompt)
|
||||
|
||||
if not skip_processed_check and processed_prompts[prompt_key] then
|
||||
return
|
||||
end
|
||||
|
||||
processed_prompts[prompt_key] = true
|
||||
|
||||
vim.schedule(function()
|
||||
local queue = require("codetyper.core.events.queue")
|
||||
local patch_mod = require("codetyper.core.diff.patch")
|
||||
local intent_mod = require("codetyper.core.intent")
|
||||
local scope_mod = require("codetyper.core.scope")
|
||||
|
||||
local snapshot = patch_mod.snapshot_buffer(bufnr, {
|
||||
start_line = prompt.start_line,
|
||||
end_line = prompt.end_line,
|
||||
})
|
||||
|
||||
local target_path
|
||||
local is_from_coder_file = utils.is_coder_file(current_file)
|
||||
if is_from_coder_file then
|
||||
target_path = utils.get_target_path(current_file)
|
||||
else
|
||||
target_path = current_file
|
||||
end
|
||||
|
||||
local attached_files = read_attached_files(prompt.content, current_file)
|
||||
|
||||
local cleaned = clean_prompt(strip_file_references(prompt.content))
|
||||
|
||||
local target_bufnr = vim.fn.bufnr(target_path)
|
||||
local scope = nil
|
||||
local scope_text = nil
|
||||
local scope_range = nil
|
||||
|
||||
if not is_from_coder_file then
|
||||
if target_bufnr == -1 then
|
||||
target_bufnr = bufnr
|
||||
end
|
||||
scope = scope_mod.resolve_scope(target_bufnr, prompt.start_line, 1)
|
||||
if scope and scope.type ~= "file" then
|
||||
scope_text = scope.text
|
||||
scope_range = {
|
||||
start_line = scope.range.start_row,
|
||||
end_line = scope.range.end_row,
|
||||
}
|
||||
end
|
||||
else
|
||||
if target_bufnr == -1 then
|
||||
target_bufnr = vim.fn.bufadd(target_path)
|
||||
if target_bufnr ~= 0 then
|
||||
vim.fn.bufload(target_bufnr)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local intent = intent_mod.detect(cleaned)
|
||||
|
||||
if prompt.intent_override then
|
||||
intent.action = prompt.intent_override.action or intent.action
|
||||
if prompt.intent_override.type then
|
||||
intent.type = prompt.intent_override.type
|
||||
end
|
||||
elseif not is_from_coder_file and scope and (scope.type == "function" or scope.type == "method") then
|
||||
if intent.type == "add" or intent.action == "insert" or intent.action == "append" then
|
||||
intent = {
|
||||
type = "complete",
|
||||
scope_hint = "function",
|
||||
confidence = intent.confidence,
|
||||
action = "replace",
|
||||
keywords = intent.keywords,
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
if is_from_coder_file and (intent.action == "replace" or intent.type == "complete") then
|
||||
intent = {
|
||||
type = intent.type == "complete" and "add" or intent.type,
|
||||
confidence = intent.confidence,
|
||||
action = "append",
|
||||
keywords = intent.keywords,
|
||||
}
|
||||
end
|
||||
|
||||
local project_context = nil
|
||||
if prompt.is_whole_file then
|
||||
pcall(function()
|
||||
local tree = require("codetyper.support.tree")
|
||||
local tree_log = tree.get_tree_log_path()
|
||||
if tree_log and vim.fn.filereadable(tree_log) == 1 then
|
||||
local tree_lines = vim.fn.readfile(tree_log)
|
||||
if tree_lines and #tree_lines > 0 then
|
||||
local tree_content = table.concat(tree_lines, "\n")
|
||||
project_context = tree_content:sub(1, 4000)
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
local priority = 2
|
||||
if intent.type == "fix" or intent.type == "complete" then
|
||||
priority = 1
|
||||
elseif intent.type == "test" or intent.type == "document" then
|
||||
priority = 3
|
||||
end
|
||||
|
||||
local raw_start = (prompt.injection_range and prompt.injection_range.start_line) or prompt.start_line or 1
|
||||
local raw_end = (prompt.injection_range and prompt.injection_range.end_line) or prompt.end_line or 1
|
||||
local target_line_count = vim.api.nvim_buf_line_count(target_bufnr)
|
||||
target_line_count = math.max(1, target_line_count)
|
||||
local range_start = math.max(1, math.min(raw_start, target_line_count))
|
||||
local range_end = math.max(1, math.min(raw_end, target_line_count))
|
||||
if range_end < range_start then
|
||||
range_end = range_start
|
||||
end
|
||||
local event_range = { start_line = range_start, end_line = range_end }
|
||||
|
||||
local range_for_marks = scope_range or event_range
|
||||
local injection_marks = create_injection_marks(target_bufnr, range_for_marks)
|
||||
|
||||
queue.enqueue({
|
||||
id = queue.generate_id(),
|
||||
bufnr = bufnr,
|
||||
range = event_range,
|
||||
timestamp = os.clock(),
|
||||
changedtick = snapshot.changedtick,
|
||||
content_hash = snapshot.content_hash,
|
||||
prompt_content = cleaned,
|
||||
target_path = target_path,
|
||||
priority = priority,
|
||||
status = "pending",
|
||||
attempt_count = 0,
|
||||
intent = intent,
|
||||
intent_override = prompt.intent_override,
|
||||
scope = scope,
|
||||
scope_text = scope_text,
|
||||
scope_range = scope_range,
|
||||
attached_files = attached_files,
|
||||
injection_marks = injection_marks,
|
||||
injection_range = prompt.injection_range,
|
||||
is_whole_file = prompt.is_whole_file,
|
||||
project_context = project_context,
|
||||
})
|
||||
|
||||
local scope_info = scope
|
||||
and scope.type ~= "file"
|
||||
and string.format(" [%s: %s]", scope.type, scope.name or "anonymous")
|
||||
or ""
|
||||
utils.notify(string.format("Prompt queued: %s%s", intent.type, scope_info), vim.log.levels.INFO)
|
||||
end)
|
||||
end
|
||||
|
||||
return process_single_prompt
|
||||
42
lua/codetyper/adapters/nvim/autocmds/read_attached_files.lua
Normal file
42
lua/codetyper/adapters/nvim/autocmds/read_attached_files.lua
Normal file
@@ -0,0 +1,42 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Read attached files from prompt content
|
||||
---@param prompt_content string Prompt content
|
||||
---@param base_path string Base path to resolve relative file paths
|
||||
---@return table[] attached_files List of {path, content} tables
|
||||
local function read_attached_files(prompt_content, base_path)
|
||||
local extract_file_references = require("codetyper.parser.extract_file_references")
|
||||
local file_refs = extract_file_references(prompt_content)
|
||||
local attached = {}
|
||||
local cwd = vim.fn.getcwd()
|
||||
local base_dir = vim.fn.fnamemodify(base_path, ":h")
|
||||
|
||||
for _, ref in ipairs(file_refs) do
|
||||
local file_path = nil
|
||||
|
||||
local cwd_path = cwd .. "/" .. ref
|
||||
if utils.file_exists(cwd_path) then
|
||||
file_path = cwd_path
|
||||
else
|
||||
local rel_path = base_dir .. "/" .. ref
|
||||
if utils.file_exists(rel_path) then
|
||||
file_path = rel_path
|
||||
end
|
||||
end
|
||||
|
||||
if file_path then
|
||||
local content = utils.read_file(file_path)
|
||||
if content then
|
||||
table.insert(attached, {
|
||||
path = ref,
|
||||
full_path = file_path,
|
||||
content = content,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return attached
|
||||
end
|
||||
|
||||
return read_attached_files
|
||||
19
lua/codetyper/adapters/nvim/autocmds/reset_processed.lua
Normal file
19
lua/codetyper/adapters/nvim/autocmds/reset_processed.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
local processed_prompts = require("codetyper.constants.constants").processed_prompts
|
||||
|
||||
--- Reset processed prompts for a buffer (useful for re-processing)
|
||||
---@param bufnr? number Buffer number (default: current)
|
||||
---@param silent? boolean Suppress notification (default: false)
|
||||
local function reset_processed(bufnr, silent)
|
||||
bufnr = bufnr or vim.api.nvim_get_current_buf()
|
||||
for key, _ in pairs(processed_prompts) do
|
||||
if key:match("^" .. bufnr .. ":") then
|
||||
processed_prompts[key] = nil
|
||||
end
|
||||
end
|
||||
if not silent then
|
||||
utils.notify("Prompt history cleared - prompts can be re-processed")
|
||||
end
|
||||
end
|
||||
|
||||
return reset_processed
|
||||
@@ -0,0 +1,17 @@
|
||||
local tree_update_timer = require("codetyper.constants.constants").tree_update_timer
|
||||
local TREE_UPDATE_DEBOUNCE_MS = require("codetyper.constants.constants").TREE_UPDATE_DEBOUNCE_MS
|
||||
|
||||
--- Schedule tree update with debounce
|
||||
local function schedule_tree_update()
|
||||
if tree_update_timer then
|
||||
tree_update_timer:stop()
|
||||
end
|
||||
|
||||
tree_update_timer = vim.defer_fn(function()
|
||||
local tree = require("codetyper.support.tree")
|
||||
tree.update_tree_log()
|
||||
tree_update_timer = nil
|
||||
end, TREE_UPDATE_DEBOUNCE_MS)
|
||||
end
|
||||
|
||||
return schedule_tree_update
|
||||
38
lua/codetyper/adapters/nvim/autocmds/set_coder_filetype.lua
Normal file
38
lua/codetyper/adapters/nvim/autocmds/set_coder_filetype.lua
Normal file
@@ -0,0 +1,38 @@
|
||||
--- Set appropriate filetype for coder files based on extension
|
||||
local function set_coder_filetype()
|
||||
local filepath = vim.fn.expand("%:p")
|
||||
|
||||
local ext = filepath:match("%.codetyper%.(%w+)$")
|
||||
|
||||
if ext then
|
||||
local ft_map = {
|
||||
ts = "typescript",
|
||||
tsx = "typescriptreact",
|
||||
js = "javascript",
|
||||
jsx = "javascriptreact",
|
||||
py = "python",
|
||||
lua = "lua",
|
||||
go = "go",
|
||||
rs = "rust",
|
||||
rb = "ruby",
|
||||
java = "java",
|
||||
c = "c",
|
||||
cpp = "cpp",
|
||||
cs = "cs",
|
||||
json = "json",
|
||||
yaml = "yaml",
|
||||
yml = "yaml",
|
||||
md = "markdown",
|
||||
html = "html",
|
||||
css = "css",
|
||||
scss = "scss",
|
||||
vue = "vue",
|
||||
svelte = "svelte",
|
||||
}
|
||||
|
||||
local filetype = ft_map[ext] or ext
|
||||
vim.bo.filetype = filetype
|
||||
end
|
||||
end
|
||||
|
||||
return set_coder_filetype
|
||||
203
lua/codetyper/adapters/nvim/autocmds/setup.lua
Normal file
203
lua/codetyper/adapters/nvim/autocmds/setup.lua
Normal file
@@ -0,0 +1,203 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
local AUGROUP = require("codetyper.constants.constants").AUGROUP
|
||||
local processed_prompts = require("codetyper.constants.constants").processed_prompts
|
||||
local is_processing = require("codetyper.constants.constants").is_processing
|
||||
local previous_mode = require("codetyper.constants.constants").previous_mode
|
||||
local prompt_process_timer = require("codetyper.constants.constants").prompt_process_timer
|
||||
local PROMPT_PROCESS_DEBOUNCE_MS = require("codetyper.constants.constants").PROMPT_PROCESS_DEBOUNCE_MS
|
||||
local schedule_tree_update = require("codetyper.adapters.nvim.autocmds.schedule_tree_update")
|
||||
local check_for_closed_prompt_with_preference = require("codetyper.adapters.nvim.autocmds.check_for_closed_prompt_with_preference")
|
||||
local check_all_prompts_with_preference = require("codetyper.adapters.nvim.autocmds.check_all_prompts_with_preference")
|
||||
local set_coder_filetype = require("codetyper.adapters.nvim.autocmds.set_coder_filetype")
|
||||
local clear_auto_opened = require("codetyper.adapters.nvim.autocmds.clear_auto_opened")
|
||||
local auto_index_file = require("codetyper.adapters.nvim.autocmds.auto_index_file")
|
||||
local update_brain_from_file = require("codetyper.adapters.nvim.autocmds.update_brain_from_file")
|
||||
|
||||
--- Setup autocommands
|
||||
local function setup()
|
||||
local group = vim.api.nvim_create_augroup(AUGROUP, { clear = true })
|
||||
|
||||
vim.api.nvim_create_autocmd("InsertLeave", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function()
|
||||
local buftype = vim.bo.buftype
|
||||
if buftype ~= "" then
|
||||
return
|
||||
end
|
||||
local filepath = vim.fn.expand("%:p")
|
||||
if utils.is_coder_file(filepath) and vim.bo.modified then
|
||||
vim.cmd("silent! write")
|
||||
end
|
||||
check_for_closed_prompt_with_preference()
|
||||
end,
|
||||
desc = "Check for closed prompt tags on InsertLeave",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("ModeChanged", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function(ev)
|
||||
local old_mode = ev.match:match("^(.-):")
|
||||
if old_mode then
|
||||
previous_mode = old_mode
|
||||
end
|
||||
end,
|
||||
desc = "Track previous mode for visual mode detection",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("ModeChanged", {
|
||||
group = group,
|
||||
pattern = "*:n",
|
||||
callback = function()
|
||||
local buftype = vim.bo.buftype
|
||||
if buftype ~= "" then
|
||||
return
|
||||
end
|
||||
|
||||
if is_processing then
|
||||
return
|
||||
end
|
||||
|
||||
if previous_mode == "v" or previous_mode == "V" or previous_mode == "\22" then
|
||||
return
|
||||
end
|
||||
|
||||
if prompt_process_timer then
|
||||
prompt_process_timer:stop()
|
||||
prompt_process_timer = nil
|
||||
end
|
||||
|
||||
prompt_process_timer = vim.defer_fn(function()
|
||||
prompt_process_timer = nil
|
||||
local mode = vim.api.nvim_get_mode().mode
|
||||
if mode ~= "n" then
|
||||
return
|
||||
end
|
||||
check_all_prompts_with_preference()
|
||||
end, PROMPT_PROCESS_DEBOUNCE_MS)
|
||||
end,
|
||||
desc = "Auto-process closed prompts when entering normal mode",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("CursorHold", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function()
|
||||
local buftype = vim.bo.buftype
|
||||
if buftype ~= "" then
|
||||
return
|
||||
end
|
||||
if is_processing then
|
||||
return
|
||||
end
|
||||
local mode = vim.api.nvim_get_mode().mode
|
||||
if mode == "n" then
|
||||
check_all_prompts_with_preference()
|
||||
end
|
||||
end,
|
||||
desc = "Auto-process closed prompts when idle in normal mode",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, {
|
||||
group = group,
|
||||
pattern = "*.codetyper/*",
|
||||
callback = function()
|
||||
set_coder_filetype()
|
||||
end,
|
||||
desc = "Set filetype for coder files",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("BufWipeout", {
|
||||
group = group,
|
||||
pattern = "*.codetyper/*",
|
||||
callback = function(ev)
|
||||
local bufnr = ev.buf
|
||||
for key, _ in pairs(processed_prompts) do
|
||||
if key:match("^" .. bufnr .. ":") then
|
||||
processed_prompts[key] = nil
|
||||
end
|
||||
end
|
||||
clear_auto_opened(bufnr)
|
||||
end,
|
||||
desc = "Cleanup on coder buffer close",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd({ "BufWritePost", "BufNewFile" }, {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function(ev)
|
||||
local filepath = ev.file or vim.fn.expand("%:p")
|
||||
if filepath:match("%.codetyper%.") or filepath:match("tree%.log$") then
|
||||
return
|
||||
end
|
||||
if filepath:match("node_modules") or filepath:match("%.git/") or filepath:match("%.codetyper/") then
|
||||
return
|
||||
end
|
||||
schedule_tree_update()
|
||||
|
||||
local indexer_loaded, indexer = pcall(require, "codetyper.indexer")
|
||||
if indexer_loaded then
|
||||
indexer.schedule_index_file(filepath)
|
||||
end
|
||||
|
||||
local brain_loaded, brain = pcall(require, "codetyper.brain")
|
||||
if brain_loaded and brain.is_initialized and brain.is_initialized() then
|
||||
vim.defer_fn(function()
|
||||
update_brain_from_file(filepath)
|
||||
end, 500)
|
||||
end
|
||||
end,
|
||||
desc = "Update tree.log, index, and brain on file creation/save",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("BufDelete", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function(ev)
|
||||
local filepath = ev.file or ""
|
||||
if filepath == "" or filepath:match("%.codetyper%.") or filepath:match("tree%.log$") then
|
||||
return
|
||||
end
|
||||
schedule_tree_update()
|
||||
end,
|
||||
desc = "Update tree.log on file deletion",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("DirChanged", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function()
|
||||
schedule_tree_update()
|
||||
end,
|
||||
desc = "Update tree.log on directory change",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function()
|
||||
local brain_loaded, brain = pcall(require, "codetyper.brain")
|
||||
if brain_loaded and brain.is_initialized and brain.is_initialized() then
|
||||
brain.shutdown()
|
||||
end
|
||||
end,
|
||||
desc = "Shutdown brain and flush pending changes",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_autocmd("BufEnter", {
|
||||
group = group,
|
||||
pattern = "*",
|
||||
callback = function(ev)
|
||||
vim.defer_fn(function()
|
||||
auto_index_file(ev.buf)
|
||||
end, 100)
|
||||
end,
|
||||
desc = "Auto-index source files with coder companion",
|
||||
})
|
||||
|
||||
local thinking_setup = require("codetyper.adapters.nvim.ui.thinking.setup")
|
||||
thinking_setup()
|
||||
end
|
||||
|
||||
return setup
|
||||
@@ -0,0 +1,27 @@
|
||||
local ignored_files = require("codetyper.adapters.nvim.autocmds.ignored_files")
|
||||
local is_in_ignored_directory = require("codetyper.adapters.nvim.autocmds.is_in_ignored_directory")
|
||||
|
||||
--- Check if a file should be ignored for coder companion creation
|
||||
---@param filepath string Full file path
|
||||
---@return boolean
|
||||
local function should_ignore_for_coder(filepath)
|
||||
local filename = vim.fn.fnamemodify(filepath, ":t")
|
||||
|
||||
for _, ignored in ipairs(ignored_files) do
|
||||
if filename == ignored then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
if filename:match("^%.") then
|
||||
return true
|
||||
end
|
||||
|
||||
if is_in_ignored_directory(filepath) then
|
||||
return true
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
return should_ignore_for_coder
|
||||
7
lua/codetyper/adapters/nvim/autocmds/state.lua
Normal file
7
lua/codetyper/adapters/nvim/autocmds/state.lua
Normal file
@@ -0,0 +1,7 @@
|
||||
local state = {
|
||||
auto_opened_buffers = {},
|
||||
auto_indexed_buffers = {},
|
||||
brain_update_timers = {},
|
||||
}
|
||||
|
||||
return state
|
||||
@@ -0,0 +1,29 @@
|
||||
local supported_extensions = {
|
||||
"ts",
|
||||
"tsx",
|
||||
"js",
|
||||
"jsx",
|
||||
"py",
|
||||
"lua",
|
||||
"go",
|
||||
"rs",
|
||||
"rb",
|
||||
"java",
|
||||
"c",
|
||||
"cpp",
|
||||
"cs",
|
||||
"json",
|
||||
"yaml",
|
||||
"yml",
|
||||
"md",
|
||||
"html",
|
||||
"css",
|
||||
"scss",
|
||||
"vue",
|
||||
"svelte",
|
||||
"php",
|
||||
"sh",
|
||||
"zsh",
|
||||
}
|
||||
|
||||
return supported_extensions
|
||||
@@ -0,0 +1,91 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Update brain with patterns from a file
|
||||
---@param filepath string
|
||||
local function update_brain_from_file(filepath)
|
||||
local brain_loaded, brain = pcall(require, "codetyper.brain")
|
||||
if not brain_loaded or not brain.is_initialized() then
|
||||
return
|
||||
end
|
||||
|
||||
local content = utils.read_file(filepath)
|
||||
if not content or content == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local ext = vim.fn.fnamemodify(filepath, ":e")
|
||||
local lines = vim.split(content, "\n")
|
||||
|
||||
local functions = {}
|
||||
local classes = {}
|
||||
local imports = {}
|
||||
|
||||
for line_index, line in ipairs(lines) do
|
||||
local func_name = line:match("^%s*function%s+([%w_:%.]+)%s*%(")
|
||||
or line:match("^%s*local%s+function%s+([%w_]+)%s*%(")
|
||||
or line:match("^%s*def%s+([%w_]+)%s*%(")
|
||||
or line:match("^%s*func%s+([%w_]+)%s*%(")
|
||||
or line:match("^%s*async%s+function%s+([%w_]+)%s*%(")
|
||||
or line:match("^%s*public%s+.*%s+([%w_]+)%s*%(")
|
||||
or line:match("^%s*private%s+.*%s+([%w_]+)%s*%(")
|
||||
if func_name then
|
||||
table.insert(functions, { name = func_name, line = line_index })
|
||||
end
|
||||
|
||||
local class_name = line:match("^%s*class%s+([%w_]+)")
|
||||
or line:match("^%s*public%s+class%s+([%w_]+)")
|
||||
or line:match("^%s*interface%s+([%w_]+)")
|
||||
or line:match("^%s*struct%s+([%w_]+)")
|
||||
if class_name then
|
||||
table.insert(classes, { name = class_name, line = line_index })
|
||||
end
|
||||
|
||||
local import_path = line:match("import%s+.*%s+from%s+[\"']([^\"']+)[\"']")
|
||||
or line:match("require%([\"']([^\"']+)[\"']%)")
|
||||
or line:match("from%s+([%w_.]+)%s+import")
|
||||
if import_path then
|
||||
table.insert(imports, import_path)
|
||||
end
|
||||
end
|
||||
|
||||
if #functions == 0 and #classes == 0 then
|
||||
return
|
||||
end
|
||||
|
||||
local parts = {}
|
||||
if #functions > 0 then
|
||||
local func_names = {}
|
||||
for func_index, func_entry in ipairs(functions) do
|
||||
if func_index <= 5 then
|
||||
table.insert(func_names, func_entry.name)
|
||||
end
|
||||
end
|
||||
table.insert(parts, "functions: " .. table.concat(func_names, ", "))
|
||||
end
|
||||
if #classes > 0 then
|
||||
local class_names = {}
|
||||
for _, class_entry in ipairs(classes) do
|
||||
table.insert(class_names, class_entry.name)
|
||||
end
|
||||
table.insert(parts, "classes: " .. table.concat(class_names, ", "))
|
||||
end
|
||||
|
||||
local summary = vim.fn.fnamemodify(filepath, ":t") .. " - " .. table.concat(parts, "; ")
|
||||
|
||||
brain.learn({
|
||||
type = "pattern_detected",
|
||||
file = filepath,
|
||||
timestamp = os.time(),
|
||||
data = {
|
||||
name = summary,
|
||||
description = #functions .. " functions, " .. #classes .. " classes",
|
||||
language = ext,
|
||||
symbols = vim.tbl_map(function(func_entry)
|
||||
return func_entry.name
|
||||
end, functions),
|
||||
example = nil,
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
return update_brain_from_file
|
||||
63
lua/codetyper/adapters/nvim/cmp/get_brain_completions.lua
Normal file
63
lua/codetyper/adapters/nvim/cmp/get_brain_completions.lua
Normal file
@@ -0,0 +1,63 @@
|
||||
--- Get completion items from brain context
|
||||
---@param prefix string Current word prefix
|
||||
---@return table[] items
|
||||
local function get_brain_completions(prefix)
|
||||
local items = {}
|
||||
|
||||
local brain_loaded, brain = pcall(require, "codetyper.brain")
|
||||
if not brain_loaded then
|
||||
return items
|
||||
end
|
||||
|
||||
local brain_initialized = false
|
||||
if brain.is_initialized then
|
||||
local init_check_success, init_state = pcall(brain.is_initialized)
|
||||
brain_initialized = init_check_success and init_state
|
||||
end
|
||||
|
||||
if not brain_initialized then
|
||||
return items
|
||||
end
|
||||
|
||||
local query_success, query_result = pcall(brain.query, {
|
||||
query = prefix,
|
||||
max_results = 10,
|
||||
types = { "pattern" },
|
||||
})
|
||||
|
||||
if query_success and query_result and query_result.nodes then
|
||||
for _, node in ipairs(query_result.nodes) do
|
||||
if node.c and node.c.s then
|
||||
local summary = node.c.s
|
||||
for matched_functions in summary:gmatch("functions:%s*([^;]+)") do
|
||||
for func_name in matched_functions:gmatch("([%w_]+)") do
|
||||
if func_name:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = func_name,
|
||||
kind = 3, -- Function
|
||||
detail = "[brain]",
|
||||
documentation = summary,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
for matched_classes in summary:gmatch("classes:%s*([^;]+)") do
|
||||
for class_name in matched_classes:gmatch("([%w_]+)") do
|
||||
if class_name:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = class_name,
|
||||
kind = 7, -- Class
|
||||
detail = "[brain]",
|
||||
documentation = summary,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
return get_brain_completions
|
||||
28
lua/codetyper/adapters/nvim/cmp/get_buffer_completions.lua
Normal file
28
lua/codetyper/adapters/nvim/cmp/get_buffer_completions.lua
Normal file
@@ -0,0 +1,28 @@
|
||||
--- Get completion items from current buffer (fallback)
|
||||
---@param prefix string Current word prefix
|
||||
---@param bufnr number Buffer number
|
||||
---@return table[] items
|
||||
local function get_buffer_completions(prefix, bufnr)
|
||||
local items = {}
|
||||
local seen = {}
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local prefix_lower = prefix:lower()
|
||||
|
||||
for _, line in ipairs(lines) do
|
||||
for word in line:gmatch("[%a_][%w_]*") do
|
||||
if #word >= 3 and word:lower():find(prefix_lower, 1, true) and not seen[word] and word ~= prefix then
|
||||
seen[word] = true
|
||||
table.insert(items, {
|
||||
label = word,
|
||||
kind = 1, -- Text
|
||||
detail = "[buffer]",
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
return get_buffer_completions
|
||||
24
lua/codetyper/adapters/nvim/cmp/get_copilot_suggestion.lua
Normal file
24
lua/codetyper/adapters/nvim/cmp/get_copilot_suggestion.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
--- Try to get Copilot suggestion if plugin is installed
|
||||
---@param prefix string
|
||||
---@return string|nil suggestion
|
||||
local function get_copilot_suggestion(prefix)
|
||||
local suggestion_api_loaded, copilot_suggestion_api = pcall(require, "copilot.suggestion")
|
||||
if suggestion_api_loaded and copilot_suggestion_api and type(copilot_suggestion_api.get_suggestion) == "function" then
|
||||
local suggestion_fetch_success, suggestion = pcall(copilot_suggestion_api.get_suggestion)
|
||||
if suggestion_fetch_success and suggestion and suggestion ~= "" then
|
||||
return suggestion
|
||||
end
|
||||
end
|
||||
|
||||
local copilot_loaded, copilot = pcall(require, "copilot")
|
||||
if copilot_loaded and copilot and type(copilot.get_suggestion) == "function" then
|
||||
local suggestion_fetch_success, suggestion = pcall(copilot.get_suggestion)
|
||||
if suggestion_fetch_success and suggestion and suggestion ~= "" then
|
||||
return suggestion
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
return get_copilot_suggestion
|
||||
63
lua/codetyper/adapters/nvim/cmp/get_indexer_completions.lua
Normal file
63
lua/codetyper/adapters/nvim/cmp/get_indexer_completions.lua
Normal file
@@ -0,0 +1,63 @@
|
||||
--- Get completion items from indexer symbols
|
||||
---@param prefix string Current word prefix
|
||||
---@return table[] items
|
||||
local function get_indexer_completions(prefix)
|
||||
local items = {}
|
||||
|
||||
local indexer_loaded, indexer = pcall(require, "codetyper.indexer")
|
||||
if not indexer_loaded then
|
||||
return items
|
||||
end
|
||||
|
||||
local index_load_success, index = pcall(indexer.load_index)
|
||||
if not index_load_success or not index then
|
||||
return items
|
||||
end
|
||||
|
||||
if index.symbols then
|
||||
for symbol, files in pairs(index.symbols) do
|
||||
if symbol:lower():find(prefix:lower(), 1, true) then
|
||||
local files_display = type(files) == "table" and table.concat(files, ", ") or tostring(files)
|
||||
table.insert(items, {
|
||||
label = symbol,
|
||||
kind = 6, -- Variable (generic)
|
||||
detail = "[index] " .. files_display:sub(1, 30),
|
||||
documentation = "Symbol found in: " .. files_display,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if index.files then
|
||||
for filepath, file_index in pairs(index.files) do
|
||||
if file_index and file_index.functions then
|
||||
for _, func_entry in ipairs(file_index.functions) do
|
||||
if func_entry.name and func_entry.name:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = func_entry.name,
|
||||
kind = 3, -- Function
|
||||
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
|
||||
documentation = func_entry.docstring or ("Function at line " .. (func_entry.line or "?")),
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
if file_index and file_index.classes then
|
||||
for _, class_entry in ipairs(file_index.classes) do
|
||||
if class_entry.name and class_entry.name:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = class_entry.name,
|
||||
kind = 7, -- Class
|
||||
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
|
||||
documentation = class_entry.docstring or ("Class at line " .. (class_entry.line or "?")),
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
return get_indexer_completions
|
||||
7
lua/codetyper/adapters/nvim/cmp/has_cmp.lua
Normal file
7
lua/codetyper/adapters/nvim/cmp/has_cmp.lua
Normal file
@@ -0,0 +1,7 @@
|
||||
--- Check if cmp is available
|
||||
---@return boolean
|
||||
local function has_cmp()
|
||||
return pcall(require, "cmp")
|
||||
end
|
||||
|
||||
return has_cmp
|
||||
@@ -6,352 +6,135 @@
|
||||
|
||||
local M = {}
|
||||
|
||||
local source = {}
|
||||
local has_cmp = require("codetyper.adapters.nvim.cmp.has_cmp")
|
||||
local get_brain_completions = require("codetyper.adapters.nvim.cmp.get_brain_completions")
|
||||
local get_indexer_completions = require("codetyper.adapters.nvim.cmp.get_indexer_completions")
|
||||
local get_buffer_completions = require("codetyper.adapters.nvim.cmp.get_buffer_completions")
|
||||
local get_copilot_suggestion = require("codetyper.adapters.nvim.cmp.get_copilot_suggestion")
|
||||
|
||||
--- Check if cmp is available
|
||||
---@return boolean
|
||||
local function has_cmp()
|
||||
return pcall(require, "cmp")
|
||||
end
|
||||
|
||||
--- Get completion items from brain context
|
||||
---@param prefix string Current word prefix
|
||||
---@return table[] items
|
||||
local function get_brain_completions(prefix)
|
||||
local items = {}
|
||||
|
||||
local ok_brain, brain = pcall(require, "codetyper.brain")
|
||||
if not ok_brain then
|
||||
return items
|
||||
end
|
||||
|
||||
-- Check if brain is initialized safely
|
||||
local is_init = false
|
||||
if brain.is_initialized then
|
||||
local ok, result = pcall(brain.is_initialized)
|
||||
is_init = ok and result
|
||||
end
|
||||
|
||||
if not is_init then
|
||||
return items
|
||||
end
|
||||
|
||||
-- Query brain for relevant patterns
|
||||
local ok_query, result = pcall(brain.query, {
|
||||
query = prefix,
|
||||
max_results = 10,
|
||||
types = { "pattern" },
|
||||
})
|
||||
|
||||
if ok_query and result and result.nodes then
|
||||
for _, node in ipairs(result.nodes) do
|
||||
if node.c and node.c.s then
|
||||
-- Extract function/class names from summary
|
||||
local summary = node.c.s
|
||||
for name in summary:gmatch("functions:%s*([^;]+)") do
|
||||
for func in name:gmatch("([%w_]+)") do
|
||||
if func:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = func,
|
||||
kind = 3, -- Function
|
||||
detail = "[brain]",
|
||||
documentation = summary,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
for name in summary:gmatch("classes:%s*([^;]+)") do
|
||||
for class in name:gmatch("([%w_]+)") do
|
||||
if class:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = class,
|
||||
kind = 7, -- Class
|
||||
detail = "[brain]",
|
||||
documentation = summary,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
--- Get completion items from indexer symbols
|
||||
---@param prefix string Current word prefix
|
||||
---@return table[] items
|
||||
local function get_indexer_completions(prefix)
|
||||
local items = {}
|
||||
|
||||
local ok_indexer, indexer = pcall(require, "codetyper.indexer")
|
||||
if not ok_indexer then
|
||||
return items
|
||||
end
|
||||
|
||||
local ok_load, index = pcall(indexer.load_index)
|
||||
if not ok_load or not index then
|
||||
return items
|
||||
end
|
||||
|
||||
-- Search symbols
|
||||
if index.symbols then
|
||||
for symbol, files in pairs(index.symbols) do
|
||||
if symbol:lower():find(prefix:lower(), 1, true) then
|
||||
local files_str = type(files) == "table" and table.concat(files, ", ") or tostring(files)
|
||||
table.insert(items, {
|
||||
label = symbol,
|
||||
kind = 6, -- Variable (generic)
|
||||
detail = "[index] " .. files_str:sub(1, 30),
|
||||
documentation = "Symbol found in: " .. files_str,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Search functions in files
|
||||
if index.files then
|
||||
for filepath, file_index in pairs(index.files) do
|
||||
if file_index and file_index.functions then
|
||||
for _, func in ipairs(file_index.functions) do
|
||||
if func.name and func.name:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = func.name,
|
||||
kind = 3, -- Function
|
||||
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
|
||||
documentation = func.docstring or ("Function at line " .. (func.line or "?")),
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
if file_index and file_index.classes then
|
||||
for _, class in ipairs(file_index.classes) do
|
||||
if class.name and class.name:lower():find(prefix:lower(), 1, true) then
|
||||
table.insert(items, {
|
||||
label = class.name,
|
||||
kind = 7, -- Class
|
||||
detail = "[index] " .. vim.fn.fnamemodify(filepath, ":t"),
|
||||
documentation = class.docstring or ("Class at line " .. (class.line or "?")),
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
--- Get completion items from current buffer (fallback)
|
||||
---@param prefix string Current word prefix
|
||||
---@param bufnr number Buffer number
|
||||
---@return table[] items
|
||||
local function get_buffer_completions(prefix, bufnr)
|
||||
local items = {}
|
||||
local seen = {}
|
||||
|
||||
-- Get all lines in buffer
|
||||
local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false)
|
||||
local prefix_lower = prefix:lower()
|
||||
|
||||
for _, line in ipairs(lines) do
|
||||
-- Extract words that could be identifiers
|
||||
for word in line:gmatch("[%a_][%w_]*") do
|
||||
if #word >= 3 and word:lower():find(prefix_lower, 1, true) and not seen[word] and word ~= prefix then
|
||||
seen[word] = true
|
||||
table.insert(items, {
|
||||
label = word,
|
||||
kind = 1, -- Text
|
||||
detail = "[buffer]",
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return items
|
||||
end
|
||||
|
||||
--- Try to get Copilot suggestion if plugin is installed
|
||||
---@param prefix string
|
||||
---@return string|nil suggestion
|
||||
local function get_copilot_suggestion(prefix)
|
||||
-- Try copilot.lua suggestion API first
|
||||
local ok, copilot_suggestion = pcall(require, "copilot.suggestion")
|
||||
if ok and copilot_suggestion and type(copilot_suggestion.get_suggestion) == "function" then
|
||||
local ok2, suggestion = pcall(copilot_suggestion.get_suggestion)
|
||||
if ok2 and suggestion and suggestion ~= "" then
|
||||
-- Only return if suggestion seems to start with prefix (best-effort)
|
||||
if prefix == "" or suggestion:lower():match(prefix:lower(), 1) then
|
||||
return suggestion
|
||||
else
|
||||
return suggestion
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Fallback: try older copilot module if present
|
||||
local ok3, copilot = pcall(require, "copilot")
|
||||
if ok3 and copilot and type(copilot.get_suggestion) == "function" then
|
||||
local ok4, suggestion = pcall(copilot.get_suggestion)
|
||||
if ok4 and suggestion and suggestion ~= "" then
|
||||
return suggestion
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Create new cmp source instance
|
||||
function source.new()
|
||||
return setmetatable({}, { __index = source })
|
||||
end
|
||||
|
||||
--- Get source name
|
||||
function source:get_keyword_pattern()
|
||||
return [[\k\+]]
|
||||
end
|
||||
|
||||
--- Check if source is available
|
||||
function source:is_available()
|
||||
return true
|
||||
end
|
||||
|
||||
--- Get debug name
|
||||
function source:get_debug_name()
|
||||
return "codetyper"
|
||||
end
|
||||
|
||||
--- Get trigger characters
|
||||
function source:get_trigger_characters()
|
||||
return { ".", ":", "_" }
|
||||
end
|
||||
local source = require("codetyper.utils.cmp_source")
|
||||
|
||||
--- Complete
|
||||
---@param params table
|
||||
---@param callback fun(response: table|nil)
|
||||
function source:complete(params, callback)
|
||||
local prefix = params.context.cursor_before_line:match("[%w_]+$") or ""
|
||||
local prefix = params.context.cursor_before_line:match("[%w_]+$") or ""
|
||||
|
||||
if #prefix < 2 then
|
||||
callback({ items = {}, isIncomplete = true })
|
||||
return
|
||||
end
|
||||
if #prefix < 2 then
|
||||
callback({ items = {}, isIncomplete = true })
|
||||
return
|
||||
end
|
||||
|
||||
-- Collect completions from brain, indexer, and buffer
|
||||
local items = {}
|
||||
local seen = {}
|
||||
-- Collect completions from brain, indexer, and buffer
|
||||
local items = {}
|
||||
local seen = {}
|
||||
|
||||
-- Get brain completions (highest priority)
|
||||
local ok1, brain_items = pcall(get_brain_completions, prefix)
|
||||
if ok1 and brain_items then
|
||||
for _, item in ipairs(brain_items) do
|
||||
if not seen[item.label] then
|
||||
seen[item.label] = true
|
||||
item.sortText = "1" .. item.label
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Get brain completions (highest priority)
|
||||
local brain_completions_success, brain_items = pcall(get_brain_completions, prefix)
|
||||
if brain_completions_success and brain_items then
|
||||
for _, item in ipairs(brain_items) do
|
||||
if not seen[item.label] then
|
||||
seen[item.label] = true
|
||||
item.sortText = "1" .. item.label
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Get indexer completions
|
||||
local ok2, indexer_items = pcall(get_indexer_completions, prefix)
|
||||
if ok2 and indexer_items then
|
||||
for _, item in ipairs(indexer_items) do
|
||||
if not seen[item.label] then
|
||||
seen[item.label] = true
|
||||
item.sortText = "2" .. item.label
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Get indexer completions
|
||||
local indexer_completions_success, indexer_items = pcall(get_indexer_completions, prefix)
|
||||
if indexer_completions_success and indexer_items then
|
||||
for _, item in ipairs(indexer_items) do
|
||||
if not seen[item.label] then
|
||||
seen[item.label] = true
|
||||
item.sortText = "2" .. item.label
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Get buffer completions as fallback (lower priority)
|
||||
local bufnr = params.context.bufnr
|
||||
if bufnr then
|
||||
local ok3, buffer_items = pcall(get_buffer_completions, prefix, bufnr)
|
||||
if ok3 and buffer_items then
|
||||
for _, item in ipairs(buffer_items) do
|
||||
if not seen[item.label] then
|
||||
seen[item.label] = true
|
||||
item.sortText = "3" .. item.label
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- Get buffer completions as fallback (lower priority)
|
||||
local bufnr = params.context.bufnr
|
||||
if bufnr then
|
||||
local buffer_completions_success, buffer_items = pcall(get_buffer_completions, prefix, bufnr)
|
||||
if buffer_completions_success and buffer_items then
|
||||
for _, item in ipairs(buffer_items) do
|
||||
if not seen[item.label] then
|
||||
seen[item.label] = true
|
||||
item.sortText = "3" .. item.label
|
||||
table.insert(items, item)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- If Copilot is installed, prefer its suggestion as a top-priority completion
|
||||
local ok_cp, _ = pcall(require, "copilot")
|
||||
if ok_cp then
|
||||
local suggestion = nil
|
||||
local ok_sug, res = pcall(get_copilot_suggestion, prefix)
|
||||
if ok_sug then
|
||||
suggestion = res
|
||||
end
|
||||
if suggestion and suggestion ~= "" then
|
||||
-- Truncate suggestion to first line for label display
|
||||
local first_line = suggestion:match("([^
|
||||
]+)") or suggestion
|
||||
-- Avoid duplicates
|
||||
if not seen[first_line] then
|
||||
seen[first_line] = true
|
||||
table.insert(items, 1, {
|
||||
label = first_line,
|
||||
kind = 1,
|
||||
detail = "[copilot]",
|
||||
documentation = suggestion,
|
||||
sortText = "0" .. first_line,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
-- If Copilot is installed, prefer its suggestion as a top-priority completion
|
||||
local copilot_installed = pcall(require, "copilot")
|
||||
if copilot_installed then
|
||||
local suggestion = nil
|
||||
local copilot_suggestion_success, copilot_suggestion_result = pcall(get_copilot_suggestion, prefix)
|
||||
if copilot_suggestion_success then
|
||||
suggestion = copilot_suggestion_result
|
||||
end
|
||||
if suggestion and suggestion ~= "" then
|
||||
local first_line = suggestion:match("([^\n]+)") or suggestion
|
||||
if not seen[first_line] then
|
||||
seen[first_line] = true
|
||||
table.insert(items, 1, {
|
||||
label = first_line,
|
||||
kind = 1,
|
||||
detail = "[copilot]",
|
||||
documentation = suggestion,
|
||||
sortText = "0" .. first_line,
|
||||
})
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
callback({
|
||||
items = items,
|
||||
isIncomplete = #items >= 50,
|
||||
})
|
||||
callback({
|
||||
items = items,
|
||||
isIncomplete = #items >= 50,
|
||||
})
|
||||
end
|
||||
|
||||
--- Setup the completion source
|
||||
function M.setup()
|
||||
if not has_cmp() then
|
||||
return false
|
||||
end
|
||||
if not has_cmp() then
|
||||
return false
|
||||
end
|
||||
|
||||
local cmp = require("cmp")
|
||||
local new_source = source.new()
|
||||
local cmp = require("cmp")
|
||||
local new_source = source.new()
|
||||
|
||||
-- Register the source
|
||||
cmp.register_source("codetyper", new_source)
|
||||
-- Register the source
|
||||
cmp.register_source("codetyper", new_source)
|
||||
|
||||
return true
|
||||
return true
|
||||
end
|
||||
|
||||
--- Check if source is registered
|
||||
---@return boolean
|
||||
function M.is_registered()
|
||||
local ok, cmp = pcall(require, "cmp")
|
||||
if not ok then
|
||||
return false
|
||||
end
|
||||
local cmp_loaded, cmp = pcall(require, "cmp")
|
||||
if not cmp_loaded then
|
||||
return false
|
||||
end
|
||||
|
||||
-- Try to get registered sources
|
||||
local config = cmp.get_config()
|
||||
if config and config.sources then
|
||||
for _, src in ipairs(config.sources) do
|
||||
if src.name == "codetyper" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
local config = cmp.get_config()
|
||||
if config and config.sources then
|
||||
for _, src in ipairs(config.sources) do
|
||||
if src.name == "codetyper" then
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
return false
|
||||
end
|
||||
|
||||
--- Get source for manual registration
|
||||
function M.get_source()
|
||||
return source
|
||||
return source
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -1,414 +0,0 @@
|
||||
---@mod codetyper.commands Command definitions for Codetyper.nvim
|
||||
|
||||
local M = {}
|
||||
|
||||
local transform = require("codetyper.core.transform")
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Refresh tree.log manually
|
||||
local function cmd_tree()
|
||||
local tree = require("codetyper.support.tree")
|
||||
if tree.update_tree_log() then
|
||||
utils.notify("Tree log updated: " .. tree.get_tree_log_path())
|
||||
else
|
||||
utils.notify("Failed to update tree log", vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
--- Open tree.log file
|
||||
local function cmd_tree_view()
|
||||
local tree = require("codetyper.support.tree")
|
||||
local tree_log_path = tree.get_tree_log_path()
|
||||
|
||||
if not tree_log_path then
|
||||
utils.notify("Could not find tree.log", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
-- Ensure tree is up to date
|
||||
tree.update_tree_log()
|
||||
|
||||
-- Open in a new split
|
||||
vim.cmd("vsplit " .. vim.fn.fnameescape(tree_log_path))
|
||||
vim.bo.readonly = true
|
||||
vim.bo.modifiable = false
|
||||
end
|
||||
|
||||
--- Reset processed prompts to allow re-processing
|
||||
local function cmd_reset()
|
||||
local autocmds = require("codetyper.adapters.nvim.autocmds")
|
||||
autocmds.reset_processed()
|
||||
end
|
||||
|
||||
--- Force update gitignore
|
||||
local function cmd_gitignore()
|
||||
local gitignore = require("codetyper.support.gitignore")
|
||||
gitignore.force_update()
|
||||
end
|
||||
|
||||
--- Index the entire project
|
||||
local function cmd_index_project()
|
||||
local indexer = require("codetyper.features.indexer")
|
||||
|
||||
utils.notify("Indexing project...", vim.log.levels.INFO)
|
||||
|
||||
indexer.index_project(function(index)
|
||||
if index then
|
||||
local msg = string.format(
|
||||
"Indexed: %d files, %d functions, %d classes, %d exports",
|
||||
index.stats.files,
|
||||
index.stats.functions,
|
||||
index.stats.classes,
|
||||
index.stats.exports
|
||||
)
|
||||
utils.notify(msg, vim.log.levels.INFO)
|
||||
else
|
||||
utils.notify("Failed to index project", vim.log.levels.ERROR)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
--- Show index status
|
||||
local function cmd_index_status()
|
||||
local indexer = require("codetyper.features.indexer")
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
|
||||
local status = indexer.get_status()
|
||||
local mem_stats = memory.get_stats()
|
||||
|
||||
local lines = {
|
||||
"Project Index Status",
|
||||
"====================",
|
||||
"",
|
||||
}
|
||||
|
||||
if status.indexed then
|
||||
table.insert(lines, "Status: Indexed")
|
||||
table.insert(lines, "Project Type: " .. (status.project_type or "unknown"))
|
||||
table.insert(lines, "Last Indexed: " .. os.date("%Y-%m-%d %H:%M:%S", status.last_indexed))
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "Stats:")
|
||||
table.insert(lines, " Files: " .. (status.stats.files or 0))
|
||||
table.insert(lines, " Functions: " .. (status.stats.functions or 0))
|
||||
table.insert(lines, " Classes: " .. (status.stats.classes or 0))
|
||||
table.insert(lines, " Exports: " .. (status.stats.exports or 0))
|
||||
else
|
||||
table.insert(lines, "Status: Not indexed")
|
||||
table.insert(lines, "Run :CoderIndexProject to index")
|
||||
end
|
||||
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "Memories:")
|
||||
table.insert(lines, " Patterns: " .. mem_stats.patterns)
|
||||
table.insert(lines, " Conventions: " .. mem_stats.conventions)
|
||||
table.insert(lines, " Symbols: " .. mem_stats.symbols)
|
||||
|
||||
utils.notify(table.concat(lines, "\n"))
|
||||
end
|
||||
|
||||
--- Show learned memories
|
||||
local function cmd_memories()
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
|
||||
local all = memory.get_all()
|
||||
local lines = {
|
||||
"Learned Memories",
|
||||
"================",
|
||||
"",
|
||||
"Patterns:",
|
||||
}
|
||||
|
||||
local pattern_count = 0
|
||||
for _, mem in pairs(all.patterns) do
|
||||
pattern_count = pattern_count + 1
|
||||
if pattern_count <= 10 then
|
||||
table.insert(lines, " - " .. (mem.content or ""):sub(1, 60))
|
||||
end
|
||||
end
|
||||
if pattern_count > 10 then
|
||||
table.insert(lines, " ... and " .. (pattern_count - 10) .. " more")
|
||||
elseif pattern_count == 0 then
|
||||
table.insert(lines, " (none)")
|
||||
end
|
||||
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "Conventions:")
|
||||
|
||||
local conv_count = 0
|
||||
for _, mem in pairs(all.conventions) do
|
||||
conv_count = conv_count + 1
|
||||
if conv_count <= 10 then
|
||||
table.insert(lines, " - " .. (mem.content or ""):sub(1, 60))
|
||||
end
|
||||
end
|
||||
if conv_count > 10 then
|
||||
table.insert(lines, " ... and " .. (conv_count - 10) .. " more")
|
||||
elseif conv_count == 0 then
|
||||
table.insert(lines, " (none)")
|
||||
end
|
||||
|
||||
utils.notify(table.concat(lines, "\n"))
|
||||
end
|
||||
|
||||
--- Clear memories
|
||||
---@param pattern string|nil Optional pattern to match
|
||||
local function cmd_forget(pattern)
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
|
||||
if not pattern or pattern == "" then
|
||||
-- Confirm before clearing all
|
||||
vim.ui.select({ "Yes", "No" }, {
|
||||
prompt = "Clear all memories?",
|
||||
}, function(choice)
|
||||
if choice == "Yes" then
|
||||
memory.clear()
|
||||
utils.notify("All memories cleared", vim.log.levels.INFO)
|
||||
end
|
||||
end)
|
||||
else
|
||||
memory.clear(pattern)
|
||||
utils.notify("Cleared memories matching: " .. pattern, vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
|
||||
--- Main command dispatcher
|
||||
---@param args table Command arguments
|
||||
--- Show LLM accuracy statistics
|
||||
local function cmd_llm_stats()
|
||||
local llm = require("codetyper.core.llm")
|
||||
local stats = llm.get_accuracy_stats()
|
||||
|
||||
local lines = {
|
||||
"LLM Provider Accuracy Statistics",
|
||||
"================================",
|
||||
"",
|
||||
string.format("Ollama:"),
|
||||
string.format(" Total requests: %d", stats.ollama.total),
|
||||
string.format(" Correct: %d", stats.ollama.correct),
|
||||
string.format(" Accuracy: %.1f%%", stats.ollama.accuracy * 100),
|
||||
"",
|
||||
string.format("Copilot:"),
|
||||
string.format(" Total requests: %d", stats.copilot.total),
|
||||
string.format(" Correct: %d", stats.copilot.correct),
|
||||
string.format(" Accuracy: %.1f%%", stats.copilot.accuracy * 100),
|
||||
"",
|
||||
"Note: Smart selection prefers Ollama when brain memories",
|
||||
"provide enough context. Accuracy improves over time via",
|
||||
"pondering (verification with other LLMs).",
|
||||
}
|
||||
|
||||
vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
--- Report feedback on last LLM response
|
||||
---@param was_good boolean Whether the response was good
|
||||
local function cmd_llm_feedback(was_good)
|
||||
local llm = require("codetyper.core.llm")
|
||||
-- Default to ollama for feedback
|
||||
local provider = "ollama"
|
||||
|
||||
llm.report_feedback(provider, was_good)
|
||||
local feedback_type = was_good and "positive" or "negative"
|
||||
utils.notify(string.format("Reported %s feedback for %s", feedback_type, provider), vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
--- Reset LLM accuracy statistics
|
||||
local function cmd_llm_reset_stats()
|
||||
local selector = require("codetyper.core.llm.selector")
|
||||
selector.reset_accuracy_stats()
|
||||
utils.notify("LLM accuracy statistics reset", vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
local function coder_cmd(args)
|
||||
local subcommand = args.fargs[1] or "toggle"
|
||||
|
||||
local commands = {
|
||||
tree = cmd_tree,
|
||||
["tree-view"] = cmd_tree_view,
|
||||
reset = cmd_reset,
|
||||
gitignore = cmd_gitignore,
|
||||
["transform-selection"] = transform.cmd_transform_selection,
|
||||
["index-project"] = cmd_index_project,
|
||||
["index-status"] = cmd_index_status,
|
||||
["llm-stats"] = cmd_llm_stats,
|
||||
["llm-reset-stats"] = cmd_llm_reset_stats,
|
||||
["cost"] = function()
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.toggle()
|
||||
end,
|
||||
["cost-clear"] = function()
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.clear()
|
||||
end,
|
||||
["credentials"] = function()
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
credentials.show_status()
|
||||
end,
|
||||
["switch-provider"] = function()
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
credentials.interactive_switch_provider()
|
||||
end,
|
||||
["model"] = function(args)
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local provider = config.llm.provider
|
||||
|
||||
if provider ~= "copilot" then
|
||||
utils.notify(
|
||||
"CoderModel is only available when using Copilot provider. Current: " .. provider:upper(),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local model_arg = args.fargs[2]
|
||||
if model_arg and model_arg ~= "" then
|
||||
local cost = credentials.get_copilot_model_cost(model_arg) or "custom"
|
||||
credentials.set_credentials("copilot", { model = model_arg, configured = true })
|
||||
utils.notify("Copilot model set to: " .. model_arg .. " — " .. cost, vim.log.levels.INFO)
|
||||
else
|
||||
credentials.interactive_copilot_config(true)
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
local cmd_fn = commands[subcommand]
|
||||
if cmd_fn then
|
||||
cmd_fn(args)
|
||||
else
|
||||
utils.notify("Unknown subcommand: " .. subcommand, vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
--- Setup all commands
|
||||
function M.setup()
|
||||
vim.api.nvim_create_user_command("Coder", coder_cmd, {
|
||||
nargs = "?",
|
||||
complete = function()
|
||||
return {
|
||||
"tree",
|
||||
"tree-view",
|
||||
"reset",
|
||||
"gitignore",
|
||||
"transform-selection",
|
||||
"index-project",
|
||||
"index-status",
|
||||
"llm-stats",
|
||||
"llm-reset-stats",
|
||||
"cost",
|
||||
"cost-clear",
|
||||
"credentials",
|
||||
"switch-provider",
|
||||
"model",
|
||||
}
|
||||
end,
|
||||
desc = "Codetyper.nvim commands",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command("CoderTree", function()
|
||||
cmd_tree()
|
||||
end, { desc = "Refresh tree.log" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderTreeView", function()
|
||||
cmd_tree_view()
|
||||
end, { desc = "View tree.log" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderTransformSelection", function()
|
||||
transform.cmd_transform_selection()
|
||||
end, { desc = "Transform visual selection with custom prompt input" })
|
||||
|
||||
-- Project indexer commands
|
||||
vim.api.nvim_create_user_command("CoderIndexProject", function()
|
||||
cmd_index_project()
|
||||
end, { desc = "Index the entire project" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderIndexStatus", function()
|
||||
cmd_index_status()
|
||||
end, { desc = "Show project index status" })
|
||||
|
||||
-- TODO: re-enable CoderMemories, CoderForget when memory UI is reworked
|
||||
-- TODO: re-enable CoderFeedback when feedback loop is reworked
|
||||
-- TODO: re-enable CoderBrain when brain management UI is reworked
|
||||
|
||||
-- Cost estimation command
|
||||
vim.api.nvim_create_user_command("CoderCost", function()
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.toggle()
|
||||
end, { desc = "Show LLM cost estimation window" })
|
||||
|
||||
-- TODO: re-enable CoderAddApiKey when multi-provider support returns
|
||||
|
||||
vim.api.nvim_create_user_command("CoderCredentials", function()
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
credentials.show_status()
|
||||
end, { desc = "Show credentials status" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderSwitchProvider", function()
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
credentials.interactive_switch_provider()
|
||||
end, { desc = "Switch active LLM provider" })
|
||||
|
||||
-- Quick model switcher command (Copilot only)
|
||||
vim.api.nvim_create_user_command("CoderModel", function(opts)
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local provider = config.llm.provider
|
||||
|
||||
-- Only available for Copilot provider
|
||||
if provider ~= "copilot" then
|
||||
utils.notify(
|
||||
"CoderModel is only available when using Copilot provider. Current: " .. provider:upper(),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
-- If an argument is provided, set the model directly
|
||||
if opts.args and opts.args ~= "" then
|
||||
local cost = credentials.get_copilot_model_cost(opts.args) or "custom"
|
||||
credentials.set_credentials("copilot", { model = opts.args, configured = true })
|
||||
utils.notify("Copilot model set to: " .. opts.args .. " — " .. cost, vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Show interactive selector with costs (silent mode - no OAuth message)
|
||||
credentials.interactive_copilot_config(true)
|
||||
end, {
|
||||
nargs = "?",
|
||||
desc = "Quick switch Copilot model (only available with Copilot provider)",
|
||||
complete = function()
|
||||
local codetyper = require("codetyper")
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
local config = codetyper.get_config()
|
||||
if config.llm.provider == "copilot" then
|
||||
return credentials.get_copilot_model_names()
|
||||
end
|
||||
return {}
|
||||
end,
|
||||
})
|
||||
|
||||
-- Setup default keymaps
|
||||
M.setup_keymaps()
|
||||
end
|
||||
|
||||
--- Setup default keymaps for transform commands
|
||||
function M.setup_keymaps()
|
||||
-- Visual mode: transform selection with custom prompt input
|
||||
vim.keymap.set("v", "<leader>ctt", function()
|
||||
transform.cmd_transform_selection()
|
||||
end, {
|
||||
silent = true,
|
||||
desc = "Coder: Transform selection with prompt",
|
||||
})
|
||||
-- Normal mode: prompt only (no selection); request is entered in the prompt
|
||||
vim.keymap.set("n", "<leader>ctt", function()
|
||||
transform.cmd_transform_selection()
|
||||
end, {
|
||||
silent = true,
|
||||
desc = "Coder: Open prompt window",
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
23
lua/codetyper/adapters/nvim/commands/cmd_forget.lua
Normal file
23
lua/codetyper/adapters/nvim/commands/cmd_forget.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Clear memories
|
||||
---@param pattern string|nil Optional pattern to match
|
||||
local function cmd_forget(pattern)
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
|
||||
if not pattern or pattern == "" then
|
||||
vim.ui.select({ "Yes", "No" }, {
|
||||
prompt = "Clear all memories?",
|
||||
}, function(choice)
|
||||
if choice == "Yes" then
|
||||
memory.clear()
|
||||
utils.notify("All memories cleared", vim.log.levels.INFO)
|
||||
end
|
||||
end)
|
||||
else
|
||||
memory.clear(pattern)
|
||||
utils.notify("Cleared memories matching: " .. pattern, vim.log.levels.INFO)
|
||||
end
|
||||
end
|
||||
|
||||
return cmd_forget
|
||||
7
lua/codetyper/adapters/nvim/commands/cmd_gitignore.lua
Normal file
7
lua/codetyper/adapters/nvim/commands/cmd_gitignore.lua
Normal file
@@ -0,0 +1,7 @@
|
||||
--- Force update gitignore
|
||||
local function cmd_gitignore()
|
||||
local gitignore = require("codetyper.support.gitignore")
|
||||
gitignore.force_update()
|
||||
end
|
||||
|
||||
return cmd_gitignore
|
||||
25
lua/codetyper/adapters/nvim/commands/cmd_index_project.lua
Normal file
25
lua/codetyper/adapters/nvim/commands/cmd_index_project.lua
Normal file
@@ -0,0 +1,25 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Index the entire project
|
||||
local function cmd_index_project()
|
||||
local indexer = require("codetyper.features.indexer")
|
||||
|
||||
utils.notify("Indexing project...", vim.log.levels.INFO)
|
||||
|
||||
indexer.index_project(function(index)
|
||||
if index then
|
||||
local msg = string.format(
|
||||
"Indexed: %d files, %d functions, %d classes, %d exports",
|
||||
index.stats.files,
|
||||
index.stats.functions,
|
||||
index.stats.classes,
|
||||
index.stats.exports
|
||||
)
|
||||
utils.notify(msg, vim.log.levels.INFO)
|
||||
else
|
||||
utils.notify("Failed to index project", vim.log.levels.ERROR)
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
return cmd_index_project
|
||||
41
lua/codetyper/adapters/nvim/commands/cmd_index_status.lua
Normal file
41
lua/codetyper/adapters/nvim/commands/cmd_index_status.lua
Normal file
@@ -0,0 +1,41 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Show index status
|
||||
local function cmd_index_status()
|
||||
local indexer = require("codetyper.features.indexer")
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
|
||||
local status = indexer.get_status()
|
||||
local mem_stats = memory.get_stats()
|
||||
|
||||
local lines = {
|
||||
"Project Index Status",
|
||||
"====================",
|
||||
"",
|
||||
}
|
||||
|
||||
if status.indexed then
|
||||
table.insert(lines, "Status: Indexed")
|
||||
table.insert(lines, "Project Type: " .. (status.project_type or "unknown"))
|
||||
table.insert(lines, "Last Indexed: " .. os.date("%Y-%m-%d %H:%M:%S", status.last_indexed))
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "Stats:")
|
||||
table.insert(lines, " Files: " .. (status.stats.files or 0))
|
||||
table.insert(lines, " Functions: " .. (status.stats.functions or 0))
|
||||
table.insert(lines, " Classes: " .. (status.stats.classes or 0))
|
||||
table.insert(lines, " Exports: " .. (status.stats.exports or 0))
|
||||
else
|
||||
table.insert(lines, "Status: Not indexed")
|
||||
table.insert(lines, "Run :CoderIndexProject to index")
|
||||
end
|
||||
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "Memories:")
|
||||
table.insert(lines, " Patterns: " .. mem_stats.patterns)
|
||||
table.insert(lines, " Conventions: " .. mem_stats.conventions)
|
||||
table.insert(lines, " Symbols: " .. mem_stats.symbols)
|
||||
|
||||
utils.notify(table.concat(lines, "\n"))
|
||||
end
|
||||
|
||||
return cmd_index_status
|
||||
14
lua/codetyper/adapters/nvim/commands/cmd_llm_feedback.lua
Normal file
14
lua/codetyper/adapters/nvim/commands/cmd_llm_feedback.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Report feedback on last LLM response
|
||||
---@param was_good boolean Whether the response was good
|
||||
local function cmd_llm_feedback(was_good)
|
||||
local llm = require("codetyper.core.llm")
|
||||
local provider = "ollama"
|
||||
|
||||
llm.report_feedback(provider, was_good)
|
||||
local feedback_type = was_good and "positive" or "negative"
|
||||
utils.notify(string.format("Reported %s feedback for %s", feedback_type, provider), vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
return cmd_llm_feedback
|
||||
10
lua/codetyper/adapters/nvim/commands/cmd_llm_reset_stats.lua
Normal file
10
lua/codetyper/adapters/nvim/commands/cmd_llm_reset_stats.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Reset LLM accuracy statistics
|
||||
local function cmd_llm_reset_stats()
|
||||
local selector = require("codetyper.core.llm.selector")
|
||||
selector.reset_accuracy_stats()
|
||||
utils.notify("LLM accuracy statistics reset", vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
return cmd_llm_reset_stats
|
||||
28
lua/codetyper/adapters/nvim/commands/cmd_llm_stats.lua
Normal file
28
lua/codetyper/adapters/nvim/commands/cmd_llm_stats.lua
Normal file
@@ -0,0 +1,28 @@
|
||||
--- Show LLM accuracy statistics
|
||||
local function cmd_llm_stats()
|
||||
local llm = require("codetyper.core.llm")
|
||||
local stats = llm.get_accuracy_stats()
|
||||
|
||||
local lines = {
|
||||
"LLM Provider Accuracy Statistics",
|
||||
"================================",
|
||||
"",
|
||||
string.format("Ollama:"),
|
||||
string.format(" Total requests: %d", stats.ollama.total),
|
||||
string.format(" Correct: %d", stats.ollama.correct),
|
||||
string.format(" Accuracy: %.1f%%", stats.ollama.accuracy * 100),
|
||||
"",
|
||||
string.format("Copilot:"),
|
||||
string.format(" Total requests: %d", stats.copilot.total),
|
||||
string.format(" Correct: %d", stats.copilot.correct),
|
||||
string.format(" Accuracy: %.1f%%", stats.copilot.accuracy * 100),
|
||||
"",
|
||||
"Note: Smart selection prefers Ollama when brain memories",
|
||||
"provide enough context. Accuracy improves over time via",
|
||||
"pondering (verification with other LLMs).",
|
||||
}
|
||||
|
||||
vim.notify(table.concat(lines, "\n"), vim.log.levels.INFO)
|
||||
end
|
||||
|
||||
return cmd_llm_stats
|
||||
47
lua/codetyper/adapters/nvim/commands/cmd_memories.lua
Normal file
47
lua/codetyper/adapters/nvim/commands/cmd_memories.lua
Normal file
@@ -0,0 +1,47 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Show learned memories
|
||||
local function cmd_memories()
|
||||
local memory = require("codetyper.features.indexer.memory")
|
||||
|
||||
local all = memory.get_all()
|
||||
local lines = {
|
||||
"Learned Memories",
|
||||
"================",
|
||||
"",
|
||||
"Patterns:",
|
||||
}
|
||||
|
||||
local pattern_count = 0
|
||||
for _, mem in pairs(all.patterns) do
|
||||
pattern_count = pattern_count + 1
|
||||
if pattern_count <= 10 then
|
||||
table.insert(lines, " - " .. (mem.content or ""):sub(1, 60))
|
||||
end
|
||||
end
|
||||
if pattern_count > 10 then
|
||||
table.insert(lines, " ... and " .. (pattern_count - 10) .. " more")
|
||||
elseif pattern_count == 0 then
|
||||
table.insert(lines, " (none)")
|
||||
end
|
||||
|
||||
table.insert(lines, "")
|
||||
table.insert(lines, "Conventions:")
|
||||
|
||||
local conv_count = 0
|
||||
for _, mem in pairs(all.conventions) do
|
||||
conv_count = conv_count + 1
|
||||
if conv_count <= 10 then
|
||||
table.insert(lines, " - " .. (mem.content or ""):sub(1, 60))
|
||||
end
|
||||
end
|
||||
if conv_count > 10 then
|
||||
table.insert(lines, " ... and " .. (conv_count - 10) .. " more")
|
||||
elseif conv_count == 0 then
|
||||
table.insert(lines, " (none)")
|
||||
end
|
||||
|
||||
utils.notify(table.concat(lines, "\n"))
|
||||
end
|
||||
|
||||
return cmd_memories
|
||||
7
lua/codetyper/adapters/nvim/commands/cmd_reset.lua
Normal file
7
lua/codetyper/adapters/nvim/commands/cmd_reset.lua
Normal file
@@ -0,0 +1,7 @@
|
||||
--- Reset processed prompts to allow re-processing
|
||||
local function cmd_reset()
|
||||
local reset_processed = require("codetyper.adapters.nvim.autocmds.reset_processed")
|
||||
reset_processed()
|
||||
end
|
||||
|
||||
return cmd_reset
|
||||
13
lua/codetyper/adapters/nvim/commands/cmd_tree.lua
Normal file
13
lua/codetyper/adapters/nvim/commands/cmd_tree.lua
Normal file
@@ -0,0 +1,13 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Refresh tree.log manually
|
||||
local function cmd_tree()
|
||||
local tree = require("codetyper.support.tree")
|
||||
if tree.update_tree_log() then
|
||||
utils.notify("Tree log updated: " .. tree.get_tree_log_path())
|
||||
else
|
||||
utils.notify("Failed to update tree log", vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
return cmd_tree
|
||||
20
lua/codetyper/adapters/nvim/commands/cmd_tree_view.lua
Normal file
20
lua/codetyper/adapters/nvim/commands/cmd_tree_view.lua
Normal file
@@ -0,0 +1,20 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
|
||||
--- Open tree.log file in a vertical split
|
||||
local function cmd_tree_view()
|
||||
local tree = require("codetyper.support.tree")
|
||||
local tree_log_path = tree.get_tree_log_path()
|
||||
|
||||
if not tree_log_path then
|
||||
utils.notify("Could not find tree.log", vim.log.levels.WARN)
|
||||
return
|
||||
end
|
||||
|
||||
tree.update_tree_log()
|
||||
|
||||
vim.cmd("vsplit " .. vim.fn.fnameescape(tree_log_path))
|
||||
vim.bo.readonly = true
|
||||
vim.bo.modifiable = false
|
||||
end
|
||||
|
||||
return cmd_tree_view
|
||||
80
lua/codetyper/adapters/nvim/commands/coder_cmd.lua
Normal file
80
lua/codetyper/adapters/nvim/commands/coder_cmd.lua
Normal file
@@ -0,0 +1,80 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
local transform = require("codetyper.core.transform")
|
||||
local cmd_tree = require("codetyper.adapters.nvim.commands.cmd_tree")
|
||||
local cmd_tree_view = require("codetyper.adapters.nvim.commands.cmd_tree_view")
|
||||
local cmd_reset = require("codetyper.adapters.nvim.commands.cmd_reset")
|
||||
local cmd_gitignore = require("codetyper.adapters.nvim.commands.cmd_gitignore")
|
||||
local cmd_index_project = require("codetyper.adapters.nvim.commands.cmd_index_project")
|
||||
local cmd_index_status = require("codetyper.adapters.nvim.commands.cmd_index_status")
|
||||
local cmd_llm_stats = require("codetyper.adapters.nvim.commands.cmd_llm_stats")
|
||||
local cmd_llm_reset_stats = require("codetyper.adapters.nvim.commands.cmd_llm_reset_stats")
|
||||
|
||||
--- Main command dispatcher
|
||||
---@param args table Command arguments
|
||||
local function coder_cmd(args)
|
||||
local subcommand = args.fargs[1] or "version"
|
||||
|
||||
local commands = {
|
||||
["version"] = function()
|
||||
local codetyper = require("codetyper")
|
||||
utils.notify("Codetyper.nvim " .. codetyper.version, vim.log.levels.INFO)
|
||||
end,
|
||||
tree = cmd_tree,
|
||||
["tree-view"] = cmd_tree_view,
|
||||
reset = cmd_reset,
|
||||
gitignore = cmd_gitignore,
|
||||
["transform-selection"] = transform.cmd_transform_selection,
|
||||
["index-project"] = cmd_index_project,
|
||||
["index-status"] = cmd_index_status,
|
||||
["llm-stats"] = cmd_llm_stats,
|
||||
["llm-reset-stats"] = cmd_llm_reset_stats,
|
||||
["cost"] = function()
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.toggle()
|
||||
end,
|
||||
["cost-clear"] = function()
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.clear()
|
||||
end,
|
||||
["credentials"] = function()
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
credentials.show_status()
|
||||
end,
|
||||
["switch-provider"] = function()
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
credentials.interactive_switch_provider()
|
||||
end,
|
||||
["model"] = function(cmd_args)
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local provider = config.llm.provider
|
||||
|
||||
if provider ~= "copilot" then
|
||||
utils.notify(
|
||||
"CoderModel is only available when using Copilot provider. Current: " .. provider:upper(),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
local model_arg = cmd_args.fargs[2]
|
||||
if model_arg and model_arg ~= "" then
|
||||
local model_cost = credentials.get_copilot_model_cost(model_arg) or "custom"
|
||||
credentials.set_credentials("copilot", { model = model_arg, configured = true })
|
||||
utils.notify("Copilot model set to: " .. model_arg .. " — " .. model_cost, vim.log.levels.INFO)
|
||||
else
|
||||
credentials.interactive_copilot_config(true)
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
local cmd_fn = commands[subcommand]
|
||||
if cmd_fn then
|
||||
cmd_fn(args)
|
||||
else
|
||||
utils.notify("Unknown subcommand: " .. subcommand, vim.log.levels.ERROR)
|
||||
end
|
||||
end
|
||||
|
||||
return coder_cmd
|
||||
116
lua/codetyper/adapters/nvim/commands/setup.lua
Normal file
116
lua/codetyper/adapters/nvim/commands/setup.lua
Normal file
@@ -0,0 +1,116 @@
|
||||
local utils = require("codetyper.support.utils")
|
||||
local transform = require("codetyper.core.transform")
|
||||
local coder_cmd = require("codetyper.adapters.nvim.commands.coder_cmd")
|
||||
local cmd_tree = require("codetyper.adapters.nvim.commands.cmd_tree")
|
||||
local cmd_tree_view = require("codetyper.adapters.nvim.commands.cmd_tree_view")
|
||||
local cmd_index_project = require("codetyper.adapters.nvim.commands.cmd_index_project")
|
||||
local cmd_index_status = require("codetyper.adapters.nvim.commands.cmd_index_status")
|
||||
local setup_keymaps = require("codetyper.adapters.nvim.commands.setup_keymaps")
|
||||
|
||||
--- Setup all commands
|
||||
local function setup()
|
||||
vim.api.nvim_create_user_command("Coder", coder_cmd, {
|
||||
nargs = "?",
|
||||
complete = function()
|
||||
return {
|
||||
"version",
|
||||
"tree",
|
||||
"tree-view",
|
||||
"reset",
|
||||
"gitignore",
|
||||
"transform-selection",
|
||||
"index-project",
|
||||
"index-status",
|
||||
"llm-stats",
|
||||
"llm-reset-stats",
|
||||
"cost",
|
||||
"cost-clear",
|
||||
"credentials",
|
||||
"switch-provider",
|
||||
"model",
|
||||
}
|
||||
end,
|
||||
desc = "Codetyper.nvim commands",
|
||||
})
|
||||
|
||||
vim.api.nvim_create_user_command("CoderTree", function()
|
||||
cmd_tree()
|
||||
end, { desc = "Refresh tree.log" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderTreeView", function()
|
||||
cmd_tree_view()
|
||||
end, { desc = "View tree.log" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderTransformSelection", function()
|
||||
transform.cmd_transform_selection()
|
||||
end, { desc = "Transform visual selection with custom prompt input" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderIndexProject", function()
|
||||
cmd_index_project()
|
||||
end, { desc = "Index the entire project" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderIndexStatus", function()
|
||||
cmd_index_status()
|
||||
end, { desc = "Show project index status" })
|
||||
|
||||
-- TODO: re-enable CoderMemories, CoderForget when memory UI is reworked
|
||||
-- TODO: re-enable CoderFeedback when feedback loop is reworked
|
||||
-- TODO: re-enable CoderBrain when brain management UI is reworked
|
||||
|
||||
vim.api.nvim_create_user_command("CoderCost", function()
|
||||
local cost = require("codetyper.core.cost")
|
||||
cost.toggle()
|
||||
end, { desc = "Show LLM cost estimation window" })
|
||||
|
||||
-- TODO: re-enable CoderAddApiKey when multi-provider support returns
|
||||
|
||||
vim.api.nvim_create_user_command("CoderCredentials", function()
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
credentials.show_status()
|
||||
end, { desc = "Show credentials status" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderSwitchProvider", function()
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
credentials.interactive_switch_provider()
|
||||
end, { desc = "Switch active LLM provider" })
|
||||
|
||||
vim.api.nvim_create_user_command("CoderModel", function(opts)
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
local codetyper = require("codetyper")
|
||||
local config = codetyper.get_config()
|
||||
local provider = config.llm.provider
|
||||
|
||||
if provider ~= "copilot" then
|
||||
utils.notify(
|
||||
"CoderModel is only available when using Copilot provider. Current: " .. provider:upper(),
|
||||
vim.log.levels.WARN
|
||||
)
|
||||
return
|
||||
end
|
||||
|
||||
if opts.args and opts.args ~= "" then
|
||||
local model_cost = credentials.get_copilot_model_cost(opts.args) or "custom"
|
||||
credentials.set_credentials("copilot", { model = opts.args, configured = true })
|
||||
utils.notify("Copilot model set to: " .. opts.args .. " — " .. model_cost, vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
credentials.interactive_copilot_config(true)
|
||||
end, {
|
||||
nargs = "?",
|
||||
desc = "Quick switch Copilot model (only available with Copilot provider)",
|
||||
complete = function()
|
||||
local codetyper = require("codetyper")
|
||||
local credentials = require("codetyper.config.credentials")
|
||||
local config = codetyper.get_config()
|
||||
if config.llm.provider == "copilot" then
|
||||
return credentials.get_copilot_model_names()
|
||||
end
|
||||
return {}
|
||||
end,
|
||||
})
|
||||
|
||||
setup_keymaps()
|
||||
end
|
||||
|
||||
return setup
|
||||
19
lua/codetyper/adapters/nvim/commands/setup_keymaps.lua
Normal file
19
lua/codetyper/adapters/nvim/commands/setup_keymaps.lua
Normal file
@@ -0,0 +1,19 @@
|
||||
local transform = require("codetyper.core.transform")
|
||||
|
||||
--- Setup default keymaps for transform commands
|
||||
local function setup_keymaps()
|
||||
vim.keymap.set("v", "<leader>ctt", function()
|
||||
transform.cmd_transform_selection()
|
||||
end, {
|
||||
silent = true,
|
||||
desc = "Coder: Transform selection with prompt",
|
||||
})
|
||||
vim.keymap.set("n", "<leader>ctt", function()
|
||||
transform.cmd_transform_selection()
|
||||
end, {
|
||||
silent = true,
|
||||
desc = "Coder: Open prompt window",
|
||||
})
|
||||
end
|
||||
|
||||
return setup_keymaps
|
||||
@@ -1,381 +0,0 @@
|
||||
---@mod codetyper.agent.context_modal Modal for additional context input
|
||||
---@brief [[
|
||||
--- Opens a floating window for user to provide additional context
|
||||
--- when the LLM requests more information.
|
||||
---@brief ]]
|
||||
|
||||
local M = {}
|
||||
|
||||
---@class ContextModalState
|
||||
---@field buf number|nil Buffer number
|
||||
---@field win number|nil Window number
|
||||
---@field original_event table|nil Original prompt event
|
||||
---@field callback function|nil Callback with additional context
|
||||
---@field llm_response string|nil LLM's response asking for context
|
||||
|
||||
local state = {
|
||||
buf = nil,
|
||||
win = nil,
|
||||
original_event = nil,
|
||||
callback = nil,
|
||||
llm_response = nil,
|
||||
attached_files = nil,
|
||||
}
|
||||
|
||||
--- Close the context modal
|
||||
function M.close()
|
||||
if state.win and vim.api.nvim_win_is_valid(state.win) then
|
||||
vim.api.nvim_win_close(state.win, true)
|
||||
end
|
||||
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
|
||||
vim.api.nvim_buf_delete(state.buf, { force = true })
|
||||
end
|
||||
state.win = nil
|
||||
state.buf = nil
|
||||
state.original_event = nil
|
||||
state.callback = nil
|
||||
state.llm_response = nil
|
||||
end
|
||||
|
||||
--- Submit the additional context
|
||||
local function submit()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(state.buf, 0, -1, false)
|
||||
local additional_context = table.concat(lines, "\n")
|
||||
|
||||
-- Trim whitespace
|
||||
additional_context = additional_context:match("^%s*(.-)%s*$") or additional_context
|
||||
|
||||
if additional_context == "" then
|
||||
M.close()
|
||||
return
|
||||
end
|
||||
|
||||
local original_event = state.original_event
|
||||
local callback = state.callback
|
||||
|
||||
M.close()
|
||||
|
||||
if callback and original_event then
|
||||
-- Pass attached_files as third optional parameter
|
||||
callback(original_event, additional_context, state.attached_files)
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
--- Parse requested file paths from LLM response and resolve to full paths
|
||||
local function parse_requested_files(response)
|
||||
if not response or response == "" then
|
||||
return {}
|
||||
end
|
||||
|
||||
local cwd = vim.fn.getcwd()
|
||||
local candidates = {}
|
||||
local seen = {}
|
||||
|
||||
for path in response:gmatch("`([%w%._%-%/]+%.[%w_]+)`") do
|
||||
if not seen[path] then
|
||||
table.insert(candidates, path)
|
||||
seen[path] = true
|
||||
end
|
||||
end
|
||||
for path in response:gmatch("([%w%._%-%/]+%.[%w_]+)") do
|
||||
if not seen[path] then
|
||||
table.insert(candidates, path)
|
||||
seen[path] = true
|
||||
end
|
||||
end
|
||||
|
||||
-- Resolve to full paths using cwd and glob
|
||||
local resolved = {}
|
||||
for _, p in ipairs(candidates) do
|
||||
local full = nil
|
||||
if p:sub(1,1) == "/" and vim.fn.filereadable(p) == 1 then
|
||||
full = p
|
||||
else
|
||||
local try1 = cwd .. "/" .. p
|
||||
if vim.fn.filereadable(try1) == 1 then
|
||||
full = try1
|
||||
else
|
||||
local tail = p:match("[^/]+$") or p
|
||||
local matches = vim.fn.globpath(cwd, "**/" .. tail, false, true)
|
||||
if matches and #matches > 0 then
|
||||
full = matches[1]
|
||||
end
|
||||
end
|
||||
end
|
||||
if full and vim.fn.filereadable(full) == 1 then
|
||||
table.insert(resolved, full)
|
||||
end
|
||||
end
|
||||
return resolved
|
||||
end
|
||||
|
||||
|
||||
--- Attach parsed files into the modal buffer and remember them for submission
|
||||
local function attach_requested_files()
|
||||
if not state.llm_response or state.llm_response == "" then
|
||||
return
|
||||
end
|
||||
local files = parse_requested_files(state.llm_response)
|
||||
if #files == 0 then
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
vim.api.nvim_buf_set_lines(state.buf, vim.api.nvim_buf_line_count(state.buf), -1, false, ui_prompts.files_header)
|
||||
return
|
||||
end
|
||||
|
||||
state.attached_files = state.attached_files or {}
|
||||
|
||||
for _, full in ipairs(files) do
|
||||
local ok, lines = pcall(vim.fn.readfile, full)
|
||||
if ok and lines and #lines > 0 then
|
||||
table.insert(state.attached_files, { path = vim.fn.fnamemodify(full, ":~:." ) , full_path = full, content = table.concat(lines, "\n") })
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Attached: " .. full .. " --" })
|
||||
for i, l in ipairs(lines) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + 1 + i, insert_at + 1 + i, false, { l })
|
||||
end
|
||||
else
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Failed to read: " .. full .. " --" })
|
||||
end
|
||||
end
|
||||
-- Move cursor to end and enter insert mode
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
--- Open the context modal
|
||||
---@param original_event table Original prompt event
|
||||
---@param llm_response string LLM's response asking for context
|
||||
---@param callback function(event: table, additional_context: string, attached_files?: table)
|
||||
---@param suggested_commands table[]|nil Optional list of {label,cmd} suggested shell commands
|
||||
function M.open(original_event, llm_response, callback, suggested_commands)
|
||||
-- Close any existing modal
|
||||
M.close()
|
||||
|
||||
state.original_event = original_event
|
||||
state.llm_response = llm_response
|
||||
state.callback = callback
|
||||
|
||||
-- Calculate window size
|
||||
local width = math.min(80, vim.o.columns - 10)
|
||||
local height = 10
|
||||
|
||||
-- Create buffer
|
||||
state.buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.buf].buftype = "nofile"
|
||||
vim.bo[state.buf].bufhidden = "wipe"
|
||||
vim.bo[state.buf].filetype = "markdown"
|
||||
|
||||
-- Create window
|
||||
local row = math.floor((vim.o.lines - height) / 2)
|
||||
local col = math.floor((vim.o.columns - width) / 2)
|
||||
|
||||
state.win = vim.api.nvim_open_win(state.buf, true, {
|
||||
relative = "editor",
|
||||
row = row,
|
||||
col = col,
|
||||
width = width,
|
||||
height = height,
|
||||
style = "minimal",
|
||||
border = "rounded",
|
||||
title = " Additional Context Needed ",
|
||||
title_pos = "center",
|
||||
})
|
||||
|
||||
-- Set window options
|
||||
vim.wo[state.win].wrap = true
|
||||
vim.wo[state.win].cursorline = true
|
||||
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
|
||||
-- Add header showing what the LLM said
|
||||
local header_lines = {
|
||||
ui_prompts.llm_response_header,
|
||||
}
|
||||
|
||||
-- Truncate LLM response for display
|
||||
local response_preview = llm_response or ""
|
||||
if #response_preview > 200 then
|
||||
response_preview = response_preview:sub(1, 200) .. "..."
|
||||
end
|
||||
for line in response_preview:gmatch("[^\n]+") do
|
||||
table.insert(header_lines, "-- " .. line)
|
||||
end
|
||||
|
||||
-- If suggested commands were provided, show them in the header
|
||||
if suggested_commands and #suggested_commands > 0 then
|
||||
table.insert(header_lines, "")
|
||||
table.insert(header_lines, ui_prompts.suggested_commands_header)
|
||||
for i, s in ipairs(suggested_commands) do
|
||||
local label = s.label or s.cmd
|
||||
table.insert(header_lines, string.format("[%d] %s: %s", i, label, s.cmd))
|
||||
end
|
||||
table.insert(header_lines, ui_prompts.commands_hint)
|
||||
end
|
||||
|
||||
table.insert(header_lines, "")
|
||||
table.insert(header_lines, ui_prompts.input_header)
|
||||
table.insert(header_lines, "")
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, header_lines)
|
||||
|
||||
-- Move cursor to the end
|
||||
vim.api.nvim_win_set_cursor(state.win, { #header_lines, 0 })
|
||||
|
||||
-- Set up keymaps
|
||||
local opts = { buffer = state.buf, noremap = true, silent = true }
|
||||
|
||||
-- Submit with Ctrl+Enter or <leader>s
|
||||
vim.keymap.set("n", "<C-CR>", submit, opts)
|
||||
vim.keymap.set("i", "<C-CR>", submit, opts)
|
||||
vim.keymap.set("n", "<leader>s", submit, opts)
|
||||
vim.keymap.set("n", "<CR><CR>", submit, opts)
|
||||
|
||||
-- Attach parsed files (from LLM response)
|
||||
vim.keymap.set("n", "a", function()
|
||||
attach_requested_files()
|
||||
end, opts)
|
||||
|
||||
-- Confirm and submit with 'c' (convenient when doing question round)
|
||||
vim.keymap.set("n", "c", submit, opts)
|
||||
|
||||
-- Quick run of project inspection from modal with <leader>r / <C-r> in insert mode
|
||||
vim.keymap.set("n", "<leader>r", run_project_inspect, opts)
|
||||
vim.keymap.set("i", "<C-r>", function()
|
||||
vim.schedule(run_project_inspect)
|
||||
end, { buffer = state.buf, noremap = true, silent = true })
|
||||
|
||||
-- If suggested commands provided, create per-command keymaps <leader>1..n to run them
|
||||
state.suggested_commands = suggested_commands
|
||||
if suggested_commands and #suggested_commands > 0 then
|
||||
for i, s in ipairs(suggested_commands) do
|
||||
local key = "<leader>" .. tostring(i)
|
||||
vim.keymap.set("n", key, function()
|
||||
-- run this single command and append output
|
||||
if not s or not s.cmd then
|
||||
return
|
||||
end
|
||||
local ok, out = pcall(vim.fn.systemlist, s.cmd)
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Output: " .. s.cmd .. " --" })
|
||||
if ok and out and #out > 0 then
|
||||
for j, line in ipairs(out) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + j, insert_at + j, false, { line })
|
||||
end
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + 1, insert_at + 1, false, { "(no output or command failed)" })
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end, opts)
|
||||
end
|
||||
-- Also map <leader>0 to run all suggested commands
|
||||
vim.keymap.set("n", "<leader>0", function()
|
||||
for _, s in ipairs(suggested_commands) do
|
||||
pcall(function()
|
||||
local ok, out = pcall(vim.fn.systemlist, s.cmd)
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Output: " .. s.cmd .. " --" })
|
||||
if ok and out and #out > 0 then
|
||||
for j, line in ipairs(out) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + j, insert_at + j, false, { line })
|
||||
end
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at + 1, insert_at + 1, false, { "(no output or command failed)" })
|
||||
end
|
||||
end)
|
||||
end
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end, opts)
|
||||
end
|
||||
|
||||
-- Close with Esc or q
|
||||
vim.keymap.set("n", "<Esc>", M.close, opts)
|
||||
vim.keymap.set("n", "q", M.close, opts)
|
||||
|
||||
-- Start in insert mode
|
||||
vim.cmd("startinsert")
|
||||
|
||||
-- Log
|
||||
pcall(function()
|
||||
local logs = require("codetyper.adapters.nvim.ui.logs")
|
||||
logs.add({
|
||||
type = "info",
|
||||
message = "Context modal opened - waiting for user input",
|
||||
})
|
||||
end)
|
||||
end
|
||||
|
||||
--- Run a small set of safe project inspection commands and insert outputs into the modal buffer
|
||||
local function run_project_inspect()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local cmds = {
|
||||
{ label = "List files (ls -la)", cmd = "ls -la" },
|
||||
{ label = "Git status (git status --porcelain)", cmd = "git status --porcelain" },
|
||||
{ label = "Git top (git rev-parse --show-toplevel)", cmd = "git rev-parse --show-toplevel" },
|
||||
{ label = "Show repo files (git ls-files)", cmd = "git ls-files" },
|
||||
}
|
||||
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
local insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos, insert_pos, false, ui_prompts.project_inspect_header)
|
||||
|
||||
for _, c in ipairs(cmds) do
|
||||
local ok, out = pcall(vim.fn.systemlist, c.cmd)
|
||||
if ok and out and #out > 0 then
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos + 2, insert_pos + 2, false, { "-- " .. c.label .. " --" })
|
||||
for i, line in ipairs(out) do
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos + 2 + i, insert_pos + 2 + i, false, { line })
|
||||
end
|
||||
insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos + 2, insert_pos + 2, false, { "-- " .. c.label .. " --", "(no output or command failed)" })
|
||||
insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
end
|
||||
end
|
||||
|
||||
-- Move cursor to end
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
-- Provide a keybinding in the modal to run project inspection commands
|
||||
pcall(function()
|
||||
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
|
||||
vim.keymap.set("n", "<leader>r", run_project_inspect, { buffer = state.buf, noremap = true, silent = true })
|
||||
vim.keymap.set("i", "<C-r>", function()
|
||||
vim.schedule(run_project_inspect)
|
||||
end, { buffer = state.buf, noremap = true, silent = true })
|
||||
end
|
||||
end)
|
||||
|
||||
--- Check if modal is open
|
||||
---@return boolean
|
||||
function M.is_open()
|
||||
return state.win ~= nil and vim.api.nvim_win_is_valid(state.win)
|
||||
end
|
||||
|
||||
--- Setup autocmds for the context modal
|
||||
function M.setup()
|
||||
local group = vim.api.nvim_create_augroup("CodetypeContextModal", { clear = true })
|
||||
|
||||
-- Close context modal when exiting Neovim
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
group = group,
|
||||
callback = function()
|
||||
M.close()
|
||||
end,
|
||||
desc = "Close context modal before exiting Neovim",
|
||||
})
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,55 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local parse_requested_files = require("codetyper.utils.parse_requested_files")
|
||||
|
||||
--- Attach parsed files from LLM response into the modal buffer
|
||||
local function attach_requested_files()
|
||||
if not state.llm_response or state.llm_response == "" then
|
||||
return
|
||||
end
|
||||
|
||||
local resolved_files = parse_requested_files(state.llm_response)
|
||||
|
||||
if #resolved_files == 0 then
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
vim.api.nvim_buf_set_lines(state.buf, vim.api.nvim_buf_line_count(state.buf), -1, false, ui_prompts.files_header)
|
||||
return
|
||||
end
|
||||
|
||||
state.attached_files = state.attached_files or {}
|
||||
|
||||
for _, file_path in ipairs(resolved_files) do
|
||||
local read_success, file_lines = pcall(vim.fn.readfile, file_path)
|
||||
if read_success and file_lines and #file_lines > 0 then
|
||||
table.insert(state.attached_files, {
|
||||
path = vim.fn.fnamemodify(file_path, ":~:."),
|
||||
full_path = file_path,
|
||||
content = table.concat(file_lines, "\n"),
|
||||
})
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Attached: " .. file_path .. " --" })
|
||||
for line_index, line_content in ipairs(file_lines) do
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_at + 1 + line_index,
|
||||
insert_at + 1 + line_index,
|
||||
false,
|
||||
{ line_content }
|
||||
)
|
||||
end
|
||||
else
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_at,
|
||||
insert_at,
|
||||
false,
|
||||
{ "", "-- Failed to read: " .. file_path .. " --" }
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
return attach_requested_files
|
||||
18
lua/codetyper/adapters/nvim/ui/context_modal/close.lua
Normal file
18
lua/codetyper/adapters/nvim/ui/context_modal/close.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Close the context modal and reset state
|
||||
local function close()
|
||||
if state.win and vim.api.nvim_win_is_valid(state.win) then
|
||||
vim.api.nvim_win_close(state.win, true)
|
||||
end
|
||||
if state.buf and vim.api.nvim_buf_is_valid(state.buf) then
|
||||
vim.api.nvim_buf_delete(state.buf, { force = true })
|
||||
end
|
||||
state.win = nil
|
||||
state.buf = nil
|
||||
state.original_event = nil
|
||||
state.callback = nil
|
||||
state.llm_response = nil
|
||||
end
|
||||
|
||||
return close
|
||||
9
lua/codetyper/adapters/nvim/ui/context_modal/is_open.lua
Normal file
9
lua/codetyper/adapters/nvim/ui/context_modal/is_open.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Check if the context modal is currently open
|
||||
---@return boolean
|
||||
local function is_open()
|
||||
return state.win ~= nil and vim.api.nvim_win_is_valid(state.win)
|
||||
end
|
||||
|
||||
return is_open
|
||||
118
lua/codetyper/adapters/nvim/ui/context_modal/open.lua
Normal file
118
lua/codetyper/adapters/nvim/ui/context_modal/open.lua
Normal file
@@ -0,0 +1,118 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local submit = require("codetyper.adapters.nvim.ui.context_modal.submit")
|
||||
local attach_requested_files = require("codetyper.adapters.nvim.ui.context_modal.attach_requested_files")
|
||||
local run_project_inspect = require("codetyper.adapters.nvim.ui.context_modal.run_project_inspect")
|
||||
local run_suggested_command = require("codetyper.adapters.nvim.ui.context_modal.run_suggested_command")
|
||||
local run_all_suggested_commands = require("codetyper.adapters.nvim.ui.context_modal.run_all_suggested_commands")
|
||||
|
||||
--- Open the context modal
|
||||
---@param original_event table Original prompt event
|
||||
---@param llm_response string LLM's response asking for context
|
||||
---@param callback function(event: table, additional_context: string, attached_files?: table)
|
||||
---@param suggested_commands table[]|nil Optional list of {label,cmd} suggested shell commands
|
||||
function M.open(original_event, llm_response, callback, suggested_commands)
|
||||
close()
|
||||
|
||||
state.original_event = original_event
|
||||
state.llm_response = llm_response
|
||||
state.callback = callback
|
||||
|
||||
local width = math.min(80, vim.o.columns - 10)
|
||||
local height = 10
|
||||
|
||||
state.buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.buf].buftype = "nofile"
|
||||
vim.bo[state.buf].bufhidden = "wipe"
|
||||
vim.bo[state.buf].filetype = "markdown"
|
||||
|
||||
local row = math.floor((vim.o.lines - height) / 2)
|
||||
local col = math.floor((vim.o.columns - width) / 2)
|
||||
|
||||
state.win = vim.api.nvim_open_win(state.buf, true, {
|
||||
relative = "editor",
|
||||
row = row,
|
||||
col = col,
|
||||
width = width,
|
||||
height = height,
|
||||
style = "minimal",
|
||||
border = "rounded",
|
||||
title = " Additional Context Needed ",
|
||||
title_pos = "center",
|
||||
})
|
||||
|
||||
vim.wo[state.win].wrap = true
|
||||
vim.wo[state.win].cursorline = true
|
||||
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
|
||||
local header_lines = {
|
||||
ui_prompts.llm_response_header,
|
||||
}
|
||||
|
||||
local response_preview = llm_response or ""
|
||||
if #response_preview > 200 then
|
||||
response_preview = response_preview:sub(1, 200) .. "..."
|
||||
end
|
||||
for line in response_preview:gmatch("[^\n]+") do
|
||||
table.insert(header_lines, "-- " .. line)
|
||||
end
|
||||
|
||||
if suggested_commands and #suggested_commands > 0 then
|
||||
table.insert(header_lines, "")
|
||||
table.insert(header_lines, ui_prompts.suggested_commands_header)
|
||||
for command_index, command in ipairs(suggested_commands) do
|
||||
local label = command.label or command.cmd
|
||||
table.insert(header_lines, string.format("[%d] %s: %s", command_index, label, command.cmd))
|
||||
end
|
||||
table.insert(header_lines, ui_prompts.commands_hint)
|
||||
end
|
||||
|
||||
table.insert(header_lines, "")
|
||||
table.insert(header_lines, ui_prompts.input_header)
|
||||
table.insert(header_lines, "")
|
||||
|
||||
vim.api.nvim_buf_set_lines(state.buf, 0, -1, false, header_lines)
|
||||
vim.api.nvim_win_set_cursor(state.win, { #header_lines, 0 })
|
||||
|
||||
local keymap_opts = { buffer = state.buf, noremap = true, silent = true }
|
||||
|
||||
vim.keymap.set("n", "<C-CR>", submit, keymap_opts)
|
||||
vim.keymap.set("i", "<C-CR>", submit, keymap_opts)
|
||||
vim.keymap.set("n", "<leader>s", submit, keymap_opts)
|
||||
vim.keymap.set("n", "<CR><CR>", submit, keymap_opts)
|
||||
vim.keymap.set("n", "c", submit, keymap_opts)
|
||||
|
||||
vim.keymap.set("n", "a", attach_requested_files, keymap_opts)
|
||||
|
||||
vim.keymap.set("n", "<leader>r", run_project_inspect, keymap_opts)
|
||||
vim.keymap.set("i", "<C-r>", function()
|
||||
vim.schedule(run_project_inspect)
|
||||
end, keymap_opts)
|
||||
|
||||
state.suggested_commands = suggested_commands
|
||||
if suggested_commands and #suggested_commands > 0 then
|
||||
for command_index, command in ipairs(suggested_commands) do
|
||||
local key = "<leader>" .. tostring(command_index)
|
||||
vim.keymap.set("n", key, function()
|
||||
run_suggested_command(command)
|
||||
end, keymap_opts)
|
||||
end
|
||||
|
||||
vim.keymap.set("n", "<leader>0", function()
|
||||
run_all_suggested_commands(suggested_commands)
|
||||
end, keymap_opts)
|
||||
end
|
||||
|
||||
vim.keymap.set("n", "<Esc>", close, keymap_opts)
|
||||
vim.keymap.set("n", "q", close, keymap_opts)
|
||||
|
||||
vim.cmd("startinsert")
|
||||
|
||||
pcall(function()
|
||||
local logs_add = require("codetyper.adapters.nvim.ui.logs.add")
|
||||
logs_add({
|
||||
type = "info",
|
||||
message = "Context modal opened - waiting for user input",
|
||||
})
|
||||
end)
|
||||
end
|
||||
@@ -0,0 +1,15 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local run_suggested_command = require("codetyper.adapters.nvim.ui.context_modal.run_suggested_command")
|
||||
|
||||
--- Run all suggested shell commands and append their outputs to the modal buffer
|
||||
---@param commands table[] List of {label, cmd} suggested command entries
|
||||
local function run_all_suggested_commands(commands)
|
||||
for _, command in ipairs(commands) do
|
||||
pcall(run_suggested_command, command)
|
||||
end
|
||||
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
return run_all_suggested_commands
|
||||
@@ -0,0 +1,56 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Run a small set of safe project inspection commands and insert outputs into the modal buffer
|
||||
local function run_project_inspect()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local inspection_commands = {
|
||||
{ label = "List files (ls -la)", cmd = "ls -la" },
|
||||
{ label = "Git status (git status --porcelain)", cmd = "git status --porcelain" },
|
||||
{ label = "Git top (git rev-parse --show-toplevel)", cmd = "git rev-parse --show-toplevel" },
|
||||
{ label = "Show repo files (git ls-files)", cmd = "git ls-files" },
|
||||
}
|
||||
|
||||
local ui_prompts = require("codetyper.prompts.agents.modal").ui
|
||||
local insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_pos, insert_pos, false, ui_prompts.project_inspect_header)
|
||||
|
||||
for _, command in ipairs(inspection_commands) do
|
||||
local run_success, output_lines = pcall(vim.fn.systemlist, command.cmd)
|
||||
if run_success and output_lines and #output_lines > 0 then
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_pos + 2,
|
||||
insert_pos + 2,
|
||||
false,
|
||||
{ "-- " .. command.label .. " --" }
|
||||
)
|
||||
for line_index, line_content in ipairs(output_lines) do
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_pos + 2 + line_index,
|
||||
insert_pos + 2 + line_index,
|
||||
false,
|
||||
{ line_content }
|
||||
)
|
||||
end
|
||||
insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_pos + 2,
|
||||
insert_pos + 2,
|
||||
false,
|
||||
{ "-- " .. command.label .. " --", "(no output or command failed)" }
|
||||
)
|
||||
insert_pos = vim.api.nvim_buf_line_count(state.buf)
|
||||
end
|
||||
end
|
||||
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
return run_project_inspect
|
||||
@@ -0,0 +1,38 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Run a single suggested shell command and append its output to the modal buffer
|
||||
---@param command table A {label, cmd} suggested command entry
|
||||
local function run_suggested_command(command)
|
||||
if not command or not command.cmd then
|
||||
return
|
||||
end
|
||||
|
||||
local run_success, output_lines = pcall(vim.fn.systemlist, command.cmd)
|
||||
local insert_at = vim.api.nvim_buf_line_count(state.buf)
|
||||
vim.api.nvim_buf_set_lines(state.buf, insert_at, insert_at, false, { "", "-- Output: " .. command.cmd .. " --" })
|
||||
|
||||
if run_success and output_lines and #output_lines > 0 then
|
||||
for line_index, line_content in ipairs(output_lines) do
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_at + line_index,
|
||||
insert_at + line_index,
|
||||
false,
|
||||
{ line_content }
|
||||
)
|
||||
end
|
||||
else
|
||||
vim.api.nvim_buf_set_lines(
|
||||
state.buf,
|
||||
insert_at + 1,
|
||||
insert_at + 1,
|
||||
false,
|
||||
{ "(no output or command failed)" }
|
||||
)
|
||||
end
|
||||
|
||||
vim.api.nvim_win_set_cursor(state.win, { vim.api.nvim_buf_line_count(state.buf), 0 })
|
||||
vim.cmd("startinsert")
|
||||
end
|
||||
|
||||
return run_suggested_command
|
||||
16
lua/codetyper/adapters/nvim/ui/context_modal/setup.lua
Normal file
16
lua/codetyper/adapters/nvim/ui/context_modal/setup.lua
Normal file
@@ -0,0 +1,16 @@
|
||||
local close = require("codetyper.adapters.nvim.ui.context_modal.close")
|
||||
|
||||
--- Setup autocmds for the context modal
|
||||
local function setup()
|
||||
local group = vim.api.nvim_create_augroup("CodetypeContextModal", { clear = true })
|
||||
|
||||
vim.api.nvim_create_autocmd("VimLeavePre", {
|
||||
group = group,
|
||||
callback = function()
|
||||
close()
|
||||
end,
|
||||
desc = "Close context modal before exiting Neovim",
|
||||
})
|
||||
end
|
||||
|
||||
return setup
|
||||
31
lua/codetyper/adapters/nvim/ui/context_modal/submit.lua
Normal file
31
lua/codetyper/adapters/nvim/ui/context_modal/submit.lua
Normal file
@@ -0,0 +1,31 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local close = require("codetyper.adapters.nvim.ui.context_modal.close")
|
||||
|
||||
--- Submit the additional context from the modal buffer
|
||||
local function submit()
|
||||
if not state.buf or not vim.api.nvim_buf_is_valid(state.buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local lines = vim.api.nvim_buf_get_lines(state.buf, 0, -1, false)
|
||||
local additional_context = table.concat(lines, "\n")
|
||||
|
||||
additional_context = additional_context:match("^%s*(.-)%s*$") or additional_context
|
||||
|
||||
if additional_context == "" then
|
||||
close()
|
||||
return
|
||||
end
|
||||
|
||||
local original_event = state.original_event
|
||||
local callback = state.callback
|
||||
local attached_files = state.attached_files
|
||||
|
||||
close()
|
||||
|
||||
if callback and original_event then
|
||||
callback(original_event, additional_context, attached_files)
|
||||
end
|
||||
end
|
||||
|
||||
return submit
|
||||
@@ -1,386 +0,0 @@
|
||||
---@mod codetyper.agent.diff_review Diff review UI for agent changes
|
||||
---
|
||||
--- Provides a lazygit-style window interface for reviewing all changes
|
||||
--- made during an agent session.
|
||||
|
||||
local M = {}
|
||||
|
||||
local utils = require("codetyper.support.utils")
|
||||
local prompts = require("codetyper.prompts.agents.diff")
|
||||
|
||||
|
||||
---@class DiffEntry
|
||||
---@field path string File path
|
||||
---@field operation string "create"|"edit"|"delete"
|
||||
---@field original string|nil Original content (nil for new files)
|
||||
---@field modified string New/modified content
|
||||
---@field approved boolean Whether change was approved
|
||||
---@field applied boolean Whether change was applied
|
||||
|
||||
---@class DiffReviewState
|
||||
---@field entries DiffEntry[] List of changes
|
||||
---@field current_index number Currently selected entry
|
||||
---@field list_buf number|nil File list buffer
|
||||
---@field list_win number|nil File list window
|
||||
---@field diff_buf number|nil Diff view buffer
|
||||
---@field diff_win number|nil Diff view window
|
||||
---@field is_open boolean Whether review UI is open
|
||||
|
||||
local state = {
|
||||
entries = {},
|
||||
current_index = 1,
|
||||
list_buf = nil,
|
||||
list_win = nil,
|
||||
diff_buf = nil,
|
||||
diff_win = nil,
|
||||
is_open = false,
|
||||
}
|
||||
|
||||
--- Clear all collected diffs
|
||||
function M.clear()
|
||||
state.entries = {}
|
||||
state.current_index = 1
|
||||
end
|
||||
|
||||
--- Add a diff entry
|
||||
---@param entry DiffEntry
|
||||
function M.add(entry)
|
||||
table.insert(state.entries, entry)
|
||||
end
|
||||
|
||||
--- Get all entries
|
||||
---@return DiffEntry[]
|
||||
function M.get_entries()
|
||||
return state.entries
|
||||
end
|
||||
|
||||
--- Get entry count
|
||||
---@return number
|
||||
function M.count()
|
||||
return #state.entries
|
||||
end
|
||||
|
||||
--- Generate unified diff between two strings
|
||||
---@param original string|nil
|
||||
---@param modified string
|
||||
---@param filepath string
|
||||
---@return string[]
|
||||
local function generate_diff_lines(original, modified, filepath)
|
||||
local lines = {}
|
||||
local filename = vim.fn.fnamemodify(filepath, ":t")
|
||||
|
||||
if not original then
|
||||
-- New file
|
||||
table.insert(lines, "--- /dev/null")
|
||||
table.insert(lines, "+++ b/" .. filename)
|
||||
table.insert(lines, "@@ -0,0 +1," .. #vim.split(modified, "\n") .. " @@")
|
||||
for _, line in ipairs(vim.split(modified, "\n")) do
|
||||
table.insert(lines, "+" .. line)
|
||||
end
|
||||
else
|
||||
-- Modified file - use vim's diff
|
||||
table.insert(lines, "--- a/" .. filename)
|
||||
table.insert(lines, "+++ b/" .. filename)
|
||||
|
||||
local orig_lines = vim.split(original, "\n")
|
||||
local mod_lines = vim.split(modified, "\n")
|
||||
|
||||
-- Simple diff: show removed and added lines
|
||||
local max_lines = math.max(#orig_lines, #mod_lines)
|
||||
local context_start = 1
|
||||
local in_change = false
|
||||
|
||||
for i = 1, max_lines do
|
||||
local orig = orig_lines[i] or ""
|
||||
local mod = mod_lines[i] or ""
|
||||
|
||||
if orig ~= mod then
|
||||
if not in_change then
|
||||
table.insert(lines, string.format("@@ -%d,%d +%d,%d @@",
|
||||
math.max(1, i - 2), math.min(5, #orig_lines - i + 3),
|
||||
math.max(1, i - 2), math.min(5, #mod_lines - i + 3)))
|
||||
in_change = true
|
||||
end
|
||||
if orig ~= "" then
|
||||
table.insert(lines, "-" .. orig)
|
||||
end
|
||||
if mod ~= "" then
|
||||
table.insert(lines, "+" .. mod)
|
||||
end
|
||||
else
|
||||
if in_change then
|
||||
table.insert(lines, " " .. orig)
|
||||
in_change = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
--- Update the diff view for current entry
|
||||
local function update_diff_view()
|
||||
if not state.diff_buf or not vim.api.nvim_buf_is_valid(state.diff_buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local entry = state.entries[state.current_index]
|
||||
local ui_prompts = prompts.review
|
||||
if not entry then
|
||||
vim.bo[state.diff_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.diff_buf, 0, -1, false, { ui_prompts.messages.no_changes_short })
|
||||
vim.bo[state.diff_buf].modifiable = false
|
||||
return
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
|
||||
-- Header
|
||||
local status_icon = entry.applied and " " or (entry.approved and " " or " ")
|
||||
local op_icon = entry.operation == "create" and "+" or (entry.operation == "delete" and "-" or "~")
|
||||
local current_status = entry.applied and ui_prompts.status.applied
|
||||
or (entry.approved and ui_prompts.status.approved or ui_prompts.status.pending)
|
||||
|
||||
table.insert(lines, string.format(ui_prompts.diff_header.top,
|
||||
status_icon, op_icon, vim.fn.fnamemodify(entry.path, ":t")))
|
||||
table.insert(lines, string.format(ui_prompts.diff_header.path, entry.path))
|
||||
table.insert(lines, string.format(ui_prompts.diff_header.op, entry.operation))
|
||||
table.insert(lines, string.format(ui_prompts.diff_header.status, current_status))
|
||||
table.insert(lines, ui_prompts.diff_header.bottom)
|
||||
table.insert(lines, "")
|
||||
|
||||
-- Diff content
|
||||
local diff_lines = generate_diff_lines(entry.original, entry.modified, entry.path)
|
||||
for _, line in ipairs(diff_lines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
vim.bo[state.diff_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.diff_buf, 0, -1, false, lines)
|
||||
vim.bo[state.diff_buf].modifiable = false
|
||||
vim.bo[state.diff_buf].filetype = "diff"
|
||||
end
|
||||
|
||||
--- Update the file list
|
||||
local function update_file_list()
|
||||
if not state.list_buf or not vim.api.nvim_buf_is_valid(state.list_buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local ui_prompts = prompts.review
|
||||
local lines = {}
|
||||
table.insert(lines, string.format(ui_prompts.list_menu.top, #state.entries))
|
||||
for _, item in ipairs(ui_prompts.list_menu.items) do
|
||||
table.insert(lines, item)
|
||||
end
|
||||
table.insert(lines, ui_prompts.list_menu.bottom)
|
||||
table.insert(lines, "")
|
||||
|
||||
for i, entry in ipairs(state.entries) do
|
||||
local prefix = (i == state.current_index) and "▶ " or " "
|
||||
local status = entry.applied and "" or (entry.approved and "" or "○")
|
||||
local op = entry.operation == "create" and "[+]" or (entry.operation == "delete" and "[-]" or "[~]")
|
||||
local filename = vim.fn.fnamemodify(entry.path, ":t")
|
||||
|
||||
table.insert(lines, string.format("%s%s %s %s", prefix, status, op, filename))
|
||||
end
|
||||
|
||||
if #state.entries == 0 then
|
||||
table.insert(lines, ui_prompts.messages.no_changes)
|
||||
end
|
||||
|
||||
vim.bo[state.list_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.list_buf, 0, -1, false, lines)
|
||||
vim.bo[state.list_buf].modifiable = false
|
||||
|
||||
-- Highlight current line
|
||||
if state.list_win and vim.api.nvim_win_is_valid(state.list_win) then
|
||||
local target_line = 9 + state.current_index - 1
|
||||
if target_line <= vim.api.nvim_buf_line_count(state.list_buf) then
|
||||
vim.api.nvim_win_set_cursor(state.list_win, { target_line, 0 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- Navigate to next entry
|
||||
function M.next()
|
||||
if state.current_index < #state.entries then
|
||||
state.current_index = state.current_index + 1
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
--- Navigate to previous entry
|
||||
function M.prev()
|
||||
if state.current_index > 1 then
|
||||
state.current_index = state.current_index - 1
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
--- Approve current entry
|
||||
function M.approve_current()
|
||||
local entry = state.entries[state.current_index]
|
||||
if entry and not entry.applied then
|
||||
entry.approved = true
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
--- Reject current entry
|
||||
function M.reject_current()
|
||||
local entry = state.entries[state.current_index]
|
||||
if entry and not entry.applied then
|
||||
entry.approved = false
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
--- Approve all entries
|
||||
function M.approve_all()
|
||||
for _, entry in ipairs(state.entries) do
|
||||
if not entry.applied then
|
||||
entry.approved = true
|
||||
end
|
||||
end
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
|
||||
--- Apply approved changes
|
||||
function M.apply_approved()
|
||||
local applied_count = 0
|
||||
|
||||
for _, entry in ipairs(state.entries) do
|
||||
if entry.approved and not entry.applied then
|
||||
if entry.operation == "create" or entry.operation == "edit" then
|
||||
local ok = utils.write_file(entry.path, entry.modified)
|
||||
if ok then
|
||||
entry.applied = true
|
||||
applied_count = applied_count + 1
|
||||
end
|
||||
elseif entry.operation == "delete" then
|
||||
local ok = os.remove(entry.path)
|
||||
if ok then
|
||||
entry.applied = true
|
||||
applied_count = applied_count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
|
||||
if applied_count > 0 then
|
||||
utils.notify(string.format(prompts.review.messages.applied_count, applied_count))
|
||||
end
|
||||
|
||||
return applied_count
|
||||
end
|
||||
|
||||
--- Open the diff review UI
|
||||
function M.open()
|
||||
if state.is_open then
|
||||
return
|
||||
end
|
||||
|
||||
if #state.entries == 0 then
|
||||
utils.notify(prompts.review.messages.no_changes_short, vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
-- Create list buffer
|
||||
state.list_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.list_buf].buftype = "nofile"
|
||||
vim.bo[state.list_buf].bufhidden = "wipe"
|
||||
vim.bo[state.list_buf].swapfile = false
|
||||
|
||||
-- Create diff buffer
|
||||
state.diff_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.diff_buf].buftype = "nofile"
|
||||
vim.bo[state.diff_buf].bufhidden = "wipe"
|
||||
vim.bo[state.diff_buf].swapfile = false
|
||||
|
||||
-- Create layout: list on left (30 cols), diff on right
|
||||
vim.cmd("tabnew")
|
||||
state.diff_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(state.diff_win, state.diff_buf)
|
||||
|
||||
vim.cmd("topleft vsplit")
|
||||
state.list_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(state.list_win, state.list_buf)
|
||||
vim.api.nvim_win_set_width(state.list_win, 35)
|
||||
|
||||
-- Window options
|
||||
for _, win in ipairs({ state.list_win, state.diff_win }) do
|
||||
vim.wo[win].number = false
|
||||
vim.wo[win].relativenumber = false
|
||||
vim.wo[win].signcolumn = "no"
|
||||
vim.wo[win].wrap = false
|
||||
vim.wo[win].cursorline = true
|
||||
end
|
||||
|
||||
-- Set up keymaps for list buffer
|
||||
local list_opts = { buffer = state.list_buf, noremap = true, silent = true }
|
||||
vim.keymap.set("n", "j", M.next, list_opts)
|
||||
vim.keymap.set("n", "k", M.prev, list_opts)
|
||||
vim.keymap.set("n", "<Down>", M.next, list_opts)
|
||||
vim.keymap.set("n", "<Up>", M.prev, list_opts)
|
||||
vim.keymap.set("n", "<CR>", function() vim.api.nvim_set_current_win(state.diff_win) end, list_opts)
|
||||
vim.keymap.set("n", "a", M.approve_current, list_opts)
|
||||
vim.keymap.set("n", "r", M.reject_current, list_opts)
|
||||
vim.keymap.set("n", "A", M.approve_all, list_opts)
|
||||
vim.keymap.set("n", "q", M.close, list_opts)
|
||||
vim.keymap.set("n", "<Esc>", M.close, list_opts)
|
||||
|
||||
-- Set up keymaps for diff buffer
|
||||
local diff_opts = { buffer = state.diff_buf, noremap = true, silent = true }
|
||||
vim.keymap.set("n", "j", M.next, diff_opts)
|
||||
vim.keymap.set("n", "k", M.prev, diff_opts)
|
||||
vim.keymap.set("n", "<Tab>", function() vim.api.nvim_set_current_win(state.list_win) end, diff_opts)
|
||||
vim.keymap.set("n", "a", M.approve_current, diff_opts)
|
||||
vim.keymap.set("n", "r", M.reject_current, diff_opts)
|
||||
vim.keymap.set("n", "A", M.approve_all, diff_opts)
|
||||
vim.keymap.set("n", "q", M.close, diff_opts)
|
||||
vim.keymap.set("n", "<Esc>", M.close, diff_opts)
|
||||
|
||||
state.is_open = true
|
||||
state.current_index = 1
|
||||
|
||||
-- Initial render
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
|
||||
-- Focus list window
|
||||
vim.api.nvim_set_current_win(state.list_win)
|
||||
end
|
||||
|
||||
--- Close the diff review UI
|
||||
function M.close()
|
||||
if not state.is_open then
|
||||
return
|
||||
end
|
||||
|
||||
-- Close the tab (which closes both windows)
|
||||
pcall(vim.cmd, "tabclose")
|
||||
|
||||
state.list_buf = nil
|
||||
state.list_win = nil
|
||||
state.diff_buf = nil
|
||||
state.diff_win = nil
|
||||
state.is_open = false
|
||||
end
|
||||
|
||||
--- Check if review UI is open
|
||||
---@return boolean
|
||||
function M.is_open()
|
||||
return state.is_open
|
||||
end
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,40 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local utils = require("codetyper.support.utils")
|
||||
local prompts = require("codetyper.prompts.agents.diff")
|
||||
local update_file_list = require("codetyper.adapters.nvim.ui.diff_review.update_file_list")
|
||||
local update_diff_view = require("codetyper.adapters.nvim.ui.diff_review.update_diff_view")
|
||||
|
||||
--- Apply all approved changes to disk
|
||||
---@return number applied_count Number of successfully applied changes
|
||||
local function apply_approved()
|
||||
local applied_count = 0
|
||||
|
||||
for _, entry in ipairs(state.entries) do
|
||||
if entry.approved and not entry.applied then
|
||||
if entry.operation == "create" or entry.operation == "edit" then
|
||||
local write_success = utils.write_file(entry.path, entry.modified)
|
||||
if write_success then
|
||||
entry.applied = true
|
||||
applied_count = applied_count + 1
|
||||
end
|
||||
elseif entry.operation == "delete" then
|
||||
local delete_success = os.remove(entry.path)
|
||||
if delete_success then
|
||||
entry.applied = true
|
||||
applied_count = applied_count + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
|
||||
if applied_count > 0 then
|
||||
utils.notify(string.format(prompts.review.messages.applied_count, applied_count))
|
||||
end
|
||||
|
||||
return applied_count
|
||||
end
|
||||
|
||||
return apply_approved
|
||||
16
lua/codetyper/adapters/nvim/ui/diff_review/approve_all.lua
Normal file
16
lua/codetyper/adapters/nvim/ui/diff_review/approve_all.lua
Normal file
@@ -0,0 +1,16 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local update_file_list = require("codetyper.adapters.nvim.ui.diff_review.update_file_list")
|
||||
local update_diff_view = require("codetyper.adapters.nvim.ui.diff_review.update_diff_view")
|
||||
|
||||
--- Approve all unapplied diff entries
|
||||
local function approve_all()
|
||||
for _, entry in ipairs(state.entries) do
|
||||
if not entry.applied then
|
||||
entry.approved = true
|
||||
end
|
||||
end
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
|
||||
return approve_all
|
||||
@@ -0,0 +1,15 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local update_file_list = require("codetyper.adapters.nvim.ui.diff_review.update_file_list")
|
||||
local update_diff_view = require("codetyper.adapters.nvim.ui.diff_review.update_diff_view")
|
||||
|
||||
--- Approve the currently selected diff entry
|
||||
local function approve_current()
|
||||
local entry = state.entries[state.current_index]
|
||||
if entry and not entry.applied then
|
||||
entry.approved = true
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
return approve_current
|
||||
18
lua/codetyper/adapters/nvim/ui/diff_review/close.lua
Normal file
18
lua/codetyper/adapters/nvim/ui/diff_review/close.lua
Normal file
@@ -0,0 +1,18 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Close the diff review UI
|
||||
local function close()
|
||||
if not state.is_open then
|
||||
return
|
||||
end
|
||||
|
||||
pcall(vim.cmd, "tabclose")
|
||||
|
||||
state.list_buf = nil
|
||||
state.list_win = nil
|
||||
state.diff_buf = nil
|
||||
state.diff_win = nil
|
||||
state.is_open = false
|
||||
end
|
||||
|
||||
return close
|
||||
@@ -0,0 +1,67 @@
|
||||
--- Generate unified diff between two strings
|
||||
---@param original string|nil
|
||||
---@param modified string
|
||||
---@param filepath string
|
||||
---@return string[]
|
||||
local function generate_diff_lines(original, modified, filepath)
|
||||
local lines = {}
|
||||
local filename = vim.fn.fnamemodify(filepath, ":t")
|
||||
|
||||
if not original then
|
||||
-- New file
|
||||
table.insert(lines, "--- /dev/null")
|
||||
table.insert(lines, "+++ b/" .. filename)
|
||||
table.insert(lines, "@@ -0,0 +1," .. #vim.split(modified, "\n") .. " @@")
|
||||
for _, line in ipairs(vim.split(modified, "\n")) do
|
||||
table.insert(lines, "+" .. line)
|
||||
end
|
||||
else
|
||||
-- Modified file - use vim's diff
|
||||
table.insert(lines, "--- a/" .. filename)
|
||||
table.insert(lines, "+++ b/" .. filename)
|
||||
|
||||
local orig_lines = vim.split(original, "\n")
|
||||
local mod_lines = vim.split(modified, "\n")
|
||||
|
||||
-- Simple diff: show removed and added lines
|
||||
local max_lines = math.max(#orig_lines, #mod_lines)
|
||||
local context_start = 1
|
||||
local in_change = false
|
||||
|
||||
for i = 1, max_lines do
|
||||
local orig = orig_lines[i] or ""
|
||||
local mod = mod_lines[i] or ""
|
||||
|
||||
if orig ~= mod then
|
||||
if not in_change then
|
||||
table.insert(
|
||||
lines,
|
||||
string.format(
|
||||
"@@ -%d,%d +%d,%d @@",
|
||||
math.max(1, i - 2),
|
||||
math.min(5, #orig_lines - i + 3),
|
||||
math.max(1, i - 2),
|
||||
math.min(5, #mod_lines - i + 3)
|
||||
)
|
||||
)
|
||||
in_change = true
|
||||
end
|
||||
if orig ~= "" then
|
||||
table.insert(lines, "-" .. orig)
|
||||
end
|
||||
if mod ~= "" then
|
||||
table.insert(lines, "+" .. mod)
|
||||
end
|
||||
else
|
||||
if in_change then
|
||||
table.insert(lines, " " .. orig)
|
||||
in_change = false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return lines
|
||||
end
|
||||
|
||||
return generate_diff_lines
|
||||
9
lua/codetyper/adapters/nvim/ui/diff_review/is_open.lua
Normal file
9
lua/codetyper/adapters/nvim/ui/diff_review/is_open.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Check if the diff review UI is open
|
||||
---@return boolean
|
||||
local function is_open()
|
||||
return state.is_open
|
||||
end
|
||||
|
||||
return is_open
|
||||
14
lua/codetyper/adapters/nvim/ui/diff_review/navigate_next.lua
Normal file
14
lua/codetyper/adapters/nvim/ui/diff_review/navigate_next.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local update_file_list = require("codetyper.adapters.nvim.ui.diff_review.update_file_list")
|
||||
local update_diff_view = require("codetyper.adapters.nvim.ui.diff_review.update_diff_view")
|
||||
|
||||
--- Navigate to next diff entry
|
||||
local function navigate_next()
|
||||
if state.current_index < #state.entries then
|
||||
state.current_index = state.current_index + 1
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
return navigate_next
|
||||
14
lua/codetyper/adapters/nvim/ui/diff_review/navigate_prev.lua
Normal file
14
lua/codetyper/adapters/nvim/ui/diff_review/navigate_prev.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local update_file_list = require("codetyper.adapters.nvim.ui.diff_review.update_file_list")
|
||||
local update_diff_view = require("codetyper.adapters.nvim.ui.diff_review.update_diff_view")
|
||||
|
||||
--- Navigate to previous diff entry
|
||||
local function navigate_prev()
|
||||
if state.current_index > 1 then
|
||||
state.current_index = state.current_index - 1
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
return navigate_prev
|
||||
86
lua/codetyper/adapters/nvim/ui/diff_review/open.lua
Normal file
86
lua/codetyper/adapters/nvim/ui/diff_review/open.lua
Normal file
@@ -0,0 +1,86 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local utils = require("codetyper.support.utils")
|
||||
local prompts = require("codetyper.prompts.agents.diff")
|
||||
local update_file_list = require("codetyper.adapters.nvim.ui.diff_review.update_file_list")
|
||||
local update_diff_view = require("codetyper.adapters.nvim.ui.diff_review.update_diff_view")
|
||||
local navigate_next = require("codetyper.adapters.nvim.ui.diff_review.navigate_next")
|
||||
local navigate_prev = require("codetyper.adapters.nvim.ui.diff_review.navigate_prev")
|
||||
local approve_current = require("codetyper.adapters.nvim.ui.diff_review.approve_current")
|
||||
local reject_current = require("codetyper.adapters.nvim.ui.diff_review.reject_current")
|
||||
local approve_all = require("codetyper.adapters.nvim.ui.diff_review.approve_all")
|
||||
local close = require("codetyper.adapters.nvim.ui.diff_review.close")
|
||||
|
||||
--- Open the diff review UI
|
||||
local function open()
|
||||
if state.is_open then
|
||||
return
|
||||
end
|
||||
|
||||
if #state.entries == 0 then
|
||||
utils.notify(prompts.review.messages.no_changes_short, vim.log.levels.INFO)
|
||||
return
|
||||
end
|
||||
|
||||
state.list_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.list_buf].buftype = "nofile"
|
||||
vim.bo[state.list_buf].bufhidden = "wipe"
|
||||
vim.bo[state.list_buf].swapfile = false
|
||||
|
||||
state.diff_buf = vim.api.nvim_create_buf(false, true)
|
||||
vim.bo[state.diff_buf].buftype = "nofile"
|
||||
vim.bo[state.diff_buf].bufhidden = "wipe"
|
||||
vim.bo[state.diff_buf].swapfile = false
|
||||
|
||||
vim.cmd("tabnew")
|
||||
state.diff_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(state.diff_win, state.diff_buf)
|
||||
|
||||
vim.cmd("topleft vsplit")
|
||||
state.list_win = vim.api.nvim_get_current_win()
|
||||
vim.api.nvim_win_set_buf(state.list_win, state.list_buf)
|
||||
vim.api.nvim_win_set_width(state.list_win, 35)
|
||||
|
||||
for _, win in ipairs({ state.list_win, state.diff_win }) do
|
||||
vim.wo[win].number = false
|
||||
vim.wo[win].relativenumber = false
|
||||
vim.wo[win].signcolumn = "no"
|
||||
vim.wo[win].wrap = false
|
||||
vim.wo[win].cursorline = true
|
||||
end
|
||||
|
||||
local list_keymap_opts = { buffer = state.list_buf, noremap = true, silent = true }
|
||||
vim.keymap.set("n", "j", navigate_next, list_keymap_opts)
|
||||
vim.keymap.set("n", "k", navigate_prev, list_keymap_opts)
|
||||
vim.keymap.set("n", "<Down>", navigate_next, list_keymap_opts)
|
||||
vim.keymap.set("n", "<Up>", navigate_prev, list_keymap_opts)
|
||||
vim.keymap.set("n", "<CR>", function()
|
||||
vim.api.nvim_set_current_win(state.diff_win)
|
||||
end, list_keymap_opts)
|
||||
vim.keymap.set("n", "a", approve_current, list_keymap_opts)
|
||||
vim.keymap.set("n", "r", reject_current, list_keymap_opts)
|
||||
vim.keymap.set("n", "A", approve_all, list_keymap_opts)
|
||||
vim.keymap.set("n", "q", close, list_keymap_opts)
|
||||
vim.keymap.set("n", "<Esc>", close, list_keymap_opts)
|
||||
|
||||
local diff_keymap_opts = { buffer = state.diff_buf, noremap = true, silent = true }
|
||||
vim.keymap.set("n", "j", navigate_next, diff_keymap_opts)
|
||||
vim.keymap.set("n", "k", navigate_prev, diff_keymap_opts)
|
||||
vim.keymap.set("n", "<Tab>", function()
|
||||
vim.api.nvim_set_current_win(state.list_win)
|
||||
end, diff_keymap_opts)
|
||||
vim.keymap.set("n", "a", approve_current, diff_keymap_opts)
|
||||
vim.keymap.set("n", "r", reject_current, diff_keymap_opts)
|
||||
vim.keymap.set("n", "A", approve_all, diff_keymap_opts)
|
||||
vim.keymap.set("n", "q", close, diff_keymap_opts)
|
||||
vim.keymap.set("n", "<Esc>", close, diff_keymap_opts)
|
||||
|
||||
state.is_open = true
|
||||
state.current_index = 1
|
||||
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
|
||||
vim.api.nvim_set_current_win(state.list_win)
|
||||
end
|
||||
|
||||
return open
|
||||
@@ -0,0 +1,15 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local update_file_list = require("codetyper.adapters.nvim.ui.diff_review.update_file_list")
|
||||
local update_diff_view = require("codetyper.adapters.nvim.ui.diff_review.update_diff_view")
|
||||
|
||||
--- Reject the currently selected diff entry
|
||||
local function reject_current()
|
||||
local entry = state.entries[state.current_index]
|
||||
if entry and not entry.applied then
|
||||
entry.approved = false
|
||||
update_file_list()
|
||||
update_diff_view()
|
||||
end
|
||||
end
|
||||
|
||||
return reject_current
|
||||
@@ -0,0 +1,48 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local prompts = require("codetyper.prompts.agents.diff")
|
||||
local generate_diff_lines = require("codetyper.adapters.nvim.ui.diff_review.generate_diff_lines")
|
||||
|
||||
--- Update the diff view for current entry
|
||||
local function update_diff_view()
|
||||
if not state.diff_buf or not vim.api.nvim_buf_is_valid(state.diff_buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local entry = state.entries[state.current_index]
|
||||
local ui_prompts = prompts.review
|
||||
if not entry then
|
||||
vim.bo[state.diff_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.diff_buf, 0, -1, false, { ui_prompts.messages.no_changes_short })
|
||||
vim.bo[state.diff_buf].modifiable = false
|
||||
return
|
||||
end
|
||||
|
||||
local lines = {}
|
||||
|
||||
local status_icon = entry.applied and " " or (entry.approved and " " or " ")
|
||||
local op_icon = entry.operation == "create" and "+" or (entry.operation == "delete" and "-" or "~")
|
||||
local current_status = entry.applied and ui_prompts.status.applied
|
||||
or (entry.approved and ui_prompts.status.approved or ui_prompts.status.pending)
|
||||
|
||||
table.insert(
|
||||
lines,
|
||||
string.format(ui_prompts.diff_header.top, status_icon, op_icon, vim.fn.fnamemodify(entry.path, ":t"))
|
||||
)
|
||||
table.insert(lines, string.format(ui_prompts.diff_header.path, entry.path))
|
||||
table.insert(lines, string.format(ui_prompts.diff_header.op, entry.operation))
|
||||
table.insert(lines, string.format(ui_prompts.diff_header.status, current_status))
|
||||
table.insert(lines, ui_prompts.diff_header.bottom)
|
||||
table.insert(lines, "")
|
||||
|
||||
local diff_lines = generate_diff_lines(entry.original, entry.modified, entry.path)
|
||||
for _, line in ipairs(diff_lines) do
|
||||
table.insert(lines, line)
|
||||
end
|
||||
|
||||
vim.bo[state.diff_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.diff_buf, 0, -1, false, lines)
|
||||
vim.bo[state.diff_buf].modifiable = false
|
||||
vim.bo[state.diff_buf].filetype = "diff"
|
||||
end
|
||||
|
||||
return update_diff_view
|
||||
@@ -0,0 +1,44 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local prompts = require("codetyper.prompts.agents.diff")
|
||||
|
||||
--- Update the file list sidebar
|
||||
local function update_file_list()
|
||||
if not state.list_buf or not vim.api.nvim_buf_is_valid(state.list_buf) then
|
||||
return
|
||||
end
|
||||
|
||||
local ui_prompts = prompts.review
|
||||
local lines = {}
|
||||
table.insert(lines, string.format(ui_prompts.list_menu.top, #state.entries))
|
||||
for _, item in ipairs(ui_prompts.list_menu.items) do
|
||||
table.insert(lines, item)
|
||||
end
|
||||
table.insert(lines, ui_prompts.list_menu.bottom)
|
||||
table.insert(lines, "")
|
||||
|
||||
for entry_index, entry in ipairs(state.entries) do
|
||||
local prefix = (entry_index == state.current_index) and "▶ " or " "
|
||||
local status = entry.applied and "" or (entry.approved and "" or "○")
|
||||
local operation_icon = entry.operation == "create" and "[+]" or (entry.operation == "delete" and "[-]" or "[~]")
|
||||
local filename = vim.fn.fnamemodify(entry.path, ":t")
|
||||
|
||||
table.insert(lines, string.format("%s%s %s %s", prefix, status, operation_icon, filename))
|
||||
end
|
||||
|
||||
if #state.entries == 0 then
|
||||
table.insert(lines, ui_prompts.messages.no_changes)
|
||||
end
|
||||
|
||||
vim.bo[state.list_buf].modifiable = true
|
||||
vim.api.nvim_buf_set_lines(state.list_buf, 0, -1, false, lines)
|
||||
vim.bo[state.list_buf].modifiable = false
|
||||
|
||||
if state.list_win and vim.api.nvim_win_is_valid(state.list_win) then
|
||||
local target_line = 9 + state.current_index - 1
|
||||
if target_line <= vim.api.nvim_buf_line_count(state.list_buf) then
|
||||
vim.api.nvim_win_set_cursor(state.list_win, { target_line, 0 })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return update_file_list
|
||||
@@ -1,380 +0,0 @@
|
||||
---@mod codetyper.agent.logs Real-time logging for agent operations
|
||||
---
|
||||
--- Captures and displays the agent's thinking process, token usage, and LLM info.
|
||||
|
||||
local M = {}
|
||||
|
||||
local params = require("codetyper.params.agents.logs")
|
||||
|
||||
|
||||
---@class LogEntry
|
||||
---@field timestamp string ISO timestamp
|
||||
---@field level string "info" | "debug" | "request" | "response" | "tool" | "error"
|
||||
---@field message string Log message
|
||||
---@field data? table Optional structured data
|
||||
|
||||
---@class LogState
|
||||
---@field entries LogEntry[] All log entries
|
||||
---@field listeners table[] Functions to call when new entries are added
|
||||
---@field total_prompt_tokens number Running total of prompt tokens
|
||||
---@field total_response_tokens number Running total of response tokens
|
||||
|
||||
local state = {
|
||||
entries = {},
|
||||
listeners = {},
|
||||
total_prompt_tokens = 0,
|
||||
total_response_tokens = 0,
|
||||
current_provider = nil,
|
||||
current_model = nil,
|
||||
}
|
||||
|
||||
--- Get current timestamp
|
||||
---@return string
|
||||
local function get_timestamp()
|
||||
return os.date("%H:%M:%S")
|
||||
end
|
||||
|
||||
--- Add a log entry
|
||||
---@param level string Log level
|
||||
---@param message string Log message
|
||||
---@param data? table Optional data
|
||||
function M.log(level, message, data)
|
||||
local entry = {
|
||||
timestamp = get_timestamp(),
|
||||
level = level,
|
||||
message = message,
|
||||
data = data,
|
||||
}
|
||||
|
||||
table.insert(state.entries, entry)
|
||||
|
||||
-- Notify all listeners
|
||||
for _, listener in ipairs(state.listeners) do
|
||||
pcall(listener, entry)
|
||||
end
|
||||
end
|
||||
|
||||
--- Log info message
|
||||
---@param message string
|
||||
---@param data? table
|
||||
function M.info(message, data)
|
||||
M.log("info", message, data)
|
||||
end
|
||||
|
||||
--- Log debug message
|
||||
---@param message string
|
||||
---@param data? table
|
||||
function M.debug(message, data)
|
||||
M.log("debug", message, data)
|
||||
end
|
||||
|
||||
--- Log API request
|
||||
---@param provider string LLM provider
|
||||
---@param model string Model name
|
||||
---@param prompt_tokens? number Estimated prompt tokens
|
||||
function M.request(provider, model, prompt_tokens)
|
||||
state.current_provider = provider
|
||||
state.current_model = model
|
||||
|
||||
local msg = string.format("[%s] %s", provider:upper(), model)
|
||||
if prompt_tokens then
|
||||
msg = msg .. string.format(" | Prompt: ~%d tokens", prompt_tokens)
|
||||
end
|
||||
|
||||
M.log("request", msg, {
|
||||
provider = provider,
|
||||
model = model,
|
||||
prompt_tokens = prompt_tokens,
|
||||
})
|
||||
end
|
||||
|
||||
--- Log API response with token usage
|
||||
---@param prompt_tokens number Tokens used in prompt
|
||||
---@param response_tokens number Tokens in response
|
||||
---@param stop_reason? string Why the response stopped
|
||||
function M.response(prompt_tokens, response_tokens, stop_reason)
|
||||
state.total_prompt_tokens = state.total_prompt_tokens + prompt_tokens
|
||||
state.total_response_tokens = state.total_response_tokens + response_tokens
|
||||
|
||||
local msg = string.format(
|
||||
"Tokens: %d in / %d out | Total: %d in / %d out",
|
||||
prompt_tokens,
|
||||
response_tokens,
|
||||
state.total_prompt_tokens,
|
||||
state.total_response_tokens
|
||||
)
|
||||
|
||||
if stop_reason then
|
||||
msg = msg .. " | Stop: " .. stop_reason
|
||||
end
|
||||
|
||||
M.log("response", msg, {
|
||||
prompt_tokens = prompt_tokens,
|
||||
response_tokens = response_tokens,
|
||||
total_prompt = state.total_prompt_tokens,
|
||||
total_response = state.total_response_tokens,
|
||||
stop_reason = stop_reason,
|
||||
})
|
||||
end
|
||||
|
||||
--- Log tool execution
|
||||
---@param tool_name string Name of the tool
|
||||
---@param status string "start" | "success" | "error" | "approval"
|
||||
---@param details? string Additional details
|
||||
function M.tool(tool_name, status, details)
|
||||
local icons = params.icons
|
||||
|
||||
local msg = string.format("[%s] %s", icons[status] or status, tool_name)
|
||||
if details then
|
||||
msg = msg .. ": " .. details
|
||||
end
|
||||
|
||||
M.log("tool", msg, {
|
||||
tool = tool_name,
|
||||
status = status,
|
||||
details = details,
|
||||
})
|
||||
end
|
||||
|
||||
--- Log error
|
||||
---@param message string
|
||||
---@param data? table
|
||||
function M.error(message, data)
|
||||
M.log("error", "ERROR: " .. message, data)
|
||||
end
|
||||
|
||||
--- Log warning
|
||||
---@param message string
|
||||
---@param data? table
|
||||
function M.warning(message, data)
|
||||
M.log("warning", "WARN: " .. message, data)
|
||||
end
|
||||
|
||||
--- Add log entry (compatibility function for scheduler)
|
||||
--- Accepts {type = "info", message = "..."} format
|
||||
---@param entry table Log entry with type and message
|
||||
function M.add(entry)
|
||||
if entry.type == "clear" then
|
||||
M.clear()
|
||||
return
|
||||
end
|
||||
M.log(entry.type or "info", entry.message or "", entry.data)
|
||||
end
|
||||
|
||||
--- Log thinking/reasoning step (Claude Code style)
|
||||
---@param step string Description of what's happening
|
||||
function M.thinking(step)
|
||||
M.log("thinking", step)
|
||||
end
|
||||
|
||||
--- Log a reasoning/explanation message (shown prominently)
|
||||
---@param message string The reasoning message
|
||||
function M.reason(message)
|
||||
M.log("reason", message)
|
||||
end
|
||||
|
||||
--- Log file read operation
|
||||
---@param filepath string Path of file being read
|
||||
---@param lines? number Number of lines read
|
||||
function M.read(filepath, lines)
|
||||
local msg = string.format("Read(%s)", vim.fn.fnamemodify(filepath, ":~:."))
|
||||
if lines then
|
||||
msg = msg .. string.format("\n ⎿ Read %d lines", lines)
|
||||
end
|
||||
M.log("action", msg)
|
||||
end
|
||||
|
||||
--- Log explore/search operation
|
||||
---@param description string What we're exploring
|
||||
function M.explore(description)
|
||||
M.log("action", string.format("Explore(%s)", description))
|
||||
end
|
||||
|
||||
--- Log explore done
|
||||
---@param tool_uses number Number of tool uses
|
||||
---@param tokens number Tokens used
|
||||
---@param duration number Duration in seconds
|
||||
function M.explore_done(tool_uses, tokens, duration)
|
||||
M.log("result", string.format(" ⎿ Done (%d tool uses · %.1fk tokens · %.1fs)", tool_uses, tokens / 1000, duration))
|
||||
end
|
||||
|
||||
--- Log update/edit operation
|
||||
---@param filepath string Path of file being edited
|
||||
---@param added? number Lines added
|
||||
---@param removed? number Lines removed
|
||||
function M.update(filepath, added, removed)
|
||||
local msg = string.format("Update(%s)", vim.fn.fnamemodify(filepath, ":~:."))
|
||||
if added or removed then
|
||||
local parts = {}
|
||||
if added and added > 0 then
|
||||
table.insert(parts, string.format("Added %d lines", added))
|
||||
end
|
||||
if removed and removed > 0 then
|
||||
table.insert(parts, string.format("Removed %d lines", removed))
|
||||
end
|
||||
if #parts > 0 then
|
||||
msg = msg .. "\n ⎿ " .. table.concat(parts, ", ")
|
||||
end
|
||||
end
|
||||
M.log("action", msg)
|
||||
end
|
||||
|
||||
--- Log a task/step that's in progress
|
||||
---@param task string Task name
|
||||
---@param status string Status message (optional)
|
||||
function M.task(task, status)
|
||||
local msg = task
|
||||
if status then
|
||||
msg = msg .. " " .. status
|
||||
end
|
||||
M.log("task", msg)
|
||||
end
|
||||
|
||||
--- Log task completion
|
||||
---@param next_task? string Next task (optional)
|
||||
function M.task_done(next_task)
|
||||
local msg = " ⎿ Done"
|
||||
if next_task then
|
||||
msg = msg .. "\n✶ " .. next_task
|
||||
end
|
||||
M.log("result", msg)
|
||||
end
|
||||
|
||||
--- Register a listener for new log entries
|
||||
---@param callback fun(entry: LogEntry)
|
||||
---@return number Listener ID for removal
|
||||
function M.add_listener(callback)
|
||||
table.insert(state.listeners, callback)
|
||||
return #state.listeners
|
||||
end
|
||||
|
||||
--- Remove a listener
|
||||
---@param id number Listener ID
|
||||
function M.remove_listener(id)
|
||||
if id > 0 and id <= #state.listeners then
|
||||
table.remove(state.listeners, id)
|
||||
end
|
||||
end
|
||||
|
||||
--- Get all log entries
|
||||
---@return LogEntry[]
|
||||
function M.get_entries()
|
||||
return state.entries
|
||||
end
|
||||
|
||||
--- Get token totals
|
||||
---@return number, number prompt_tokens, response_tokens
|
||||
function M.get_token_totals()
|
||||
return state.total_prompt_tokens, state.total_response_tokens
|
||||
end
|
||||
|
||||
--- Get current provider info
|
||||
---@return string?, string? provider, model
|
||||
function M.get_provider_info()
|
||||
return state.current_provider, state.current_model
|
||||
end
|
||||
|
||||
--- Clear all logs and reset counters
|
||||
function M.clear()
|
||||
state.entries = {}
|
||||
state.total_prompt_tokens = 0
|
||||
state.total_response_tokens = 0
|
||||
state.current_provider = nil
|
||||
state.current_model = nil
|
||||
|
||||
-- Notify listeners of clear
|
||||
for _, listener in ipairs(state.listeners) do
|
||||
pcall(listener, { level = "clear" })
|
||||
end
|
||||
end
|
||||
|
||||
--- Format entry for display
|
||||
---@param entry LogEntry
|
||||
---@return string
|
||||
function M.format_entry(entry)
|
||||
-- Claude Code style formatting for thinking/action entries
|
||||
local thinking_types = params.thinking_types
|
||||
local is_thinking = vim.tbl_contains(thinking_types, entry.level)
|
||||
|
||||
if is_thinking then
|
||||
local prefix = params.thinking_prefixes[entry.level] or "⏺"
|
||||
|
||||
if prefix ~= "" then
|
||||
return prefix .. " " .. entry.message
|
||||
else
|
||||
return entry.message
|
||||
end
|
||||
end
|
||||
|
||||
-- Traditional log format for other types
|
||||
local level_prefix = params.level_icons[entry.level] or "?"
|
||||
|
||||
local base = string.format("[%s] %s %s", entry.timestamp, level_prefix, entry.message)
|
||||
|
||||
-- If this is a response entry with raw_response, append the full response
|
||||
if entry.data and entry.data.raw_response then
|
||||
local response = entry.data.raw_response
|
||||
-- Add separator and the full response
|
||||
base = base .. "\n" .. string.rep("-", 40) .. "\n" .. response .. "\n" .. string.rep("-", 40)
|
||||
end
|
||||
|
||||
return base
|
||||
end
|
||||
|
||||
--- Format entry for display in chat (compact Claude Code style)
|
||||
---@param entry LogEntry
|
||||
---@return string|nil Formatted string or nil to skip
|
||||
function M.format_for_chat(entry)
|
||||
-- Skip certain log types in chat view
|
||||
local skip_types = { "debug", "queue", "patch" }
|
||||
if vim.tbl_contains(skip_types, entry.level) then
|
||||
return nil
|
||||
end
|
||||
|
||||
-- Claude Code style formatting
|
||||
local thinking_types = params.thinking_types
|
||||
if vim.tbl_contains(thinking_types, entry.level) then
|
||||
local prefix = params.thinking_prefixes[entry.level] or "⏺"
|
||||
|
||||
if prefix ~= "" then
|
||||
return prefix .. " " .. entry.message
|
||||
else
|
||||
return entry.message
|
||||
end
|
||||
end
|
||||
|
||||
-- Tool logs
|
||||
if entry.level == "tool" then
|
||||
return "⏺ " .. entry.message:gsub("^%[.-%] ", "")
|
||||
end
|
||||
|
||||
-- Info/success
|
||||
if entry.level == "info" or entry.level == "success" then
|
||||
return "⏺ " .. entry.message
|
||||
end
|
||||
|
||||
-- Errors
|
||||
if entry.level == "error" then
|
||||
return "⚠ " .. entry.message
|
||||
end
|
||||
|
||||
-- Request/response (compact)
|
||||
if entry.level == "request" then
|
||||
return "⏺ " .. entry.message
|
||||
end
|
||||
if entry.level == "response" then
|
||||
return " ⎿ " .. entry.message
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
--- Estimate token count for a string (rough approximation)
|
||||
---@param text string
|
||||
---@return number
|
||||
function M.estimate_tokens(text)
|
||||
-- Rough estimate: ~4 characters per token for English text
|
||||
return math.ceil(#text / 4)
|
||||
end
|
||||
|
||||
return M
|
||||
15
lua/codetyper/adapters/nvim/ui/logs/add.lua
Normal file
15
lua/codetyper/adapters/nvim/ui/logs/add.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
local clear = require("codetyper.adapters.nvim.ui.logs.clear")
|
||||
|
||||
--- Add log entry (compatibility function for scheduler)
|
||||
--- Accepts {type = "info", message = "..."} format
|
||||
---@param entry table Log entry with type and message
|
||||
local function add(entry)
|
||||
if entry.type == "clear" then
|
||||
clear()
|
||||
return
|
||||
end
|
||||
log(entry.type or "info", entry.message or "", entry.data)
|
||||
end
|
||||
|
||||
return add
|
||||
11
lua/codetyper/adapters/nvim/ui/logs/add_listener.lua
Normal file
11
lua/codetyper/adapters/nvim/ui/logs/add_listener.lua
Normal file
@@ -0,0 +1,11 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Register a listener for new log entries
|
||||
---@param callback fun(entry: LogEntry)
|
||||
---@return number listener_id Listener ID for removal
|
||||
local function add_listener(callback)
|
||||
table.insert(state.listeners, callback)
|
||||
return #state.listeners
|
||||
end
|
||||
|
||||
return add_listener
|
||||
16
lua/codetyper/adapters/nvim/ui/logs/clear.lua
Normal file
16
lua/codetyper/adapters/nvim/ui/logs/clear.lua
Normal file
@@ -0,0 +1,16 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Clear all logs and reset counters
|
||||
local function clear()
|
||||
state.entries = {}
|
||||
state.total_prompt_tokens = 0
|
||||
state.total_response_tokens = 0
|
||||
state.current_provider = nil
|
||||
state.current_model = nil
|
||||
|
||||
for _, listener in ipairs(state.listeners) do
|
||||
pcall(listener, { level = "clear" })
|
||||
end
|
||||
end
|
||||
|
||||
return clear
|
||||
10
lua/codetyper/adapters/nvim/ui/logs/debug.lua
Normal file
10
lua/codetyper/adapters/nvim/ui/logs/debug.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log debug message
|
||||
---@param message string
|
||||
---@param data? table
|
||||
local function debug(message, data)
|
||||
log("debug", message, data)
|
||||
end
|
||||
|
||||
return debug
|
||||
10
lua/codetyper/adapters/nvim/ui/logs/error.lua
Normal file
10
lua/codetyper/adapters/nvim/ui/logs/error.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log error message
|
||||
---@param message string
|
||||
---@param data? table
|
||||
local function log_error(message, data)
|
||||
log("error", "ERROR: " .. message, data)
|
||||
end
|
||||
|
||||
return log_error
|
||||
9
lua/codetyper/adapters/nvim/ui/logs/explore.lua
Normal file
9
lua/codetyper/adapters/nvim/ui/logs/explore.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log explore/search operation
|
||||
---@param description string What we're exploring
|
||||
local function explore(description)
|
||||
log("action", string.format("Explore(%s)", description))
|
||||
end
|
||||
|
||||
return explore
|
||||
14
lua/codetyper/adapters/nvim/ui/logs/explore_done.lua
Normal file
14
lua/codetyper/adapters/nvim/ui/logs/explore_done.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log explore done with stats
|
||||
---@param tool_uses number Number of tool uses
|
||||
---@param tokens number Tokens used
|
||||
---@param duration number Duration in seconds
|
||||
local function explore_done(tool_uses, tokens, duration)
|
||||
log(
|
||||
"result",
|
||||
string.format(" ⎿ Done (%d tool uses · %.1fk tokens · %.1fs)", tool_uses, tokens / 1000, duration)
|
||||
)
|
||||
end
|
||||
|
||||
return explore_done
|
||||
30
lua/codetyper/adapters/nvim/ui/logs/format_entry.lua
Normal file
30
lua/codetyper/adapters/nvim/ui/logs/format_entry.lua
Normal file
@@ -0,0 +1,30 @@
|
||||
local params = require("codetyper.params.agents.logs")
|
||||
|
||||
--- Format a log entry for display
|
||||
---@param entry LogEntry
|
||||
---@return string
|
||||
local function format_entry(entry)
|
||||
local thinking_types = params.thinking_types
|
||||
local is_thinking = vim.tbl_contains(thinking_types, entry.level)
|
||||
|
||||
if is_thinking then
|
||||
local prefix = params.thinking_prefixes[entry.level] or "⏺"
|
||||
if prefix ~= "" then
|
||||
return prefix .. " " .. entry.message
|
||||
else
|
||||
return entry.message
|
||||
end
|
||||
end
|
||||
|
||||
local level_prefix = params.level_icons[entry.level] or "?"
|
||||
local base = string.format("[%s] %s %s", entry.timestamp, level_prefix, entry.message)
|
||||
|
||||
if entry.data and entry.data.raw_response then
|
||||
local separator = string.rep("-", 40)
|
||||
base = base .. "\n" .. separator .. "\n" .. entry.data.raw_response .. "\n" .. separator
|
||||
end
|
||||
|
||||
return base
|
||||
end
|
||||
|
||||
return format_entry
|
||||
45
lua/codetyper/adapters/nvim/ui/logs/format_for_chat.lua
Normal file
45
lua/codetyper/adapters/nvim/ui/logs/format_for_chat.lua
Normal file
@@ -0,0 +1,45 @@
|
||||
local params = require("codetyper.params.agents.logs")
|
||||
|
||||
--- Format entry for display in chat (compact Claude Code style)
|
||||
---@param entry LogEntry
|
||||
---@return string|nil formatted Formatted string or nil to skip
|
||||
local function format_for_chat(entry)
|
||||
local skip_types = { "debug", "queue", "patch" }
|
||||
if vim.tbl_contains(skip_types, entry.level) then
|
||||
return nil
|
||||
end
|
||||
|
||||
local thinking_types = params.thinking_types
|
||||
if vim.tbl_contains(thinking_types, entry.level) then
|
||||
local prefix = params.thinking_prefixes[entry.level] or "⏺"
|
||||
if prefix ~= "" then
|
||||
return prefix .. " " .. entry.message
|
||||
else
|
||||
return entry.message
|
||||
end
|
||||
end
|
||||
|
||||
if entry.level == "tool" then
|
||||
return "⏺ " .. entry.message:gsub("^%[.-%] ", "")
|
||||
end
|
||||
|
||||
if entry.level == "info" or entry.level == "success" then
|
||||
return "⏺ " .. entry.message
|
||||
end
|
||||
|
||||
if entry.level == "error" then
|
||||
return "⚠ " .. entry.message
|
||||
end
|
||||
|
||||
if entry.level == "request" then
|
||||
return "⏺ " .. entry.message
|
||||
end
|
||||
|
||||
if entry.level == "response" then
|
||||
return " ⎿ " .. entry.message
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
return format_for_chat
|
||||
9
lua/codetyper/adapters/nvim/ui/logs/get_entries.lua
Normal file
9
lua/codetyper/adapters/nvim/ui/logs/get_entries.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Get all log entries
|
||||
---@return LogEntry[]
|
||||
local function get_entries()
|
||||
return state.entries
|
||||
end
|
||||
|
||||
return get_entries
|
||||
10
lua/codetyper/adapters/nvim/ui/logs/get_provider_info.lua
Normal file
10
lua/codetyper/adapters/nvim/ui/logs/get_provider_info.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Get current provider info
|
||||
---@return string|nil provider
|
||||
---@return string|nil model
|
||||
local function get_provider_info()
|
||||
return state.current_provider, state.current_model
|
||||
end
|
||||
|
||||
return get_provider_info
|
||||
10
lua/codetyper/adapters/nvim/ui/logs/get_token_totals.lua
Normal file
10
lua/codetyper/adapters/nvim/ui/logs/get_token_totals.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Get token totals
|
||||
---@return number prompt_tokens
|
||||
---@return number response_tokens
|
||||
local function get_token_totals()
|
||||
return state.total_prompt_tokens, state.total_response_tokens
|
||||
end
|
||||
|
||||
return get_token_totals
|
||||
10
lua/codetyper/adapters/nvim/ui/logs/info.lua
Normal file
10
lua/codetyper/adapters/nvim/ui/logs/info.lua
Normal file
@@ -0,0 +1,10 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log info message
|
||||
---@param message string
|
||||
---@param data? table
|
||||
local function info(message, data)
|
||||
log("info", message, data)
|
||||
end
|
||||
|
||||
return info
|
||||
23
lua/codetyper/adapters/nvim/ui/logs/log.lua
Normal file
23
lua/codetyper/adapters/nvim/ui/logs/log.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local get_timestamp = require("codetyper.utils.get_timestamp")
|
||||
|
||||
--- Add a log entry and notify all listeners
|
||||
---@param level string Log level
|
||||
---@param message string Log message
|
||||
---@param data? table Optional data
|
||||
local function log(level, message, data)
|
||||
local entry = {
|
||||
timestamp = get_timestamp(),
|
||||
level = level,
|
||||
message = message,
|
||||
data = data,
|
||||
}
|
||||
|
||||
table.insert(state.entries, entry)
|
||||
|
||||
for _, listener in ipairs(state.listeners) do
|
||||
pcall(listener, entry)
|
||||
end
|
||||
end
|
||||
|
||||
return log
|
||||
14
lua/codetyper/adapters/nvim/ui/logs/read.lua
Normal file
14
lua/codetyper/adapters/nvim/ui/logs/read.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log file read operation
|
||||
---@param filepath string Path of file being read
|
||||
---@param lines? number Number of lines read
|
||||
local function read(filepath, lines)
|
||||
local message = string.format("Read(%s)", vim.fn.fnamemodify(filepath, ":~:."))
|
||||
if lines then
|
||||
message = message .. string.format("\n ⎿ Read %d lines", lines)
|
||||
end
|
||||
log("action", message)
|
||||
end
|
||||
|
||||
return read
|
||||
9
lua/codetyper/adapters/nvim/ui/logs/reason.lua
Normal file
9
lua/codetyper/adapters/nvim/ui/logs/reason.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log a reasoning/explanation message (shown prominently)
|
||||
---@param message string The reasoning message
|
||||
local function reason(message)
|
||||
log("reason", message)
|
||||
end
|
||||
|
||||
return reason
|
||||
11
lua/codetyper/adapters/nvim/ui/logs/remove_listener.lua
Normal file
11
lua/codetyper/adapters/nvim/ui/logs/remove_listener.lua
Normal file
@@ -0,0 +1,11 @@
|
||||
local state = require("codetyper.state.state")
|
||||
|
||||
--- Remove a listener by ID
|
||||
---@param listener_id number Listener ID
|
||||
local function remove_listener(listener_id)
|
||||
if listener_id > 0 and listener_id <= #state.listeners then
|
||||
table.remove(state.listeners, listener_id)
|
||||
end
|
||||
end
|
||||
|
||||
return remove_listener
|
||||
24
lua/codetyper/adapters/nvim/ui/logs/request.lua
Normal file
24
lua/codetyper/adapters/nvim/ui/logs/request.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log API request
|
||||
---@param provider string LLM provider
|
||||
---@param model string Model name
|
||||
---@param prompt_tokens? number Estimated prompt tokens
|
||||
local function request(provider, model, prompt_tokens)
|
||||
state.current_provider = provider
|
||||
state.current_model = model
|
||||
|
||||
local message = string.format("[%s] %s", provider:upper(), model)
|
||||
if prompt_tokens then
|
||||
message = message .. string.format(" | Prompt: ~%d tokens", prompt_tokens)
|
||||
end
|
||||
|
||||
log("request", message, {
|
||||
provider = provider,
|
||||
model = model,
|
||||
prompt_tokens = prompt_tokens,
|
||||
})
|
||||
end
|
||||
|
||||
return request
|
||||
33
lua/codetyper/adapters/nvim/ui/logs/response.lua
Normal file
33
lua/codetyper/adapters/nvim/ui/logs/response.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
local state = require("codetyper.state.state")
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log API response with token usage
|
||||
---@param prompt_tokens number Tokens used in prompt
|
||||
---@param response_tokens number Tokens in response
|
||||
---@param stop_reason? string Why the response stopped
|
||||
local function response(prompt_tokens, response_tokens, stop_reason)
|
||||
state.total_prompt_tokens = state.total_prompt_tokens + prompt_tokens
|
||||
state.total_response_tokens = state.total_response_tokens + response_tokens
|
||||
|
||||
local message = string.format(
|
||||
"Tokens: %d in / %d out | Total: %d in / %d out",
|
||||
prompt_tokens,
|
||||
response_tokens,
|
||||
state.total_prompt_tokens,
|
||||
state.total_response_tokens
|
||||
)
|
||||
|
||||
if stop_reason then
|
||||
message = message .. " | Stop: " .. stop_reason
|
||||
end
|
||||
|
||||
log("response", message, {
|
||||
prompt_tokens = prompt_tokens,
|
||||
response_tokens = response_tokens,
|
||||
total_prompt = state.total_prompt_tokens,
|
||||
total_response = state.total_response_tokens,
|
||||
stop_reason = stop_reason,
|
||||
})
|
||||
end
|
||||
|
||||
return response
|
||||
14
lua/codetyper/adapters/nvim/ui/logs/task.lua
Normal file
14
lua/codetyper/adapters/nvim/ui/logs/task.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log a task/step that's in progress
|
||||
---@param task_name string Task name
|
||||
---@param status string|nil Status message
|
||||
local function task(task_name, status)
|
||||
local message = task_name
|
||||
if status then
|
||||
message = message .. " " .. status
|
||||
end
|
||||
log("task", message)
|
||||
end
|
||||
|
||||
return task
|
||||
13
lua/codetyper/adapters/nvim/ui/logs/task_done.lua
Normal file
13
lua/codetyper/adapters/nvim/ui/logs/task_done.lua
Normal file
@@ -0,0 +1,13 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log task completion
|
||||
---@param next_task? string Next task
|
||||
local function task_done(next_task)
|
||||
local message = " ⎿ Done"
|
||||
if next_task then
|
||||
message = message .. "\n✶ " .. next_task
|
||||
end
|
||||
log("result", message)
|
||||
end
|
||||
|
||||
return task_done
|
||||
9
lua/codetyper/adapters/nvim/ui/logs/thinking.lua
Normal file
9
lua/codetyper/adapters/nvim/ui/logs/thinking.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log thinking/reasoning step
|
||||
---@param step string Description of what's happening
|
||||
local function thinking(step)
|
||||
log("thinking", step)
|
||||
end
|
||||
|
||||
return thinking
|
||||
23
lua/codetyper/adapters/nvim/ui/logs/tool.lua
Normal file
23
lua/codetyper/adapters/nvim/ui/logs/tool.lua
Normal file
@@ -0,0 +1,23 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
local params = require("codetyper.params.agents.logs")
|
||||
|
||||
--- Log tool execution
|
||||
---@param tool_name string Name of the tool
|
||||
---@param status string "start" | "success" | "error" | "approval"
|
||||
---@param details? string Additional details
|
||||
local function tool(tool_name, status, details)
|
||||
local icons = params.icons
|
||||
|
||||
local message = string.format("[%s] %s", icons[status] or status, tool_name)
|
||||
if details then
|
||||
message = message .. ": " .. details
|
||||
end
|
||||
|
||||
log("tool", message, {
|
||||
tool = tool_name,
|
||||
status = status,
|
||||
details = details,
|
||||
})
|
||||
end
|
||||
|
||||
return tool
|
||||
24
lua/codetyper/adapters/nvim/ui/logs/update.lua
Normal file
24
lua/codetyper/adapters/nvim/ui/logs/update.lua
Normal file
@@ -0,0 +1,24 @@
|
||||
local log = require("codetyper.adapters.nvim.ui.logs.log")
|
||||
|
||||
--- Log update/edit operation
|
||||
---@param filepath string Path of file being edited
|
||||
---@param added? number Lines added
|
||||
---@param removed? number Lines removed
|
||||
local function update(filepath, added, removed)
|
||||
local message = string.format("Update(%s)", vim.fn.fnamemodify(filepath, ":~:."))
|
||||
if added or removed then
|
||||
local parts = {}
|
||||
if added and added > 0 then
|
||||
table.insert(parts, string.format("Added %d lines", added))
|
||||
end
|
||||
if removed and removed > 0 then
|
||||
table.insert(parts, string.format("Removed %d lines", removed))
|
||||
end
|
||||
if #parts > 0 then
|
||||
message = message .. "\n ⎿ " .. table.concat(parts, ", ")
|
||||
end
|
||||
end
|
||||
log("action", message)
|
||||
end
|
||||
|
||||
return update
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user