15 Commits

Author SHA1 Message Date
ddd9ce7de8 Fix CI failures — StyLua parse error and Luacheck warnings
- Replace invalid // comment in window/init.lua with Lua -- comment
- Make check_for_closed_prompt a local function (was leaking global)
- Require get_config for close_tag pattern instead of undefined config
- Wire auto_process through preferences.is_auto_process_enabled()
- Add local extract_functions/classes/imports helpers to auto_index_file
- Remove unused comment_block_start/comment_block_end variables

Made-with: Cursor
2026-03-24 23:05:27 -04:00
a7d269944d Bump version to 1.0.2 — module refactoring to one-function-per-file
Migrated 10 monolithic modules (parser, cmp, context_modal, diff_review,
logs, logs_panel, thinking, throbber, commands, autocmds) into granular
pure files. Deleted all barrel files and updated consumers to import
directly. Removed unused dead code files.
2026-03-24 23:00:45 -04:00
5c20f57eb4 migrating autocmds 2026-03-24 22:56:38 -04:00
75de3198cd Adding the migration to pure files only 2026-03-24 22:31:49 -04:00
69c8061b8e migrating the logs 2026-03-24 21:57:02 -04:00
9687b352d5 migrating the logs file 2026-03-24 21:53:14 -04:00
4416626acf migrating the diff_review 2026-03-24 21:47:10 -04:00
565e3658b5 refactoring the context 2026-03-24 21:32:03 -04:00
f8ce473877 migrating the cmp brain 2026-03-24 21:18:06 -04:00
0d83c6ba4d Making changes on the parser file to pure functions 2026-03-24 21:02:14 -04:00
d93fed165f Refactor: moving injects and start the parser
- moving all the inject functions inside its proper folder
- starting the parser refactoring
2026-03-23 23:48:25 -04:00
Carlos Gutierrez
5fa7d7d347 Add SECURITY.md for security policy and reporting
Added a security policy document outlining supported versions and vulnerability reporting.
2026-03-18 23:36:20 -04:00
35b808ca1e release: v1.0.1 — CI, formatting, docs, version command
- Add :Coder version command with M.version constant
- Add .stylua.toml (2-space) and .luacheckrc configs
- Rewrite CI: lua.yaml (lint + auto-format + health), release.yaml
- Remove avante.nvim Rust/pre-commit workflows
- Fix 7 files missing local M = {}, cmp newline bug, loop.lua syntax
- Update all docs to match current project state
- Switch codebase from tabs to 2-space indentation

Made-with: Cursor
2026-03-18 23:31:41 -04:00
3a1472670b feat: adding lua linter 2026-03-18 23:29:02 -04:00
fe118e0885 fixing github actions 2026-03-18 23:20:00 -04:00
230 changed files with 15128 additions and 15086 deletions

View File

@@ -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

View File

@@ -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()

View File

@@ -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 }}

View File

@@ -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
View 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
View File

@@ -0,0 +1,6 @@
column_width = 120
line_endings = "Unix"
indent_type = "Spaces"
indent_width = 2
quote_style = "AutoPreferDouble"
call_parentheses = "Always"

View File

@@ -7,6 +7,74 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [1.0.3] - 2025-03-25
### Fixed
- Fixed `window/init.lua` containing invalid `//` comment syntax causing StyLua parse failure
- Fixed `check_for_closed_prompt.lua` declaring a global instead of local function
- Fixed `check_for_closed_prompt.lua` accessing undefined `config` variable — now properly requires `get_config`
- Fixed `check_for_closed_prompt_with_preference.lua` and `check_all_prompts_with_preference.lua` accessing undefined `auto_process` — now uses `preferences.is_auto_process_enabled()`
- Fixed `auto_index_file.lua` calling undefined `extract_functions`, `extract_classes`, `extract_imports` — added local implementations
- Removed unused `comment_block_start` and `comment_block_end` variables in `auto_index_file.lua`
## [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 +294,10 @@ 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.3...HEAD
[1.0.3]: https://github.com/cargdev/codetyper.nvim/compare/v1.0.2...v1.0.3
[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
View 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

View File

@@ -0,0 +1,290 @@
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")
local function extract_functions(content, _ext)
local results = {}
for line in content:gmatch("[^\n]+") do
local 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 name then
table.insert(results, { name = name })
end
end
return results
end
local function extract_classes(content, _ext)
local results = {}
for line in content:gmatch("[^\n]+") do
local 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 name then
table.insert(results, { name = name })
end
end
return results
end
local function extract_imports(content, _ext)
local results = {}
for line in content:gmatch("[^\n]+") do
local imp = line:match("import%s+.*%s+from%s+[\"']([^\"']+)[\"']")
or line:match("require%([\"']([^\"']+)[\"']%)")
or line:match("from%s+([%w_.]+)%s+import")
if imp then
table.insert(results, imp)
end
end
return results
end
--- 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 = "--"
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 = "//"
elseif file_ext == "py" or file_ext == "rb" or file_ext == "yaml" or file_ext == "yml" then
comment_prefix = "#"
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

View 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

View File

@@ -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")
local preferences = require("codetyper.config.preferences")
--- 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 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 preferences.is_auto_process_enabled() then
check_all_prompts()
end
end
return check_all_prompts_with_preference

View File

@@ -0,0 +1,189 @@
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")
local get_config = require("codetyper.utils.get_config").get_config
--- Check if the buffer has a newly closed prompt and auto-process
local 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]
local cfg = get_config()
if has_closing_tag(current_line, cfg.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

View File

@@ -0,0 +1,20 @@
local check_for_closed_prompt = require("codetyper.adapters.nvim.autocmds.check_for_closed_prompt")
local preferences = require("codetyper.config.preferences")
--- 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 preferences.is_auto_process_enabled() then
check_for_closed_prompt()
end
end
return check_for_closed_prompt_with_preference

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -0,0 +1,7 @@
local state = {
auto_opened_buffers = {},
auto_indexed_buffers = {},
brain_update_timers = {},
}
return state

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

View File

@@ -0,0 +1,7 @@
--- Check if cmp is available
---@return boolean
local function has_cmp()
return pcall(require, "cmp")
end
return has_cmp

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View File

@@ -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

View 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

View File

@@ -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

View 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

View 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

View 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

View 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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

View 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