From bba0647b473759254d4716cad1619ea3cd2d2c07 Mon Sep 17 00:00:00 2001 From: Carlos Gutierrez Date: Sun, 11 Jan 2026 15:24:06 -0500 Subject: [PATCH] feat: initial release of codetyper.nvim v0.2.0 AI-powered coding partner for Neovim with LLM integration. Features: - Split view for coder files (*.coder.*) and target files - Tag-based prompts with /@ and @/ syntax - Claude API and Ollama (local) LLM support - Smart prompt detection (refactor, add, document, explain) - Automatic code injection into target files - Project tree logging (.coder/tree.log) - Auto .gitignore management Ask Panel (chat interface): - Fixed at 1/4 screen width - File attachment with @ key - Ctrl+n for new chat - Ctrl+Enter to submit - Proper window close behavior - Navigation with Ctrl+h/j/k/l Commands: Coder, CoderOpen, CoderClose, CoderToggle, CoderProcess, CoderAsk, CoderTree, CoderTreeView --- CHANGELOG.md | 88 +++ CONTRIBUTING.md | 227 ++++++++ LICENSE | 21 + README.md | 605 ++++++++++++++++++++ doc/codetyper.txt | 220 ++++++++ llms.txt | 164 ++++++ lua/codetyper/ask.lua | 866 +++++++++++++++++++++++++++++ lua/codetyper/autocmds.lua | 349 ++++++++++++ lua/codetyper/commands.lua | 330 +++++++++++ lua/codetyper/config.lua | 84 +++ lua/codetyper/gitignore.lua | 190 +++++++ lua/codetyper/health.lua | 91 +++ lua/codetyper/init.lua | 69 +++ lua/codetyper/inject.lua | 239 ++++++++ lua/codetyper/llm/claude.lua | 154 +++++ lua/codetyper/llm/init.lua | 101 ++++ lua/codetyper/llm/ollama.lua | 173 ++++++ lua/codetyper/parser.lua | 183 ++++++ lua/codetyper/prompts/ask.lua | 128 +++++ lua/codetyper/prompts/code.lua | 93 ++++ lua/codetyper/prompts/document.lua | 136 +++++ lua/codetyper/prompts/init.lua | 56 ++ lua/codetyper/prompts/refactor.lua | 128 +++++ lua/codetyper/prompts/system.lua | 96 ++++ lua/codetyper/tree.lua | 245 ++++++++ lua/codetyper/types.lua | 44 ++ lua/codetyper/utils.lua | 125 +++++ lua/codetyper/window.lua | 177 ++++++ plugin/codetyper.lua | 121 ++++ 29 files changed, 5503 insertions(+) create mode 100644 CHANGELOG.md create mode 100644 CONTRIBUTING.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 doc/codetyper.txt create mode 100644 llms.txt create mode 100644 lua/codetyper/ask.lua create mode 100644 lua/codetyper/autocmds.lua create mode 100644 lua/codetyper/commands.lua create mode 100644 lua/codetyper/config.lua create mode 100644 lua/codetyper/gitignore.lua create mode 100644 lua/codetyper/health.lua create mode 100644 lua/codetyper/init.lua create mode 100644 lua/codetyper/inject.lua create mode 100644 lua/codetyper/llm/claude.lua create mode 100644 lua/codetyper/llm/init.lua create mode 100644 lua/codetyper/llm/ollama.lua create mode 100644 lua/codetyper/parser.lua create mode 100644 lua/codetyper/prompts/ask.lua create mode 100644 lua/codetyper/prompts/code.lua create mode 100644 lua/codetyper/prompts/document.lua create mode 100644 lua/codetyper/prompts/init.lua create mode 100644 lua/codetyper/prompts/refactor.lua create mode 100644 lua/codetyper/prompts/system.lua create mode 100644 lua/codetyper/tree.lua create mode 100644 lua/codetyper/types.lua create mode 100644 lua/codetyper/utils.lua create mode 100644 lua/codetyper/window.lua create mode 100644 plugin/codetyper.lua diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9eaf6bd --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,88 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [0.2.0] - 2026-01-11 + +### Added + +- **Ask Panel** - Chat interface for asking questions about code + - Fixed at 1/4 (25%) screen width for consistent layout + - File attachment with `@` key (uses Telescope if available) + - `Ctrl+n` to start a new chat (clears input and history) + - `Ctrl+Enter` to submit questions + - `Ctrl+f` to add current file as context + - `Ctrl+h/j/k/l` for window navigation + - `K/J` to jump between output and input windows + - `Y` to copy last response to clipboard + - `q` to close panel (closes both windows together) +- Auto-open Ask panel on startup (configurable via `auto_open_ask`) +- File content is now sent to LLM when attaching files with `@` + +### Changed + +- Ask panel width is now fixed at 25% (1/4 of screen) +- Improved close behavior - closing either Ask window closes both +- Proper focus management after closing Ask panel +- Compact UI elements to fit 1/4 width layout +- Changed "Assistant" label to "AI" in chat messages + +### Fixed + +- Ask panel window state sync issues +- Window focus returning to code after closing Ask panel +- NerdTree/nvim-tree causing Ask panel to resize incorrectly + +--- + +## [0.1.0] - 2026-01-11 + +### Added + +- Initial release of Codetyper.nvim +- Core plugin architecture with modular Lua structure +- Split window view for coder and target files +- Tag-based prompt system (`/@` to open, `@/` to close) +- Claude API integration for code generation +- Ollama API integration for local LLM support +- Automatic `.gitignore` management for coder files and `.coder/` folder +- Smart prompt type detection (refactor, add, document, explain) +- Code injection system with multiple strategies +- User commands: `Coder`, `CoderOpen`, `CoderClose`, `CoderToggle`, `CoderProcess`, `CoderTree`, `CoderTreeView` +- Health check module (`:checkhealth codetyper`) +- Comprehensive documentation and help files +- Telescope integration for file selection (optional) +- **Project tree logging**: Automatic `.coder/tree.log` maintenance + - Updates on file create, save, delete + - Debounced updates (1 second) for performance + - File type icons for visual clarity + - Ignores common build/dependency folders + +### Configuration Options + +- LLM provider selection (Claude/Ollama) +- Window position and width customization +- Custom prompt tag patterns +- Auto gitignore toggle + +--- + +## Version History + +### Legend + +- **Added** - New features +- **Changed** - Changes in existing functionality +- **Deprecated** - Soon-to-be removed features +- **Removed** - Removed features +- **Fixed** - Bug fixes +- **Security** - Vulnerability fixes + +[Unreleased]: https://github.com/cargdev/codetyper.nvim/compare/v0.2.0...HEAD +[0.2.0]: https://github.com/cargdev/codetyper.nvim/compare/v0.1.0...v0.2.0 +[0.1.0]: https://github.com/cargdev/codetyper.nvim/releases/tag/v0.1.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..1508cce --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,227 @@ +# Contributing to Codetyper.nvim + +First off, thank you for considering contributing to Codetyper.nvim! 🎉 + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Setup](#development-setup) +- [Project Structure](#project-structure) +- [Making Changes](#making-changes) +- [Submitting Changes](#submitting-changes) +- [Style Guide](#style-guide) +- [Testing](#testing) +- [Questions](#questions) + +## Code of Conduct + +This project and everyone participating in it is governed by our commitment to creating a welcoming and inclusive environment. Please be respectful and constructive in all interactions. + +## Getting Started + +1. Fork the repository +2. Clone your fork locally +3. Set up the development environment +4. Create a branch for your changes +5. Make your changes +6. Submit a pull request + +## Development Setup + +### Prerequisites + +- Neovim >= 0.8.0 +- Lua 5.1+ or LuaJIT +- Git + +### Local Development + +1. Clone the repository: + ```bash + git clone https://github.com/YOUR_USERNAME/codetyper.nvim.git + cd codetyper.nvim + ``` + +2. Create a minimal test configuration: + ```lua + -- test/minimal_init.lua + vim.opt.runtimepath:append(".") + require("codetyper").setup({ + llm = { + provider = "ollama", -- Use local for testing + }, + }) + ``` + +3. Test your changes: + ```bash + nvim --clean -u test/minimal_init.lua + ``` + +## Project Structure + +``` +codetyper.nvim/ +├── lua/ +│ └── codetyper/ +│ ├── init.lua # Main entry point +│ ├── config.lua # Configuration management +│ ├── types.lua # Type definitions +│ ├── utils.lua # Utility functions +│ ├── commands.lua # Command definitions +│ ├── window.lua # Window/split management +│ ├── parser.lua # Prompt tag parser +│ ├── gitignore.lua # .gitignore management +│ ├── autocmds.lua # Autocommands +│ ├── inject.lua # Code injection +│ ├── health.lua # Health check +│ └── llm/ +│ ├── init.lua # LLM interface +│ ├── claude.lua # Claude API client +│ └── ollama.lua # Ollama API client +├── plugin/ +│ └── codetyper.lua # Plugin loader +├── doc/ +│ └── codetyper.txt # Vim help documentation +├── README.md +├── LICENSE +├── CHANGELOG.md +├── CONTRIBUTING.md +└── llms.txt +``` + +## Making Changes + +### Branch Naming + +Use descriptive branch names: +- `feature/description` - New features +- `fix/description` - Bug fixes +- `docs/description` - Documentation updates +- `refactor/description` - Code refactoring + +### Commit Messages + +Follow conventional commits: +``` +type(scope): description + +[optional body] + +[optional footer] +``` + +Types: +- `feat` - New feature +- `fix` - Bug fix +- `docs` - Documentation +- `style` - Formatting, no code change +- `refactor` - Code restructuring +- `test` - Adding tests +- `chore` - Maintenance + +Examples: +``` +feat(llm): add support for GPT-4 API +fix(parser): handle nested prompt tags +docs(readme): update installation instructions +``` + +## Submitting Changes + +1. **Ensure your code follows the style guide** +2. **Update documentation** if needed +3. **Update CHANGELOG.md** for notable changes +4. **Test your changes** thoroughly +5. **Create a pull request** with: + - Clear title describing the change + - Description of what and why + - Reference to any related issues + +### Pull Request Template + +```markdown +## Description +[Describe your changes] + +## Type of Change +- [ ] Bug fix +- [ ] New feature +- [ ] Documentation update +- [ ] Refactoring + +## Testing +[Describe how you tested your changes] + +## Checklist +- [ ] Code follows style guide +- [ ] Documentation updated +- [ ] CHANGELOG.md updated +- [ ] All tests pass +``` + +## Style Guide + +### Lua Style + +- Use 2 spaces for indentation +- Use `snake_case` for variables and functions +- Use `PascalCase` for module names +- Add type annotations with `---@param`, `---@return`, etc. +- Document public functions with LuaDoc comments + +```lua +---@mod module_name Module description + +local M = {} + +--- Description of the function +---@param name string The parameter description +---@return boolean Success status +function M.example_function(name) + -- Implementation + return true +end + +return M +``` + +### Documentation + +- Keep README.md up to date +- Update doc/codetyper.txt for new features +- Use clear, concise language +- Include examples where helpful + +## Testing + +### Manual Testing + +1. Test all commands work correctly +2. Test with different file types +3. Test window management +4. Test LLM integration (both Claude and Ollama) +5. Test edge cases (empty files, large files, etc.) + +### Health Check + +Run `:checkhealth codetyper` to verify the plugin setup. + +## Questions? + +Feel free to: +- Open an issue for bugs or feature requests +- Start a discussion for questions +- Reach out to the maintainer + +## Contact + +- **Maintainer**: cargdev +- **Email**: carlos.gutierrez@carg.dev +- **Website**: [cargdev.io](https://cargdev.io) +- **Blog**: [blog.cargdev.io](https://blog.cargdev.io) + +--- + +Thank you for contributing! 🙏 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..aa31ea1 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 cargdev (Carlos Gutierrez) + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..528e6d6 --- /dev/null +++ b/README.md @@ -0,0 +1,605 @@ +# 🚀 Codetyper.nvim + +**AI-powered coding partner for Neovim** - Write code faster with LLM assistance while staying in control of your logic. + +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![Neovim](https://img.shields.io/badge/Neovim-0.8%2B-green.svg)](https://neovim.io/) + +## ✨ Features + +- **🪟 Split View**: Work with your code and prompts side by side +- **💬 Ask Panel**: Chat interface for questions and explanations (like avante.nvim) +- **🏷️ Tag-based Prompts**: Use `/@` and `@/` tags to write natural language prompts +- **🤖 Multiple LLM Providers**: Support for Claude API and Ollama (local) +- **📝 Smart Injection**: Automatically detects prompt type (refactor, add, document) +- **🔒 Git Integration**: Automatically adds `.coder.*` files and `.coder/` folder to `.gitignore` +- **🌳 Project Tree Logging**: Automatically maintains a `tree.log` tracking your project structure +- **⚡ Lazy Loading**: Only loads when you need it + +--- + +## 📋 Table of Contents + +- [Requirements](#-requirements) +- [Installation](#-installation) +- [Quick Start](#-quick-start) +- [Configuration](#%EF%B8%8F-configuration) +- [Commands Reference](#-commands-reference) +- [Usage Guide](#-usage-guide) +- [How It Works](#%EF%B8%8F-how-it-works) +- [Keymaps](#-keymaps-suggested) +- [Health Check](#-health-check) +- [Contributing](#-contributing) + +--- + +## 📋 Requirements + +- Neovim >= 0.8.0 +- curl (for API calls) +- Claude API key **OR** Ollama running locally + +--- + +## 📦 Installation + +### Using [lazy.nvim](https://github.com/folke/lazy.nvim) + +```lua +{ + "cargdev/codetyper.nvim", + cmd = { "Coder", "CoderOpen", "CoderToggle" }, + keys = { + { "co", "Coder open", desc = "Coder: Open" }, + { "ct", "Coder toggle", desc = "Coder: Toggle" }, + { "cp", "Coder process", desc = "Coder: Process" }, + }, + config = function() + require("codetyper").setup({ + llm = { + provider = "claude", -- or "ollama" + }, + }) + end, +} +``` + +### Using [packer.nvim](https://github.com/wbthomason/packer.nvim) + +```lua +use { + "cargdev/codetyper.nvim", + config = function() + require("codetyper").setup() + end, +} +``` + +--- + +## 🚀 Quick Start + +**1. Open a file and start Coder:** +```vim +:e src/utils.ts +:Coder open +``` + +**2. Write a prompt in the coder file (left panel):** +```typescript +/@ Create a function to validate email addresses +using regex, return boolean @/ +``` + +**3. The LLM generates code and injects it into `utils.ts` (right panel)** + +That's it! You're now coding with AI assistance. 🎉 + +--- + +## ⚙️ Configuration + +```lua +require("codetyper").setup({ + -- LLM Provider Configuration + llm = { + provider = "claude", -- "claude" or "ollama" + + -- Claude (Anthropic) settings + claude = { + api_key = nil, -- Uses ANTHROPIC_API_KEY env var if nil + model = "claude-sonnet-4-20250514", + }, + + -- Ollama (local) settings + ollama = { + host = "http://localhost:11434", + model = "codellama", + }, + }, + + -- Window Configuration + window = { + width = 0.25, -- 25% of screen width (1/4) for Ask panel + position = "left", -- "left" or "right" + border = "rounded", -- Border style for floating windows + }, + + -- Prompt Tag Patterns + patterns = { + open_tag = "/@", -- Tag to start a prompt + close_tag = "@/", -- Tag to end a prompt + file_pattern = "*.coder.*", + }, + + -- Auto Features + auto_gitignore = true, -- Automatically add coder files to .gitignore +}) +``` + +### Environment Variables + +| Variable | Description | +|----------|-------------| +| `ANTHROPIC_API_KEY` | Your Claude API key (if not set in config) | + +--- + +## 📜 Commands Reference + +### Main Command + +| Command | Description | +|---------|-------------| +| `:Coder {subcommand}` | Main command with subcommands below | + +### Subcommands + +| Subcommand | Alias | Description | +|------------|-------|-------------| +| `open` | `:CoderOpen` | Open the coder split view for current file | +| `close` | `:CoderClose` | Close the coder split view | +| `toggle` | `:CoderToggle` | Toggle the coder split view on/off | +| `process` | `:CoderProcess` | Process the last prompt and generate code | +| `status` | - | Show plugin status and project statistics | +| `focus` | - | Switch focus between coder and target windows | +| `tree` | `:CoderTree` | Manually refresh the tree.log file | +| `tree-view` | `:CoderTreeView` | Open tree.log in a readonly split | +| `ask` | `:CoderAsk` | Open the Ask panel for questions | +| `ask-toggle` | `:CoderAskToggle` | Toggle the Ask panel | +| `ask-clear` | `:CoderAskClear` | Clear Ask chat history | + +--- + +### Command Details + +#### `:Coder open` / `:CoderOpen` + +Opens a split view with: +- **Left panel**: The coder file (`*.coder.*`) where you write prompts +- **Right panel**: The target file where generated code is injected + +```vim +" If you have index.ts open: +:Coder open +" Creates/opens index.coder.ts on the left +``` + +**Behavior:** +- If no file is in buffer, opens a file picker (Telescope if available) +- Creates the coder file if it doesn't exist +- Automatically sets the correct filetype for syntax highlighting + +--- + +#### `:Coder close` / `:CoderClose` + +Closes the coder split view, keeping only your target file open. + +```vim +:Coder close +``` + +--- + +#### `:Coder toggle` / `:CoderToggle` + +Toggles the coder view on or off. Useful for quick switching. + +```vim +:Coder toggle +``` + +--- + +#### `:Coder process` / `:CoderProcess` + +Processes the last completed prompt in the coder file and sends it to the LLM. + +```vim +" After writing a prompt and closing with @/ +:Coder process +``` + +**What happens:** +1. Finds the last `/@...@/` prompt in the coder buffer +2. Detects the prompt type (refactor, add, document, etc.) +3. Sends it to the configured LLM with file context +4. Injects the generated code into the target file + +--- + +#### `:Coder status` + +Displays current plugin status including: +- LLM provider and configuration +- API key status (configured/not set) +- Window settings +- Project statistics (files, directories) +- Tree log path + +```vim +:Coder status +``` + +--- + +#### `:Coder focus` + +Switches focus between the coder window and target window. + +```vim +:Coder focus +" Press again to switch back +``` + +--- + +#### `:Coder tree` / `:CoderTree` + +Manually refreshes the `.coder/tree.log` file with current project structure. + +```vim +:Coder tree +``` + +> Note: The tree is automatically updated on file save/create/delete. + +--- + +#### `:Coder tree-view` / `:CoderTreeView` + +Opens the tree.log file in a readonly split for viewing your project structure. + +```vim +:Coder tree-view +``` + +--- + +#### `:Coder ask` / `:CoderAsk` + +Opens the **Ask panel** - a chat interface similar to avante.nvim for asking questions about your code, getting explanations, or general programming help. + +```vim +:Coder ask +``` + +**The Ask Panel Layout:** +``` +┌───────────────────┬─────────────────────────────────────────┐ +│ 💬 Chat (output) │ │ +│ │ Your code file │ +│ ┌─ 👤 You ──── │ │ +│ │ What is this? │ │ +│ │ │ +│ ┌─ 🤖 AI ───── │ │ +│ │ This is... │ │ +├───────────────────┤ │ +│ ✏️ Input │ │ +│ Type question... │ │ +└───────────────────┴─────────────────────────────────────────┘ + (1/4 width) (3/4 width) +``` + +> **Note:** The Ask panel is fixed at 1/4 (25%) of the screen width. + +**Ask Panel Keymaps:** + +| Key | Mode | Description | +|-----|------|-------------| +| `@` | Insert | Attach/reference a file | +| `Ctrl+Enter` | Insert/Normal | Submit question | +| `Ctrl+n` | Insert/Normal | Start new chat (clear all) | +| `Ctrl+f` | Insert/Normal | Add current file as context | +| `Ctrl+h/j/k/l` | Normal/Insert | Navigate between windows | +| `q` | Normal | Close panel (closes both windows) | +| `K` / `J` | Normal | Jump between output/input | +| `Y` | Normal | Copy last response to clipboard | + +--- + +#### `:Coder ask-toggle` / `:CoderAskToggle` + +Toggles the Ask panel on or off. + +```vim +:Coder ask-toggle +``` + +--- + +#### `:Coder ask-clear` / `:CoderAskClear` + +Clears the Ask panel chat history. + +```vim +:Coder ask-clear +``` + +--- + +## 📖 Usage Guide + +### Step 1: Open Your Project File + +Open any source file you want to work with: + +```vim +:e src/components/Button.tsx +``` + +### Step 2: Start Coder View + +```vim +:Coder open +``` + +This creates a split: +``` +┌─────────────────────────┬─────────────────────────┐ +│ Button.coder.tsx │ Button.tsx │ +│ (write prompts here) │ (your actual code) │ +└─────────────────────────┴─────────────────────────┘ +``` + +### Step 3: Write Your Prompt + +In the coder file (left), write your prompt using tags: + +```tsx +/@ Create a Button component with the following props: +- variant: 'primary' | 'secondary' | 'danger' +- size: 'sm' | 'md' | 'lg' +- disabled: boolean +- onClick: function +Use Tailwind CSS for styling @/ +``` + +### Step 4: Process the Prompt + +When you close the tag with `@/`, you'll be prompted to process. Or manually: + +```vim +:Coder process +``` + +### Step 5: Review Generated Code + +The generated code appears in your target file (right panel). Review, edit if needed, and save! + +--- + +### Prompt Types + +The plugin automatically detects what you want based on keywords: + +| Keywords | Type | Behavior | +|----------|------|----------| +| `refactor`, `rewrite`, `change` | Refactor | Replaces code in target file | +| `add`, `create`, `implement`, `new` | Add | Inserts code at cursor position | +| `document`, `comment`, `jsdoc` | Document | Adds documentation above code | +| `explain`, `what`, `how` | Explain | Shows explanation (no injection) | +| *(other)* | Generic | Prompts you for injection method | + +--- + +### Prompt Examples + +#### Creating New Functions + +```typescript +/@ Create an async function fetchUsers that: +- Takes a page number and limit as parameters +- Fetches from /api/users endpoint +- Returns typed User[] array +- Handles errors gracefully @/ +``` + +#### Refactoring Code + +```typescript +/@ Refactor the handleSubmit function to: +- Use async/await instead of .then() +- Add proper TypeScript types +- Extract validation logic into separate function @/ +``` + +#### Adding Documentation + +```typescript +/@ Add JSDoc documentation to all exported functions +including @param, @returns, and @example tags @/ +``` + +#### Implementing Patterns + +```typescript +/@ Implement the singleton pattern for DatabaseConnection class +with lazy initialization and thread safety @/ +``` + +#### Adding Tests + +```typescript +/@ Create unit tests for the calculateTotal function +using Jest, cover edge cases: +- Empty array +- Negative numbers +- Large numbers @/ +``` + +--- + +## 🏗️ How It Works + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ Neovim │ +├────────────────────────────┬────────────────────────────────────┤ +│ src/api.coder.ts │ src/api.ts │ +│ │ │ +│ /@ Create a REST client │ // Generated code appears here │ +│ class with methods for │ export class RestClient { │ +│ GET, POST, PUT, DELETE │ async get(url: string) { │ +│ with TypeScript │ // ... │ +│ generics @/ │ } │ +│ │ } │ +└────────────────────────────┴────────────────────────────────────┘ +``` + +### File Structure + +``` +your-project/ +├── .coder/ # Auto-created, gitignored +│ └── tree.log # Project structure log +├── src/ +│ ├── index.ts # Your source file +│ ├── index.coder.ts # Coder file (gitignored) +│ ├── utils.ts +│ └── utils.coder.ts +└── .gitignore # Auto-updated with coder patterns +``` + +### The Flow + +1. **You write prompts** in `*.coder.*` files using `/@...@/` tags +2. **Plugin detects** when you close a prompt tag +3. **Context is gathered** from the target file (content, language, etc.) +4. **LLM generates** code based on your prompt and context +5. **Code is injected** into the target file based on prompt type +6. **You review and save** - you're always in control! + +### Project Tree Logging + +The `.coder/tree.log` file is automatically maintained: + +``` +# Project Tree: my-project +# Generated: 2026-01-11 15:30:45 +# By: Codetyper.nvim + +📦 my-project +├── 📁 src +│ ├── 📘 index.ts +│ ├── 📘 utils.ts +│ └── 📁 components +│ └── ⚛️ Button.tsx +├── 📋 package.json +└── 📝 README.md +``` + +Updated automatically when you: +- Create new files +- Save files +- Delete files + +--- + +## 🔑 Keymaps (Suggested) + +Add these to your Neovim config: + +```lua +-- Codetyper keymaps +local map = vim.keymap.set + +-- Coder view +map("n", "co", "Coder open", { desc = "Coder: Open view" }) +map("n", "cc", "Coder close", { desc = "Coder: Close view" }) +map("n", "ct", "Coder toggle", { desc = "Coder: Toggle view" }) +map("n", "cp", "Coder process", { desc = "Coder: Process prompt" }) +map("n", "cs", "Coder status", { desc = "Coder: Show status" }) +map("n", "cf", "Coder focus", { desc = "Coder: Switch focus" }) +map("n", "cv", "Coder tree-view", { desc = "Coder: View tree" }) + +-- Ask panel +map("n", "ca", "Coder ask", { desc = "Coder: Open Ask" }) +map("n", "cA", "Coder ask-toggle", { desc = "Coder: Toggle Ask" }) +map("n", "cx", "Coder ask-clear", { desc = "Coder: Clear Ask" }) +``` + +Or with [which-key.nvim](https://github.com/folke/which-key.nvim): + +```lua +local wk = require("which-key") +wk.register({ + ["c"] = { + name = "+coder", + o = { "Coder open", "Open view" }, + c = { "Coder close", "Close view" }, + t = { "Coder toggle", "Toggle view" }, + p = { "Coder process", "Process prompt" }, + s = { "Coder status", "Show status" }, + f = { "Coder focus", "Switch focus" }, + v = { "Coder tree-view", "View tree" }, + }, +}) +``` + +--- + +## 🔧 Health Check + +Verify your setup is correct: + +```vim +:checkhealth codetyper +``` + +This checks: +- ✅ Neovim version +- ✅ curl availability +- ✅ LLM configuration +- ✅ API key status +- ✅ Telescope availability (optional) +- ✅ Gitignore configuration + +--- + +## 🤝 Contributing + +Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for guidelines. + +--- + +## 📄 License + +MIT License - see [LICENSE](LICENSE) for details. + +--- + +## 👤 Author + +**cargdev** + +- 🌐 Website: [cargdev.io](https://cargdev.io) +- 📝 Blog: [blog.cargdev.io](https://blog.cargdev.io) +- 📧 Email: carlos.gutierrez@carg.dev + +--- + +

+ Made with ❤️ for the Neovim community +

diff --git a/doc/codetyper.txt b/doc/codetyper.txt new file mode 100644 index 0000000..e4869d9 --- /dev/null +++ b/doc/codetyper.txt @@ -0,0 +1,220 @@ +*codetyper.txt* AI-powered coding partner for Neovim + +Author: cargdev +Homepage: https://github.com/cargdev/codetyper.nvim +License: MIT + +============================================================================== +CONTENTS *codetyper-contents* + + 1. Introduction ............................ |codetyper-introduction| + 2. Requirements ............................ |codetyper-requirements| + 3. Installation ............................ |codetyper-installation| + 4. Configuration ........................... |codetyper-configuration| + 5. Usage ................................... |codetyper-usage| + 6. Commands ................................ |codetyper-commands| + 7. Workflow ................................ |codetyper-workflow| + 8. API ..................................... |codetyper-api| + +============================================================================== +1. INTRODUCTION *codetyper-introduction* + +Codetyper.nvim is an AI-powered coding partner that helps you write code +faster using LLM APIs (Claude, Ollama) with a unique workflow. + +Instead of generating files directly, Codetyper watches what you type in +special `.coder.*` files and generates code when you close prompt tags. + +Key features: +- Split view with coder file and target file side by side +- Prompt-based code generation using /@ ... @/ tags +- Support for Claude and Ollama LLM providers +- Automatic .gitignore management for coder files and .coder/ folder +- Intelligent code injection based on prompt type +- Automatic project tree logging in .coder/tree.log + +============================================================================== +2. REQUIREMENTS *codetyper-requirements* + +- Neovim >= 0.8.0 +- curl (for API calls) +- Claude API key (if using Claude) or Ollama running locally + +============================================================================== +3. INSTALLATION *codetyper-installation* + +Using lazy.nvim: >lua + + { + "cargdev/codetyper.nvim", + config = function() + require("codetyper").setup({ + llm = { + provider = "claude", -- or "ollama" + claude = { + api_key = vim.env.ANTHROPIC_API_KEY, + }, + }, + }) + end, + } +< +Using packer.nvim: >lua + + use { + "cargdev/codetyper.nvim", + config = function() + require("codetyper").setup() + end, + } +< +============================================================================== +4. CONFIGURATION *codetyper-configuration* + +Default configuration: >lua + + require("codetyper").setup({ + llm = { + provider = "claude", -- "claude" or "ollama" + claude = { + api_key = nil, -- Uses ANTHROPIC_API_KEY env var if nil + model = "claude-sonnet-4-20250514", + }, + ollama = { + host = "http://localhost:11434", + model = "codellama", + }, + }, + window = { + width = 0.4, -- 40% of screen width + position = "left", -- "left" or "right" + border = "rounded", + }, + patterns = { + open_tag = "/@", + close_tag = "@/", + file_pattern = "*.coder.*", + }, + auto_gitignore = true, + }) +< +============================================================================== +5. USAGE *codetyper-usage* + +1. Open any file (e.g., `index.ts`) +2. Run `:Coder open` to create/open the corresponding coder file +3. In the coder file, write prompts using the tag syntax: +> + /@ Create a function that fetches user data from an API + with error handling and returns a User object @/ +< +4. When you close the tag with `@/`, the plugin will: + - Send the prompt to the configured LLM + - Generate the code + - Inject it into the target file + +============================================================================== +6. COMMANDS *codetyper-commands* + + *:Coder* +:Coder [subcommand] + Main command with subcommands: + + open Open coder view for current file + close Close coder view + toggle Toggle coder view + process Process the last prompt and generate code + status Show plugin status and project statistics + focus Switch focus between coder and target windows + tree Manually refresh the tree.log file + tree-view Open tree.log in a split view + + *:CoderOpen* +:CoderOpen + Open the coder split view for the current file. + + *:CoderClose* +:CoderClose + Close the coder split view. + + *:CoderToggle* +:CoderToggle + Toggle the coder split view. + + *:CoderProcess* +:CoderProcess + Process the last prompt in the current coder buffer and + inject generated code into the target file. + + *:CoderTree* +:CoderTree + Manually refresh the tree.log file in .coder/ folder. + + *:CoderTreeView* +:CoderTreeView + Open the tree.log file in a vertical split for viewing. + +============================================================================== +7. WORKFLOW *codetyper-workflow* + +The Coder Workflow~ + +1. Target File: Your actual source file (e.g., `src/utils.ts`) +2. Coder File: A companion file (e.g., `src/utils.coder.ts`) + +The coder file mirrors your target file's location and extension. +When you write prompts in the coder file and close them, the +generated code appears in the target file. + +Prompt Types~ + +The plugin detects the type of request from your prompt: + +- "refactor" - Modifies existing code +- "add" / "create" / "implement" - Adds new code +- "document" / "comment" - Adds documentation +- "explain" - Provides explanations (no code injection) + +Example Prompts~ +> + /@ Refactor this function to use async/await @/ + + /@ Add input validation to the form handler @/ + + /@ Add JSDoc comments to all exported functions @/ + + /@ Create a React hook for managing form state + with validation support @/ +< +Project Tree Logging~ + +Codetyper automatically maintains a .coder/ folder with a tree.log file: +> + .coder/ + └── tree.log # Auto-updated project structure +< +The tree.log is updated whenever you: +- Create a new file +- Save a file +- Delete a file +- Change directories + +View the tree anytime with `:Coder tree-view` or refresh with `:Coder tree`. + +============================================================================== +8. API *codetyper-api* + + *codetyper.setup()* +codetyper.setup({opts}) + Initialize the plugin with configuration options. + + *codetyper.get_config()* +codetyper.get_config() + Returns the current configuration table. + + *codetyper.is_initialized()* +codetyper.is_initialized() + Returns true if the plugin has been initialized. + +============================================================================== +vim:tw=78:ts=8:ft=help:norl: diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..5d879e9 --- /dev/null +++ b/llms.txt @@ -0,0 +1,164 @@ +# Codetyper.nvim - LLM Documentation + +> This file helps LLMs understand the Codetyper.nvim plugin structure and functionality. + +## Overview + +Codetyper.nvim is a Neovim plugin written in Lua that acts as an AI-powered coding partner. It integrates with LLM APIs (Claude, Ollama) to help developers write code faster using a unique prompt-based workflow. + +## Core Concept + +Instead of having an AI generate entire files, Codetyper lets developers maintain control: + +1. Developer opens a source file (e.g., `index.ts`) +2. A companion "coder file" is created (`index.coder.ts`) +3. Developer writes prompts using special tags: `/@ prompt @/` +4. When the closing tag is typed, the LLM generates code +5. Generated code is injected into the target file + +## Plugin Architecture + +``` +lua/codetyper/ +├── init.lua # Main entry, setup function, module initialization +├── config.lua # Configuration management, defaults, validation +├── types.lua # Lua type definitions for LSP/documentation +├── utils.lua # Utility functions (file ops, notifications) +├── commands.lua # Vim command definitions (:Coder, :CoderOpen, etc.) +├── window.lua # Split window management (open, close, toggle) +├── parser.lua # Parses /@ @/ tags from buffer content +├── gitignore.lua # Manages .gitignore entries for coder files and .coder/ folder +├── autocmds.lua # Autocommands for tag detection, filetype, tree updates +├── inject.lua # Code injection strategies +├── health.lua # Health check for :checkhealth +├── tree.lua # Project tree logging (.coder/tree.log) +└── llm/ + ├── init.lua # LLM interface, provider selection + ├── claude.lua # Claude API client (Anthropic) + └── ollama.lua # Ollama API client (local LLMs) +``` + +## .coder/ Folder + +The plugin automatically creates and maintains a `.coder/` folder in your project: + +``` +.coder/ +└── tree.log # Project structure, auto-updated on file changes +``` + +The `tree.log` contains: +- Project name and timestamp +- Full directory tree with file type icons +- Automatically ignores: hidden files, node_modules, .git, build folders, coder files + +Tree updates are triggered by: +- `BufWritePost` - When files are saved +- `BufNewFile` - When new files are created +- `BufDelete` - When files are deleted +- `DirChanged` - When changing directories + +Updates are debounced (1 second) to prevent excessive writes. + +## Key Functions + +### Setup +```lua +require("codetyper").setup({ + llm = { provider = "claude" | "ollama", ... }, + window = { width = 0.4, position = "left" }, + patterns = { open_tag = "/@", close_tag = "@/" }, + auto_gitignore = true, +}) +``` + +### Commands +- `:Coder open` - Opens split view with coder file +- `:Coder close` - Closes the split +- `:Coder toggle` - Toggles the view +- `:Coder process` - Manually triggers code generation +- `:Coder status` - Shows configuration status and project stats +- `:Coder tree` - Manually refresh tree.log +- `:Coder tree-view` - Open tree.log in split view + +### Prompt Tags +- Opening tag: `/@` +- Closing tag: `@/` +- Content between tags is the prompt sent to LLM + +### Prompt Types (Auto-detected) +- `refactor` - Modifies existing code +- `add` - Adds new code at cursor/end +- `document` - Adds documentation/comments +- `explain` - Explanations (no code injection) +- `generic` - User chooses injection method + +## File Naming Convention + +| Target File | Coder File | +|-------------|------------| +| `index.ts` | `index.coder.ts` | +| `utils.py` | `utils.coder.py` | +| `main.lua` | `main.coder.lua` | + +Pattern: `name.coder.extension` + +## Configuration Schema + +```lua +{ + llm = { + provider = "claude", -- "claude" | "ollama" + claude = { + api_key = nil, -- string, uses ANTHROPIC_API_KEY env if nil + model = "claude-sonnet-4-20250514", + }, + ollama = { + host = "http://localhost:11434", + model = "codellama", + }, + }, + window = { + width = 0.4, -- number (percentage if <=1, columns if >1) + position = "left", -- "left" | "right" + border = "rounded", -- border style for floating windows + }, + patterns = { + open_tag = "/@", -- string + close_tag = "@/", -- string + file_pattern = "*.coder.*", + }, + auto_gitignore = true, -- boolean +} +``` + +## LLM Integration + +### Claude API +- Endpoint: `https://api.anthropic.com/v1/messages` +- Uses `x-api-key` header for authentication +- Requires `anthropic-version: 2023-06-01` header + +### Ollama API +- Endpoint: `{host}/api/generate` +- No authentication required for local instances +- Health check via `/api/tags` endpoint + +## Code Injection Strategies + +1. **Refactor**: Replace entire file content +2. **Add**: Insert at cursor position in target file +3. **Document**: Insert above current function/class +4. **Generic**: Prompt user for action (replace/insert/append/clipboard) + +## Dependencies + +- **Required**: Neovim >= 0.8.0, curl +- **Optional**: telescope.nvim (enhanced file picker) + +## Contact + +- Author: cargdev +- Email: carlos.gutierrez@carg.dev +- Website: https://cargdev.io +- Blog: https://blog.cargdev.io diff --git a/lua/codetyper/ask.lua b/lua/codetyper/ask.lua new file mode 100644 index 0000000..16669b3 --- /dev/null +++ b/lua/codetyper/ask.lua @@ -0,0 +1,866 @@ +---@mod codetyper.ask Ask window for Codetyper.nvim (similar to avante.nvim) + +local M = {} + +local utils = require("codetyper.utils") + +---@class AskState +---@field input_buf number|nil Input buffer +---@field input_win number|nil Input window +---@field output_buf number|nil Output buffer +---@field output_win number|nil Output window +---@field is_open boolean Whether the ask panel is open +---@field history table Chat history +---@field referenced_files table Files referenced with @ + +---@type AskState +local state = { + input_buf = nil, + input_win = nil, + output_buf = nil, + output_win = nil, + is_open = false, + history = {}, + referenced_files = {}, + target_width = nil, -- Store the target width to maintain it +} + +--- Get the ask window configuration +---@return table Config +local function get_config() + local ok, codetyper = pcall(require, "codetyper") + if ok and codetyper.is_initialized() then + return codetyper.get_config() + end + return { + window = { width = 0.4, border = "rounded" }, + } +end + +--- Create the output buffer (chat history) +---@return number Buffer number +local function create_output_buffer() + local buf = vim.api.nvim_create_buf(false, true) + + vim.bo[buf].buftype = "nofile" + vim.bo[buf].bufhidden = "hide" + vim.bo[buf].swapfile = false + vim.bo[buf].filetype = "markdown" + + -- Set initial content + local header = { + "╔═══════════════════════════════════╗", + "║ 🤖 CODETYPER ASK ║", + "╠═══════════════════════════════════╣", + "║ Ask about code or concepts ║", + "║ ║", + "║ 💡 Keymaps: ║", + "║ @ → attach file ║", + "║ C-Enter → send ║", + "║ C-n → new chat ║", + "║ C-f → add current file ║", + "║ C-h/j/k/l → navigate ║", + "║ q → close │ K/J → jump ║", + "╚═══════════════════════════════════╝", + "", + } + vim.api.nvim_buf_set_lines(buf, 0, -1, false, header) + + return buf +end + +--- Create the input buffer +---@return number Buffer number +local function create_input_buffer() + local buf = vim.api.nvim_create_buf(false, true) + + vim.bo[buf].buftype = "nofile" + vim.bo[buf].bufhidden = "hide" + vim.bo[buf].swapfile = false + vim.bo[buf].filetype = "markdown" + + -- Set placeholder text + local placeholder = { + "┌───────────────────────────────────┐", + "│ 💬 Type your question here... │", + "│ │", + "│ @ attach │ C-Enter send │ C-n new │", + "└───────────────────────────────────┘", + } + vim.api.nvim_buf_set_lines(buf, 0, -1, false, placeholder) + + return buf +end + +--- Setup keymaps for the input buffer +---@param buf number Buffer number +local function setup_input_keymaps(buf) + local opts = { buffer = buf, noremap = true, silent = true } + + -- Submit with Ctrl+Enter + vim.keymap.set("i", "", function() + M.submit() + end, opts) + + vim.keymap.set("n", "", function() + M.submit() + end, opts) + + vim.keymap.set("n", "", function() + M.submit() + end, opts) + + -- Include current file context with Ctrl+F + vim.keymap.set({ "n", "i" }, "", function() + M.include_file_context() + end, opts) + + -- File picker with @ + vim.keymap.set("i", "@", function() + M.show_file_picker() + end, opts) + + -- Close with q in normal mode + vim.keymap.set("n", "q", function() + M.close() + end, opts) + + -- Clear input with Ctrl+c + vim.keymap.set("n", "", function() + M.clear_input() + end, opts) + + -- New chat with Ctrl+n (clears everything) + vim.keymap.set({ "n", "i" }, "", function() + M.new_chat() + end, opts) + + -- Window navigation (works in both normal and insert mode) + vim.keymap.set({ "n", "i" }, "", function() + vim.cmd("wincmd h") + end, opts) + + vim.keymap.set({ "n", "i" }, "", function() + vim.cmd("wincmd j") + end, opts) + + vim.keymap.set({ "n", "i" }, "", function() + vim.cmd("wincmd k") + end, opts) + + vim.keymap.set({ "n", "i" }, "", function() + vim.cmd("wincmd l") + end, opts) + + -- Jump to output window + vim.keymap.set("n", "K", function() + M.focus_output() + end, opts) + + -- When entering insert mode, clear placeholder + vim.api.nvim_create_autocmd("InsertEnter", { + buffer = buf, + callback = function() + local lines = vim.api.nvim_buf_get_lines(buf, 0, -1, false) + local content = table.concat(lines, "\n") + if content:match("Type your question here") then + vim.api.nvim_buf_set_lines(buf, 0, -1, false, { "" }) + end + end, + }) +end + +--- Setup keymaps for the output buffer +---@param buf number Buffer number +local function setup_output_keymaps(buf) + local opts = { buffer = buf, noremap = true, silent = true } + + -- Close with q + vim.keymap.set("n", "q", function() + M.close() + end, opts) + + -- Clear history with Ctrl+c + vim.keymap.set("n", "", function() + M.clear_history() + end, opts) + + -- New chat with Ctrl+n (clears everything) + vim.keymap.set("n", "", function() + M.new_chat() + end, opts) + + -- Copy last response with Y + vim.keymap.set("n", "Y", function() + M.copy_last_response() + end, opts) + + -- Jump to input with i or J + vim.keymap.set("n", "i", function() + M.focus_input() + end, opts) + + vim.keymap.set("n", "J", function() + M.focus_input() + end, opts) + + -- Window navigation + vim.keymap.set("n", "", function() + vim.cmd("wincmd h") + end, opts) + + vim.keymap.set("n", "", function() + vim.cmd("wincmd j") + end, opts) + + vim.keymap.set("n", "", function() + vim.cmd("wincmd k") + end, opts) + + vim.keymap.set("n", "", function() + vim.cmd("wincmd l") + end, opts) + +end + +--- Calculate window dimensions (always 1/4 of screen) +---@return table Dimensions +local function calculate_dimensions() + -- Always use 1/4 of the screen width + local width = math.floor(vim.o.columns * 0.25) + + return { + width = math.max(width, 30), -- Minimum 30 columns + total_height = vim.o.lines - 4, + output_height = vim.o.lines - 14, + input_height = 8, + } +end + +--- Autocmd group for maintaining width +local ask_augroup = nil + +--- Setup autocmd to always maintain 1/4 window width +local function setup_width_autocmd() + -- Clear previous autocmd group if exists + if ask_augroup then + pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) + end + + ask_augroup = vim.api.nvim_create_augroup("CodetypeAskWidth", { clear = true }) + + -- Always maintain 1/4 width on any window event + vim.api.nvim_create_autocmd({ "WinResized", "WinNew", "WinClosed", "VimResized" }, { + group = ask_augroup, + callback = function() + if not state.is_open or not state.output_win then return end + if not vim.api.nvim_win_is_valid(state.output_win) then return end + + vim.schedule(function() + if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then + -- Always calculate 1/4 of current screen width + local target_width = math.max(math.floor(vim.o.columns * 0.25), 30) + state.target_width = target_width + + local current_width = vim.api.nvim_win_get_width(state.output_win) + if current_width ~= target_width then + pcall(vim.api.nvim_win_set_width, state.output_win, target_width) + end + end + end) + end, + desc = "Maintain Ask panel at 1/4 window width", + }) +end + +--- Open the ask panel +function M.open() + -- Use the is_open() function which validates window state + if M.is_open() then + M.focus_input() + return + end + + local dims = calculate_dimensions() + + -- Store the target width + state.target_width = dims.width + + -- Create buffers if they don't exist + if not state.output_buf or not vim.api.nvim_buf_is_valid(state.output_buf) then + state.output_buf = create_output_buffer() + setup_output_keymaps(state.output_buf) + end + + if not state.input_buf or not vim.api.nvim_buf_is_valid(state.input_buf) then + state.input_buf = create_input_buffer() + setup_input_keymaps(state.input_buf) + end + + -- Save current window to return to it later + local current_win = vim.api.nvim_get_current_win() + + -- Create output window (top-left) + vim.cmd("topleft vsplit") + state.output_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(state.output_win, state.output_buf) + vim.api.nvim_win_set_width(state.output_win, dims.width) + + -- Window options for output + vim.wo[state.output_win].number = false + vim.wo[state.output_win].relativenumber = false + vim.wo[state.output_win].signcolumn = "no" + vim.wo[state.output_win].wrap = true + vim.wo[state.output_win].linebreak = true + vim.wo[state.output_win].cursorline = false + vim.wo[state.output_win].winfixwidth = true + + -- Create input window (bottom of the left panel) + vim.cmd("belowright split") + state.input_win = vim.api.nvim_get_current_win() + vim.api.nvim_win_set_buf(state.input_win, state.input_buf) + vim.api.nvim_win_set_height(state.input_win, dims.input_height) + + -- Window options for input + vim.wo[state.input_win].number = false + vim.wo[state.input_win].relativenumber = false + vim.wo[state.input_win].signcolumn = "no" + vim.wo[state.input_win].wrap = true + vim.wo[state.input_win].linebreak = true + vim.wo[state.input_win].winfixheight = true + vim.wo[state.input_win].winfixwidth = true + + state.is_open = true + + -- Setup autocmd to maintain width + setup_width_autocmd() + + -- Setup autocmd to close both windows when one is closed + local close_group = vim.api.nvim_create_augroup("CodetypeAskClose", { clear = true }) + + vim.api.nvim_create_autocmd("WinClosed", { + group = close_group, + callback = function(args) + local closed_win = tonumber(args.match) + -- Check if one of our windows was closed + if closed_win == state.output_win or closed_win == state.input_win then + -- Defer to avoid issues during window close + vim.schedule(function() + -- Close the other window if it's still open + if closed_win == state.output_win then + if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then + pcall(vim.api.nvim_win_close, state.input_win, true) + end + elseif closed_win == state.input_win then + if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then + pcall(vim.api.nvim_win_close, state.output_win, true) + end + end + + -- Reset state + state.input_win = nil + state.output_win = nil + state.is_open = false + state.target_width = nil + + -- Clean up autocmd groups + pcall(vim.api.nvim_del_augroup_by_id, close_group) + if ask_augroup then + pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) + ask_augroup = nil + end + end) + end + end, + desc = "Close both Ask windows together", + }) + + -- Focus the input window and start insert mode + vim.api.nvim_set_current_win(state.input_win) + vim.cmd("startinsert") +end + +--- Show file picker for @ mentions +function M.show_file_picker() + -- Check if telescope is available + local has_telescope, telescope = pcall(require, "telescope.builtin") + + if has_telescope then + telescope.find_files({ + prompt_title = "Select file to reference (@)", + attach_mappings = function(prompt_bufnr, map) + local actions = require("telescope.actions") + local action_state = require("telescope.actions.state") + + actions.select_default:replace(function() + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + if selection then + local filepath = selection.path or selection[1] + local filename = vim.fn.fnamemodify(filepath, ":t") + M.add_file_reference(filepath, filename) + end + end) + return true + end, + }) + else + -- Fallback: simple input + vim.ui.input({ prompt = "Enter file path: " }, function(input) + if input and input ~= "" then + local filepath = vim.fn.fnamemodify(input, ":p") + local filename = vim.fn.fnamemodify(filepath, ":t") + M.add_file_reference(filepath, filename) + end + end) + end +end + +--- Add a file reference to the input +---@param filepath string Full path to the file +---@param filename string Display name +function M.add_file_reference(filepath, filename) + -- Normalize filepath + filepath = vim.fn.fnamemodify(filepath, ":p") + + -- Store the reference with full path + state.referenced_files[filename] = filepath + + -- Read and validate file exists + local content = utils.read_file(filepath) + if not content then + utils.notify("Warning: Could not read file: " .. filename, vim.log.levels.WARN) + end + + -- Add to input buffer + if state.input_buf and vim.api.nvim_buf_is_valid(state.input_buf) then + local lines = vim.api.nvim_buf_get_lines(state.input_buf, 0, -1, false) + local text = table.concat(lines, "\n") + + -- Clear placeholder if present + if text:match("Type your question here") then + text = "" + end + + -- Add file reference (with single @) + local reference = "[📎 " .. filename .. "] " + text = text .. reference + + vim.api.nvim_buf_set_lines(state.input_buf, 0, -1, false, vim.split(text, "\n")) + + -- Move cursor to end + if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then + vim.api.nvim_set_current_win(state.input_win) + local line_count = vim.api.nvim_buf_line_count(state.input_buf) + local last_line = vim.api.nvim_buf_get_lines(state.input_buf, line_count - 1, line_count, false)[1] or "" + vim.api.nvim_win_set_cursor(state.input_win, { line_count, #last_line }) + vim.cmd("startinsert!") + end + end + + utils.notify("Added file: " .. filename .. " (" .. (content and #content or 0) .. " bytes)") +end + +--- Close the ask panel +function M.close() + -- Remove the width maintenance autocmd first + if ask_augroup then + pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) + ask_augroup = nil + end + + -- Find a window to focus after closing (not the ask windows) + local target_win = nil + for _, win in ipairs(vim.api.nvim_list_wins()) do + local buf = vim.api.nvim_win_get_buf(win) + if win ~= state.input_win and win ~= state.output_win then + local buftype = vim.bo[buf].buftype + if buftype == "" or buftype == "acwrite" then + target_win = win + break + end + end + end + + -- Close input window + if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then + pcall(vim.api.nvim_win_close, state.input_win, true) + end + + -- Close output window + if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then + pcall(vim.api.nvim_win_close, state.output_win, true) + end + + -- Reset state + state.input_win = nil + state.output_win = nil + state.is_open = false + state.target_width = nil + + -- Focus the target window if found, otherwise focus first available + if target_win and vim.api.nvim_win_is_valid(target_win) then + pcall(vim.api.nvim_set_current_win, target_win) + else + -- If no valid window, make sure we're not left with empty state + local wins = vim.api.nvim_list_wins() + if #wins > 0 then + pcall(vim.api.nvim_set_current_win, wins[1]) + end + end +end + +--- Toggle the ask panel +function M.toggle() + if M.is_open() then + M.close() + else + M.open() + end +end + +--- Focus the input window +function M.focus_input() + if state.input_win and vim.api.nvim_win_is_valid(state.input_win) then + vim.api.nvim_set_current_win(state.input_win) + vim.cmd("startinsert") + end +end + +--- Focus the output window +function M.focus_output() + if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then + vim.api.nvim_set_current_win(state.output_win) + end +end + +--- Get input text +---@return string Input text +local function get_input_text() + if not state.input_buf or not vim.api.nvim_buf_is_valid(state.input_buf) then + return "" + end + + local lines = vim.api.nvim_buf_get_lines(state.input_buf, 0, -1, false) + local content = table.concat(lines, "\n") + + -- Ignore placeholder + if content:match("Type your question here") then + return "" + end + + return content +end + +--- Clear input buffer +function M.clear_input() + if state.input_buf and vim.api.nvim_buf_is_valid(state.input_buf) then + vim.api.nvim_buf_set_lines(state.input_buf, 0, -1, false, { "" }) + end + state.referenced_files = {} +end + +--- Append text to output buffer +---@param text string Text to append +---@param is_user boolean Whether this is user message +local function append_to_output(text, is_user) + if not state.output_buf or not vim.api.nvim_buf_is_valid(state.output_buf) then + return + end + + vim.bo[state.output_buf].modifiable = true + + local lines = vim.api.nvim_buf_get_lines(state.output_buf, 0, -1, false) + + local timestamp = os.date("%H:%M") + local header = is_user + and "┌─ 👤 You [" .. timestamp .. "] ────────" + or "┌─ 🤖 AI [" .. timestamp .. "] ──────────" + + local new_lines = { "", header, "│" } + + -- Add text lines with border + for _, line in ipairs(vim.split(text, "\n")) do + table.insert(new_lines, "│ " .. line) + end + + table.insert(new_lines, "└─────────────────────────────────") + + for _, line in ipairs(new_lines) do + table.insert(lines, line) + end + + vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, lines) + vim.bo[state.output_buf].modifiable = false + + -- Scroll to bottom + if state.output_win and vim.api.nvim_win_is_valid(state.output_win) then + local line_count = vim.api.nvim_buf_line_count(state.output_buf) + vim.api.nvim_win_set_cursor(state.output_win, { line_count, 0 }) + end +end + +--- Build context from referenced files +---@return string Context string, number File count +local function build_file_context() + local context = "" + local file_count = 0 + + for filename, filepath in pairs(state.referenced_files) do + local content = utils.read_file(filepath) + if content and content ~= "" then + -- Detect language from extension + local ext = vim.fn.fnamemodify(filepath, ":e") + local lang = ext or "text" + + context = context .. "\n\n=== FILE: " .. filename .. " ===\n" + context = context .. "Path: " .. filepath .. "\n" + context = context .. "```" .. lang .. "\n" .. content .. "\n```\n" + file_count = file_count + 1 + end + end + + return context, file_count +end + +--- Build context for the question +---@return table Context object +local function build_context() + local context = { + project_root = utils.get_project_root(), + current_file = nil, + current_content = nil, + language = nil, + referenced_files = state.referenced_files, + } + + -- Try to get current file context from the non-ask window + local wins = vim.api.nvim_list_wins() + for _, win in ipairs(wins) do + if win ~= state.input_win and win ~= state.output_win then + local buf = vim.api.nvim_win_get_buf(win) + local filepath = vim.api.nvim_buf_get_name(buf) + + if filepath and filepath ~= "" and not filepath:match("%.coder%.") then + context.current_file = filepath + context.current_content = table.concat(vim.api.nvim_buf_get_lines(buf, 0, -1, false), "\n") + context.language = vim.bo[buf].filetype + break + end + end + end + + return context +end + +--- Submit the question to LLM +function M.submit() + local question = get_input_text() + + if not question or question:match("^%s*$") then + utils.notify("Please enter a question", vim.log.levels.WARN) + M.focus_input() + return + end + + -- Build context BEFORE clearing input (to preserve file references) + local context = build_context() + local file_context, file_count = build_file_context() + + -- Build display message (without full file contents) + local display_question = question + if file_count > 0 then + display_question = question .. "\n📎 " .. file_count .. " file(s) attached" + end + + -- Add user message to output + append_to_output(display_question, true) + + -- Clear input and references AFTER building context + M.clear_input() + + -- Build system prompt for ask mode using prompts module + local prompts = require("codetyper.prompts") + local system_prompt = prompts.system.ask + + if context.current_file then + system_prompt = system_prompt .. "\n\nCurrent open file: " .. context.current_file + system_prompt = system_prompt .. "\nLanguage: " .. (context.language or "unknown") + end + + -- Add to history + table.insert(state.history, { role = "user", content = question }) + + -- Show loading indicator + append_to_output("⏳ Thinking...", false) + + -- Get LLM client and generate response + local ok, llm = pcall(require, "codetyper.llm") + if not ok then + append_to_output("❌ Error: LLM module not loaded", false) + return + end + + local client = llm.get_client() + + -- Build full prompt WITH file contents + local full_prompt = question + if file_context ~= "" then + full_prompt = "USER QUESTION: " .. question .. "\n\n" .. + "ATTACHED FILE CONTENTS (please analyze these):" .. file_context + end + + -- Also add current file if no files were explicitly attached + if file_count == 0 and context.current_content and context.current_content ~= "" then + full_prompt = "USER QUESTION: " .. question .. "\n\n" .. + "CURRENT FILE (" .. (context.current_file or "unknown") .. "):\n```\n" .. + context.current_content .. "\n```" + end + + local request_context = { + file_content = file_context ~= "" and file_context or context.current_content, + language = context.language, + prompt_type = "explain", + file_path = context.current_file, + } + + client.generate(full_prompt, request_context, function(response, err) + -- Remove loading indicator + if state.output_buf and vim.api.nvim_buf_is_valid(state.output_buf) then + vim.bo[state.output_buf].modifiable = true + local lines = vim.api.nvim_buf_get_lines(state.output_buf, 0, -1, false) + -- Remove last few lines (the thinking message) + local to_remove = 0 + for i = #lines, 1, -1 do + if lines[i]:match("Thinking") or lines[i]:match("^[│└┌─]") then + to_remove = to_remove + 1 + if lines[i]:match("┌") then + break + end + else + break + end + end + for _ = 1, math.min(to_remove, 5) do + table.remove(lines) + end + vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, lines) + vim.bo[state.output_buf].modifiable = false + end + + if err then + append_to_output("❌ Error: " .. err, false) + return + end + + if response then + -- Add to history + table.insert(state.history, { role = "assistant", content = response }) + -- Display response + append_to_output(response, false) + else + append_to_output("❌ No response received", false) + end + + -- Focus back to input + M.focus_input() + end) +end + +--- Clear chat history +function M.clear_history() + state.history = {} + state.referenced_files = {} + + if state.output_buf and vim.api.nvim_buf_is_valid(state.output_buf) then + local header = { + "╔═══════════════════════════════════╗", + "║ 🤖 CODETYPER ASK ║", + "╠═══════════════════════════════════╣", + "║ Ask about code or concepts ║", + "║ ║", + "║ 💡 Keymaps: ║", + "║ @ → attach file ║", + "║ C-Enter → send ║", + "║ C-n → new chat ║", + "║ C-f → add current file ║", + "║ C-h/j/k/l → navigate ║", + "║ q → close │ K/J → jump ║", + "╚═══════════════════════════════════╝", + "", + } + vim.bo[state.output_buf].modifiable = true + vim.api.nvim_buf_set_lines(state.output_buf, 0, -1, false, header) + vim.bo[state.output_buf].modifiable = false + end + + utils.notify("Chat history cleared") +end + +--- Start a new chat (clears history and input) +function M.new_chat() + -- Clear the input + M.clear_input() + -- Clear the history + M.clear_history() + -- Focus the input + M.focus_input() + utils.notify("Started new chat") +end + +--- Include current file context in input +function M.include_file_context() + local context = build_context() + + if not context.current_file then + utils.notify("No file context available", vim.log.levels.WARN) + return + end + + local filename = vim.fn.fnamemodify(context.current_file, ":t") + M.add_file_reference(context.current_file, filename) +end + +--- Copy last assistant response to clipboard +function M.copy_last_response() + for i = #state.history, 1, -1 do + if state.history[i].role == "assistant" then + vim.fn.setreg("+", state.history[i].content) + utils.notify("Response copied to clipboard") + return + end + end + utils.notify("No response to copy", vim.log.levels.WARN) +end +--- Check if ask panel is open (validates window state) +---@return boolean +function M.is_open() + -- Verify windows are actually valid, not just the flag + if state.is_open then + local output_valid = state.output_win and vim.api.nvim_win_is_valid(state.output_win) + local input_valid = state.input_win and vim.api.nvim_win_is_valid(state.input_win) + + -- If either window is invalid, reset the state + if not output_valid or not input_valid then + state.is_open = false + state.output_win = nil + state.input_win = nil + state.target_width = nil + -- Clean up autocmd + if ask_augroup then + pcall(vim.api.nvim_del_augroup_by_id, ask_augroup) + ask_augroup = nil + end + end + end + + return state.is_open +end + +--- Get chat history +---@return table History +function M.get_history() + return state.history +end + +return M diff --git a/lua/codetyper/autocmds.lua b/lua/codetyper/autocmds.lua new file mode 100644 index 0000000..ff172f3 --- /dev/null +++ b/lua/codetyper/autocmds.lua @@ -0,0 +1,349 @@ +---@mod codetyper.autocmds Autocommands for Codetyper.nvim + +local M = {} + +local utils = require("codetyper.utils") + +--- Autocommand group name +local AUGROUP = "Codetyper" + +--- Debounce timer for tree updates +local tree_update_timer = nil +local TREE_UPDATE_DEBOUNCE_MS = 1000 -- 1 second debounce + +--- Track processed prompts to avoid re-processing +---@type table +local processed_prompts = {} + +--- 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 + +--- 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.tree") + tree.update_tree_log() + tree_update_timer = nil + end, TREE_UPDATE_DEBOUNCE_MS) +end + +--- Setup autocommands +function M.setup() + local group = vim.api.nvim_create_augroup(AUGROUP, { clear = true }) + + -- Auto-save coder file when leaving insert mode + vim.api.nvim_create_autocmd("InsertLeave", { + group = group, + pattern = "*.coder.*", + callback = function() + -- Auto-save the coder file + if vim.bo.modified then + vim.cmd("silent! write") + end + -- Check for closed prompts and auto-process + M.check_for_closed_prompt() + end, + desc = "Auto-save and check for closed prompt tags", + }) + + -- Auto-set filetype for coder files based on extension + vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile" }, { + group = group, + pattern = "*.coder.*", + callback = function() + M.set_coder_filetype() + end, + desc = "Set filetype for coder files", + }) + + -- Auto-open split view when opening a coder file directly (e.g., from nvim-tree) + vim.api.nvim_create_autocmd("BufEnter", { + group = group, + pattern = "*.coder.*", + callback = function() + -- Delay slightly to ensure buffer is fully loaded + vim.defer_fn(function() + M.auto_open_target_file() + end, 50) + end, + desc = "Auto-open target file when coder file is opened", + }) + + -- Cleanup on buffer close + vim.api.nvim_create_autocmd("BufWipeout", { + group = group, + pattern = "*.coder.*", + callback = function(ev) + local window = require("codetyper.window") + if window.is_open() then + window.close_split() + end + -- Clear processed prompts for this buffer + 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 tracking + M.clear_auto_opened(bufnr) + end, + desc = "Cleanup on coder buffer close", + }) + + -- Update tree.log when files are created/written + vim.api.nvim_create_autocmd({ "BufWritePost", "BufNewFile" }, { + group = group, + pattern = "*", + callback = function(ev) + -- Skip coder files and tree.log itself + local filepath = ev.file or vim.fn.expand("%:p") + if filepath:match("%.coder%.") or filepath:match("tree%.log$") then + return + end + -- Schedule tree update with debounce + schedule_tree_update() + end, + desc = "Update tree.log on file creation/save", + }) + + -- Update tree.log when files are deleted (via netrw or file explorer) + vim.api.nvim_create_autocmd("BufDelete", { + group = group, + pattern = "*", + callback = function(ev) + local filepath = ev.file or "" + -- Skip special buffers and coder files + if filepath == "" or filepath:match("%.coder%.") or filepath:match("tree%.log$") then + return + end + schedule_tree_update() + end, + desc = "Update tree.log on file deletion", + }) + + -- Update tree on directory change + vim.api.nvim_create_autocmd("DirChanged", { + group = group, + pattern = "*", + callback = function() + schedule_tree_update() + end, + desc = "Update tree.log on directory change", + }) +end + +--- Check if the buffer has a newly closed prompt and auto-process +function M.check_for_closed_prompt() + local codetyper = require("codetyper") + local config = codetyper.get_config() + local parser = require("codetyper.parser") + + local bufnr = vim.api.nvim_get_current_buf() + + -- Get current line + 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 + return + end + + local current_line = lines[1] + + -- Check if line contains closing tag + if parser.has_closing_tag(current_line, config.patterns.close_tag) then + -- Find the complete prompt + local prompt = parser.get_last_prompt(bufnr) + if prompt and prompt.content and prompt.content ~= "" then + -- Generate unique key for this prompt + local prompt_key = get_prompt_key(bufnr, prompt) + + -- Check if already processed + if processed_prompts[prompt_key] then + return + end + + -- Mark as processed + processed_prompts[prompt_key] = true + + -- Auto-process the prompt (no confirmation needed) + utils.notify("Processing prompt...", vim.log.levels.INFO) + vim.schedule(function() + vim.cmd("CoderProcess") + end) + end + end +end + +--- Reset processed prompts for a buffer (useful for re-processing) +---@param bufnr? number Buffer number (default: current) +function M.reset_processed(bufnr) + 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 + utils.notify("Prompt history cleared - prompts can be re-processed") +end + +--- Track if we already opened the split for this buffer +---@type table +local auto_opened_buffers = {} + +--- Auto-open target file when a coder file is opened directly +function M.auto_open_target_file() + local window = require("codetyper.window") + + -- Skip if split is already open + if window.is_open() then + return + end + + local bufnr = vim.api.nvim_get_current_buf() + + -- Skip if we already handled this buffer + if auto_opened_buffers[bufnr] then + return + end + + local current_file = vim.fn.expand("%:p") + + -- Skip empty paths + if not current_file or current_file == "" then + return + end + + -- Verify it's a coder file + if not utils.is_coder_file(current_file) then + return + end + + -- Skip if we're in a special buffer (nvim-tree, etc.) + local buftype = vim.bo[bufnr].buftype + if buftype ~= "" then + return + end + + -- Mark as handled + auto_opened_buffers[bufnr] = true + + -- Get the target file path + local target_path = utils.get_target_path(current_file) + + -- Check if target file exists + if not utils.file_exists(target_path) then + utils.notify("Target file not found: " .. vim.fn.fnamemodify(target_path, ":t"), vim.log.levels.WARN) + return + end + + -- Get config with fallback defaults + local codetyper = require("codetyper") + local config = codetyper.get_config() + + -- Fallback width if config not fully loaded + local width = (config and config.window and config.window.width) or 0.4 + if width <= 1 then + width = math.floor(vim.o.columns * width) + end + + -- Store current coder window + local coder_win = vim.api.nvim_get_current_win() + local coder_buf = bufnr + + -- Open target file in a vertical split on the right + local ok, err = pcall(function() + vim.cmd("vsplit " .. vim.fn.fnameescape(target_path)) + end) + + if not ok then + utils.notify("Failed to open target file: " .. tostring(err), vim.log.levels.ERROR) + auto_opened_buffers[bufnr] = nil -- Allow retry + return + end + + -- Now we're in the target window (right side) + local target_win = vim.api.nvim_get_current_win() + local target_buf = vim.api.nvim_get_current_buf() + + -- Set the coder window width (left side) + pcall(vim.api.nvim_win_set_width, coder_win, width) + + -- Update window module state + window._coder_win = coder_win + window._coder_buf = coder_buf + window._target_win = target_win + window._target_buf = target_buf + + -- Set up window options for coder window + pcall(function() + vim.wo[coder_win].number = true + vim.wo[coder_win].relativenumber = true + vim.wo[coder_win].signcolumn = "yes" + end) + + utils.notify("Opened target: " .. vim.fn.fnamemodify(target_path, ":t")) +end + +--- Clear auto-opened tracking for a buffer +---@param bufnr number Buffer number +function M.clear_auto_opened(bufnr) + auto_opened_buffers[bufnr] = nil +end + +--- Set appropriate filetype for coder files +function M.set_coder_filetype() + local filepath = vim.fn.expand("%:p") + + -- Extract the actual extension (e.g., index.coder.ts -> ts) + local ext = filepath:match("%.coder%.(%w+)$") + + if ext then + -- Map extension to filetype + 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 + +--- Clear all autocommands +function M.clear() + vim.api.nvim_del_augroup_by_name(AUGROUP) +end + +return M diff --git a/lua/codetyper/commands.lua b/lua/codetyper/commands.lua new file mode 100644 index 0000000..0984e6c --- /dev/null +++ b/lua/codetyper/commands.lua @@ -0,0 +1,330 @@ +---@mod codetyper.commands Command definitions for Codetyper.nvim + +local M = {} + +local utils = require("codetyper.utils") +local window = require("codetyper.window") + +--- Open coder view for current file or select one +---@param opts? table Command options +local function cmd_open(opts) + opts = opts or {} + + local current_file = vim.fn.expand("%:p") + + -- If no file is open, prompt user to select one + if current_file == "" or vim.bo.buftype ~= "" then + -- Use telescope or vim.ui.select to pick a file + if pcall(require, "telescope") then + require("telescope.builtin").find_files({ + prompt_title = "Select file for Coder", + attach_mappings = function(prompt_bufnr, map) + local actions = require("telescope.actions") + local action_state = require("telescope.actions.state") + + actions.select_default:replace(function() + actions.close(prompt_bufnr) + local selection = action_state.get_selected_entry() + if selection then + local target_path = selection.path or selection[1] + local coder_path = utils.get_coder_path(target_path) + window.open_split(target_path, coder_path) + end + end) + return true + end, + }) + else + -- Fallback to input prompt + vim.ui.input({ prompt = "Enter file path: " }, function(input) + if input and input ~= "" then + local target_path = vim.fn.fnamemodify(input, ":p") + local coder_path = utils.get_coder_path(target_path) + window.open_split(target_path, coder_path) + end + end) + end + return + end + + local target_path, coder_path + + -- Check if current file is a coder file + if utils.is_coder_file(current_file) then + coder_path = current_file + target_path = utils.get_target_path(current_file) + else + target_path = current_file + coder_path = utils.get_coder_path(current_file) + end + + window.open_split(target_path, coder_path) +end + +--- Close coder view +local function cmd_close() + window.close_split() +end + +--- Toggle coder view +local function cmd_toggle() + local current_file = vim.fn.expand("%:p") + + if current_file == "" then + utils.notify("No file in current buffer", vim.log.levels.WARN) + return + end + + local target_path, coder_path + + if utils.is_coder_file(current_file) then + coder_path = current_file + target_path = utils.get_target_path(current_file) + else + target_path = current_file + coder_path = utils.get_coder_path(current_file) + end + + window.toggle_split(target_path, coder_path) +end + +--- Process prompt at cursor and generate code +local function cmd_process() + local parser = require("codetyper.parser") + local llm = require("codetyper.llm") + + local bufnr = vim.api.nvim_get_current_buf() + local current_file = vim.fn.expand("%:p") + + if not utils.is_coder_file(current_file) then + utils.notify("Not a coder file. Use *.coder.* files", vim.log.levels.WARN) + return + end + + local prompt = parser.get_last_prompt(bufnr) + if not prompt then + utils.notify("No prompt found. Use /@ your prompt @/", vim.log.levels.WARN) + return + end + + local target_path = utils.get_target_path(current_file) + local prompt_type = parser.detect_prompt_type(prompt.content) + local context = llm.build_context(target_path, prompt_type) + local clean_prompt = parser.clean_prompt(prompt.content) + + llm.generate(clean_prompt, context, function(response, err) + if err then + utils.notify("Generation failed: " .. err, vim.log.levels.ERROR) + return + end + + if response then + -- Inject code into target file + local inject = require("codetyper.inject") + inject.inject_code(target_path, response, prompt_type) + end + end) +end + +--- Show plugin status +local function cmd_status() + local codetyper = require("codetyper") + local config = codetyper.get_config() + local tree = require("codetyper.tree") + + local stats = tree.get_stats() + + local status = { + "Codetyper.nvim Status", + "====================", + "", + "Provider: " .. config.llm.provider, + } + + if config.llm.provider == "claude" then + local has_key = (config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY) ~= nil + table.insert(status, "Claude API Key: " .. (has_key and "configured" or "NOT SET")) + table.insert(status, "Claude Model: " .. config.llm.claude.model) + else + table.insert(status, "Ollama Host: " .. config.llm.ollama.host) + table.insert(status, "Ollama Model: " .. config.llm.ollama.model) + end + + table.insert(status, "") + table.insert(status, "Window Position: " .. config.window.position) + table.insert(status, "Window Width: " .. tostring(config.window.width * 100) .. "%") + table.insert(status, "") + table.insert(status, "View Open: " .. (window.is_open() and "yes" or "no")) + table.insert(status, "") + table.insert(status, "Project Stats:") + table.insert(status, " Files: " .. stats.files) + table.insert(status, " Directories: " .. stats.directories) + table.insert(status, " Tree Log: " .. (tree.get_tree_log_path() or "N/A")) + + utils.notify(table.concat(status, "\n")) +end + +--- Refresh tree.log manually +local function cmd_tree() + local tree = require("codetyper.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.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.autocmds") + autocmds.reset_processed() +end + +--- Force update gitignore +local function cmd_gitignore() + local gitignore = require("codetyper.gitignore") + gitignore.force_update() +end + +--- Open ask panel +local function cmd_ask() + local ask = require("codetyper.ask") + ask.open() +end + +--- Close ask panel +local function cmd_ask_close() + local ask = require("codetyper.ask") + ask.close() +end + +--- Toggle ask panel +local function cmd_ask_toggle() + local ask = require("codetyper.ask") + ask.toggle() +end + +--- Clear ask history +local function cmd_ask_clear() + local ask = require("codetyper.ask") + ask.clear_history() +end + +--- Switch focus between coder and target windows +local function cmd_focus() + if not window.is_open() then + utils.notify("Coder view not open", vim.log.levels.WARN) + return + end + + local current_win = vim.api.nvim_get_current_win() + if current_win == window.get_coder_win() then + window.focus_target() + else + window.focus_coder() + end +end + +--- Main command dispatcher +---@param args table Command arguments +local function coder_cmd(args) + local subcommand = args.fargs[1] or "toggle" + + local commands = { + open = cmd_open, + close = cmd_close, + toggle = cmd_toggle, + process = cmd_process, + status = cmd_status, + focus = cmd_focus, + tree = cmd_tree, + ["tree-view"] = cmd_tree_view, + reset = cmd_reset, + ask = cmd_ask, + ["ask-close"] = cmd_ask_close, + ["ask-toggle"] = cmd_ask_toggle, + ["ask-clear"] = cmd_ask_clear, + gitignore = cmd_gitignore, + } + + 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 { + "open", "close", "toggle", "process", "status", "focus", + "tree", "tree-view", "reset", "gitignore", + "ask", "ask-close", "ask-toggle", "ask-clear", + } + end, + desc = "Codetyper.nvim commands", + }) + + -- Convenience aliases + vim.api.nvim_create_user_command("CoderOpen", function() + cmd_open() + end, { desc = "Open Coder view" }) + + vim.api.nvim_create_user_command("CoderClose", function() + cmd_close() + end, { desc = "Close Coder view" }) + + vim.api.nvim_create_user_command("CoderToggle", function() + cmd_toggle() + end, { desc = "Toggle Coder view" }) + + vim.api.nvim_create_user_command("CoderProcess", function() + cmd_process() + end, { desc = "Process prompt and generate code" }) + + 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" }) + + -- Ask panel commands + vim.api.nvim_create_user_command("CoderAsk", function() + cmd_ask() + end, { desc = "Open Ask panel" }) + + vim.api.nvim_create_user_command("CoderAskToggle", function() + cmd_ask_toggle() + end, { desc = "Toggle Ask panel" }) + + vim.api.nvim_create_user_command("CoderAskClear", function() + cmd_ask_clear() + end, { desc = "Clear Ask history" }) +end + +return M diff --git a/lua/codetyper/config.lua b/lua/codetyper/config.lua new file mode 100644 index 0000000..dfa1e7e --- /dev/null +++ b/lua/codetyper/config.lua @@ -0,0 +1,84 @@ +---@mod codetyper.config Configuration module for Codetyper.nvim + +local M = {} + +---@type CoderConfig +local defaults = { + llm = { + provider = "claude", + claude = { + api_key = nil, -- Will use ANTHROPIC_API_KEY env var if nil + model = "claude-sonnet-4-20250514", + }, + ollama = { + host = "http://localhost:11434", + model = "codellama", + }, + }, + window = { + width = 0.25, -- 25% of screen width (1/4) + position = "left", + border = "rounded", + }, + patterns = { + open_tag = "/@", + close_tag = "@/", + file_pattern = "*.coder.*", + }, + auto_gitignore = true, + auto_open_ask = true, -- Auto-open Ask panel on startup +} + +--- Deep merge two tables +---@param t1 table Base table +---@param t2 table Table to merge into base +---@return table Merged table +local function deep_merge(t1, t2) + local result = vim.deepcopy(t1) + for k, v in pairs(t2) do + if type(v) == "table" and type(result[k]) == "table" then + result[k] = deep_merge(result[k], v) + else + result[k] = v + end + end + return result +end + +--- Setup configuration with user options +---@param opts? CoderConfig User configuration options +---@return CoderConfig Final configuration +function M.setup(opts) + opts = opts or {} + return deep_merge(defaults, opts) +end + +--- Get default configuration +---@return CoderConfig Default configuration +function M.get_defaults() + return vim.deepcopy(defaults) +end + +--- Validate configuration +---@param config CoderConfig Configuration to validate +---@return boolean, string? Valid status and optional error message +function M.validate(config) + if not config.llm then + return false, "Missing LLM configuration" + end + + if config.llm.provider ~= "claude" and config.llm.provider ~= "ollama" then + return false, "Invalid LLM provider. Must be 'claude' or 'ollama'" + end + + if config.llm.provider == "claude" then + local api_key = config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY + if not api_key or api_key == "" then + return false, "Claude API key not configured. Set llm.claude.api_key or ANTHROPIC_API_KEY env var" + end + end + + return true +end + +return M diff --git a/lua/codetyper/gitignore.lua b/lua/codetyper/gitignore.lua new file mode 100644 index 0000000..f2db666 --- /dev/null +++ b/lua/codetyper/gitignore.lua @@ -0,0 +1,190 @@ +---@mod codetyper.gitignore Gitignore management for Codetyper.nvim + +local M = {} + +local utils = require("codetyper.utils") + +--- Patterns to add to .gitignore +local IGNORE_PATTERNS = { + "*.coder.*", + ".coder/", +} + +--- Comment to identify codetyper entries +local CODER_COMMENT = "# Codetyper.nvim - AI coding partner files" + +--- Check if pattern exists in gitignore content +---@param content string Gitignore content +---@param pattern string Pattern to check +---@return boolean +local function pattern_exists(content, pattern) + local escaped = utils.escape_pattern(pattern) + return content:match("\n" .. escaped .. "\n") ~= nil + or content:match("^" .. escaped .. "\n") ~= nil + or content:match("\n" .. escaped .. "$") ~= nil + or content == pattern +end + +--- Check if all patterns exist in gitignore content +---@param content string Gitignore content +---@return boolean, string[] All exist status and list of missing patterns +local function all_patterns_exist(content) + local missing = {} + for _, pattern in ipairs(IGNORE_PATTERNS) do + if not pattern_exists(content, pattern) then + table.insert(missing, pattern) + end + end + return #missing == 0, missing +end + +--- Get the path to .gitignore in project root +---@return string|nil Path to .gitignore or nil +function M.get_gitignore_path() + local root = utils.get_project_root() + if not root then + return nil + end + return root .. "/.gitignore" +end + +--- Check if coder files are already ignored +---@return boolean +function M.is_ignored() + local gitignore_path = M.get_gitignore_path() + if not gitignore_path then + return false + end + + local content = utils.read_file(gitignore_path) + if not content then + return false + end + + local all_exist, _ = all_patterns_exist(content) + return all_exist +end + +--- Add coder patterns to .gitignore +---@return boolean Success status +function M.add_to_gitignore() + local gitignore_path = M.get_gitignore_path() + if not gitignore_path then + utils.notify("Could not determine project root", vim.log.levels.WARN) + return false + end + + local content = utils.read_file(gitignore_path) + local patterns_to_add = {} + + if content then + -- File exists, check which patterns are missing + local _, missing = all_patterns_exist(content) + if #missing == 0 then + return true -- All already ignored + end + patterns_to_add = missing + else + -- Create new .gitignore with all patterns + content = "" + patterns_to_add = IGNORE_PATTERNS + end + + -- Build the patterns string + local patterns_str = table.concat(patterns_to_add, "\n") + + if content == "" then + -- New file + content = CODER_COMMENT .. "\n" .. patterns_str .. "\n" + else + -- Append to existing + local newline = content:sub(-1) == "\n" and "" or "\n" + -- Check if comment already exists + if not content:match(utils.escape_pattern(CODER_COMMENT)) then + content = content .. newline .. "\n" .. CODER_COMMENT .. "\n" .. patterns_str .. "\n" + else + content = content .. newline .. patterns_str .. "\n" + end + end + + if utils.write_file(gitignore_path, content) then + utils.notify("Added coder patterns to .gitignore") + return true + else + utils.notify("Failed to update .gitignore", vim.log.levels.ERROR) + return false + end +end + +--- Ensure coder files are in .gitignore (called on setup) +---@param auto_gitignore? boolean Override auto_gitignore setting (default: true) +---@return boolean Success status +function M.ensure_ignored(auto_gitignore) + -- Default to true if not specified + if auto_gitignore == nil then + -- Try to get from config if available + local ok, codetyper = pcall(require, "codetyper") + if ok and codetyper.is_initialized and codetyper.is_initialized() then + local config = codetyper.get_config() + auto_gitignore = config and config.auto_gitignore + else + auto_gitignore = true -- Default to true + end + end + + if not auto_gitignore then + return true + end + + if M.is_ignored() then + return true + end + + return M.add_to_gitignore() +end + +--- Remove coder patterns from .gitignore +---@return boolean Success status +function M.remove_from_gitignore() + local gitignore_path = M.get_gitignore_path() + if not gitignore_path then + return false + end + + local content = utils.read_file(gitignore_path) + if not content then + return false + end + + -- Remove the comment and all patterns + content = content:gsub(CODER_COMMENT .. "\n", "") + for _, pattern in ipairs(IGNORE_PATTERNS) do + content = content:gsub(utils.escape_pattern(pattern) .. "\n?", "") + end + + -- Clean up extra newlines + content = content:gsub("\n\n\n+", "\n\n") + + return utils.write_file(gitignore_path, content) +end + +--- Get list of patterns being ignored +---@return string[] List of patterns +function M.get_ignore_patterns() + return vim.deepcopy(IGNORE_PATTERNS) +end + +--- Force update gitignore (manual trigger) +---@return boolean Success status +function M.force_update() + local gitignore_path = M.get_gitignore_path() + if not gitignore_path then + utils.notify("Could not determine project root for .gitignore", vim.log.levels.WARN) + return false + end + + utils.notify("Updating .gitignore at: " .. gitignore_path) + return M.add_to_gitignore() +end + +return M diff --git a/lua/codetyper/health.lua b/lua/codetyper/health.lua new file mode 100644 index 0000000..213cb9f --- /dev/null +++ b/lua/codetyper/health.lua @@ -0,0 +1,91 @@ +---@mod codetyper.health Health check for Codetyper.nvim + +local M = {} + +local health = vim.health or require("health") + +--- Run health checks +function M.check() + health.start("Codetyper.nvim") + + -- Check Neovim version + if vim.fn.has("nvim-0.8.0") == 1 then + health.ok("Neovim version >= 0.8.0") + else + health.error("Neovim 0.8.0+ required") + end + + -- Check if plugin is initialized + local ok, codetyper = pcall(require, "codetyper") + if ok and codetyper.is_initialized() then + health.ok("Plugin initialized") + else + health.info("Plugin not yet initialized (call setup() first)") + end + + -- Check curl availability + if vim.fn.executable("curl") == 1 then + health.ok("curl is available") + else + health.error("curl is required for LLM API calls") + end + + -- Check LLM configuration + if ok and codetyper.is_initialized() then + local config = codetyper.get_config() + + health.info("LLM Provider: " .. config.llm.provider) + + if config.llm.provider == "claude" then + local api_key = config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY + if api_key and api_key ~= "" then + health.ok("Claude API key configured") + else + health.warn("Claude API key not set. Set ANTHROPIC_API_KEY or llm.claude.api_key") + end + health.info("Claude model: " .. config.llm.claude.model) + elseif config.llm.provider == "ollama" then + health.info("Ollama host: " .. config.llm.ollama.host) + health.info("Ollama model: " .. config.llm.ollama.model) + + -- Try to check Ollama connectivity + local ollama = require("codetyper.llm.ollama") + ollama.health_check(function(is_ok, err) + if is_ok then + vim.schedule(function() + health.ok("Ollama is reachable") + end) + else + vim.schedule(function() + health.warn("Cannot connect to Ollama: " .. (err or "unknown error")) + end) + end + end) + end + end + + -- Check optional dependencies + if pcall(require, "telescope") then + health.ok("telescope.nvim is available (enhanced file picker)") + else + health.info("telescope.nvim not found (using basic file picker)") + end + + -- Check .gitignore configuration + local utils = require("codetyper.utils") + local gitignore = require("codetyper.gitignore") + + local root = utils.get_project_root() + if root then + health.info("Project root: " .. root) + if gitignore.is_ignored() then + health.ok("Coder files are in .gitignore") + else + health.warn("Coder files not in .gitignore (will be added on setup)") + end + else + health.info("No project root detected") + end +end + +return M diff --git a/lua/codetyper/init.lua b/lua/codetyper/init.lua new file mode 100644 index 0000000..6e17cb1 --- /dev/null +++ b/lua/codetyper/init.lua @@ -0,0 +1,69 @@ +---@mod codetyper Codetyper.nvim - AI-powered coding partner +---@brief [[ +--- Codetyper.nvim is a Neovim plugin that acts as your coding partner. +--- It uses LLM APIs (Claude, Ollama) to help you write code faster +--- using special `.coder.*` files and inline prompt tags. +---@brief ]] + +local M = {} + +---@type CoderConfig +M.config = {} + +---@type boolean +M._initialized = false + +--- Setup the plugin with user configuration +---@param opts? CoderConfig User configuration options +function M.setup(opts) + if M._initialized then + return + end + + local config = require("codetyper.config") + M.config = config.setup(opts) + + -- Initialize modules + local commands = require("codetyper.commands") + local gitignore = require("codetyper.gitignore") + local autocmds = require("codetyper.autocmds") + local tree = require("codetyper.tree") + + -- Register commands + commands.setup() + + -- Setup autocommands + autocmds.setup() + + -- Ensure .gitignore has coder files excluded + gitignore.ensure_ignored() + + -- Initialize tree logging (creates .coder folder and initial tree.log) + tree.setup() + + M._initialized = true + + -- Auto-open Ask panel after a short delay (to let UI settle) + if M.config.auto_open_ask then + vim.defer_fn(function() + local ask = require("codetyper.ask") + if not ask.is_open() then + ask.open() + end + end, 300) + end +end + +--- Get current configuration +---@return CoderConfig +function M.get_config() + return M.config +end + +--- Check if plugin is initialized +---@return boolean +function M.is_initialized() + return M._initialized +end + +return M diff --git a/lua/codetyper/inject.lua b/lua/codetyper/inject.lua new file mode 100644 index 0000000..8ced0ed --- /dev/null +++ b/lua/codetyper/inject.lua @@ -0,0 +1,239 @@ +---@mod codetyper.inject Code injection for Codetyper.nvim + +local M = {} + +local utils = require("codetyper.utils") + +--- Inject generated code into target file +---@param target_path string Path to target file +---@param code string Generated code +---@param prompt_type string Type of prompt (refactor, add, document, etc.) +function M.inject_code(target_path, code, prompt_type) + local window = require("codetyper.window") + + -- Normalize the target path + target_path = vim.fn.fnamemodify(target_path, ":p") + + -- Get target buffer + local target_buf = window.get_target_buf() + + if not target_buf or not vim.api.nvim_buf_is_valid(target_buf) then + -- Try to find buffer by path + for _, buf in ipairs(vim.api.nvim_list_bufs()) do + local buf_name = vim.fn.fnamemodify(vim.api.nvim_buf_get_name(buf), ":p") + if buf_name == target_path then + target_buf = buf + break + end + end + end + + -- If still not found, open the file + if not target_buf or not vim.api.nvim_buf_is_valid(target_buf) then + -- Check if file exists + if utils.file_exists(target_path) then + vim.cmd("edit " .. vim.fn.fnameescape(target_path)) + target_buf = vim.api.nvim_get_current_buf() + utils.notify("Opened target file: " .. vim.fn.fnamemodify(target_path, ":t")) + else + utils.notify("Target file not found: " .. target_path, vim.log.levels.ERROR) + return + end + end + + if not target_buf then + utils.notify("Target buffer not found", vim.log.levels.ERROR) + return + end + + utils.notify("Injecting code into: " .. vim.fn.fnamemodify(target_path, ":t")) + + -- Different injection strategies based on prompt type + if prompt_type == "refactor" then + M.inject_refactor(target_buf, code) + elseif prompt_type == "add" then + M.inject_add(target_buf, code) + elseif prompt_type == "document" then + M.inject_document(target_buf, code) + else + -- For generic, auto-add instead of prompting + M.inject_add(target_buf, code) + end + + -- Mark buffer as modified and save + vim.bo[target_buf].modified = true + + -- Auto-save the target file + vim.schedule(function() + if vim.api.nvim_buf_is_valid(target_buf) then + local wins = vim.fn.win_findbuf(target_buf) + if #wins > 0 then + vim.api.nvim_win_call(wins[1], function() + vim.cmd("silent! write") + end) + end + end + end) +end + +--- Inject code for refactor (replace entire file) +---@param bufnr number Buffer number +---@param code string Generated code +function M.inject_refactor(bufnr, code) + local lines = vim.split(code, "\n", { plain = true }) + + -- Save cursor position + local cursor = nil + local wins = vim.fn.win_findbuf(bufnr) + if #wins > 0 then + cursor = vim.api.nvim_win_get_cursor(wins[1]) + end + + -- Replace buffer content + vim.api.nvim_buf_set_lines(bufnr, 0, -1, false, lines) + + -- Restore cursor position if possible + if cursor then + local line_count = vim.api.nvim_buf_line_count(bufnr) + cursor[1] = math.min(cursor[1], line_count) + pcall(vim.api.nvim_win_set_cursor, wins[1], cursor) + end + + utils.notify("Code refactored", vim.log.levels.INFO) +end + +--- Inject code for add (append at cursor or end) +---@param bufnr number Buffer number +---@param code string Generated code +function M.inject_add(bufnr, code) + local lines = vim.split(code, "\n", { plain = true }) + + -- Get cursor position in target window + local window = require("codetyper.window") + local target_win = window.get_target_win() + + local insert_line + if target_win and vim.api.nvim_win_is_valid(target_win) then + local cursor = vim.api.nvim_win_get_cursor(target_win) + insert_line = cursor[1] + else + -- Append at end + insert_line = vim.api.nvim_buf_line_count(bufnr) + end + + -- Insert lines at position + vim.api.nvim_buf_set_lines(bufnr, insert_line, insert_line, false, lines) + + utils.notify("Code added at line " .. (insert_line + 1), vim.log.levels.INFO) +end + +--- Inject documentation +---@param bufnr number Buffer number +---@param code string Generated documentation +function M.inject_document(bufnr, code) + -- Documentation typically goes above the current function/class + -- For simplicity, insert at cursor position + M.inject_add(bufnr, code) + utils.notify("Documentation added", vim.log.levels.INFO) +end + +--- Generic injection (prompt user for action) +---@param bufnr number Buffer number +---@param code string Generated code +function M.inject_generic(bufnr, code) + local actions = { + "Replace entire file", + "Insert at cursor", + "Append to end", + "Copy to clipboard", + "Cancel", + } + + vim.ui.select(actions, { + prompt = "How to inject the generated code?", + }, function(choice) + if not choice then + return + end + + if choice == "Replace entire file" then + M.inject_refactor(bufnr, code) + elseif choice == "Insert at cursor" then + M.inject_add(bufnr, code) + elseif choice == "Append to end" then + local lines = vim.split(code, "\n", { plain = true }) + local line_count = vim.api.nvim_buf_line_count(bufnr) + vim.api.nvim_buf_set_lines(bufnr, line_count, line_count, false, lines) + utils.notify("Code appended to end", vim.log.levels.INFO) + elseif choice == "Copy to clipboard" then + vim.fn.setreg("+", code) + utils.notify("Code copied to clipboard", vim.log.levels.INFO) + end + end) +end + +--- Preview code in a floating window before injection +---@param code string Generated code +---@param callback fun(action: string) Callback with selected action +function M.preview(code, callback) + local codetyper = require("codetyper") + local config = codetyper.get_config() + + local lines = vim.split(code, "\n", { plain = true }) + + -- Create buffer for preview + local buf = vim.api.nvim_create_buf(false, true) + vim.api.nvim_buf_set_lines(buf, 0, -1, false, lines) + + -- Calculate window size + local width = math.min(80, vim.o.columns - 10) + local height = math.min(#lines + 2, vim.o.lines - 10) + + -- Create floating window + local win = vim.api.nvim_open_win(buf, true, { + relative = "editor", + width = width, + height = height, + row = math.floor((vim.o.lines - height) / 2), + col = math.floor((vim.o.columns - width) / 2), + style = "minimal", + border = config.window.border, + title = " Generated Code Preview ", + title_pos = "center", + }) + + -- Set buffer options + vim.bo[buf].modifiable = false + vim.bo[buf].bufhidden = "wipe" + + -- Add keymaps for actions + local opts = { buffer = buf, noremap = true, silent = true } + + vim.keymap.set("n", "q", function() + vim.api.nvim_win_close(win, true) + callback("cancel") + end, opts) + + vim.keymap.set("n", "", function() + vim.api.nvim_win_close(win, true) + callback("inject") + end, opts) + + vim.keymap.set("n", "y", function() + vim.fn.setreg("+", code) + utils.notify("Copied to clipboard") + end, opts) + + -- Show help in command line + vim.api.nvim_echo({ + { "Press ", "Normal" }, + { "", "Keyword" }, + { " to inject, ", "Normal" }, + { "y", "Keyword" }, + { " to copy, ", "Normal" }, + { "q", "Keyword" }, + { " to cancel", "Normal" }, + }, false, {}) +end + +return M diff --git a/lua/codetyper/llm/claude.lua b/lua/codetyper/llm/claude.lua new file mode 100644 index 0000000..5beddb0 --- /dev/null +++ b/lua/codetyper/llm/claude.lua @@ -0,0 +1,154 @@ +---@mod codetyper.llm.claude Claude API client for Codetyper.nvim + +local M = {} + +local utils = require("codetyper.utils") +local llm = require("codetyper.llm") + +--- Claude API endpoint +local API_URL = "https://api.anthropic.com/v1/messages" + +--- Get API key from config or environment +---@return string|nil API key +local function get_api_key() + local codetyper = require("codetyper") + local config = codetyper.get_config() + + return config.llm.claude.api_key or vim.env.ANTHROPIC_API_KEY +end + +--- Get model from config +---@return string Model name +local function get_model() + local codetyper = require("codetyper") + local config = codetyper.get_config() + + return config.llm.claude.model +end + +--- Build request body for Claude API +---@param prompt string User prompt +---@param context table Context information +---@return table Request body +local function build_request_body(prompt, context) + local system_prompt = llm.build_system_prompt(context) + + return { + model = get_model(), + max_tokens = 4096, + system = system_prompt, + messages = { + { + role = "user", + content = prompt, + }, + }, + } +end + +--- Make HTTP request to Claude API +---@param body table Request body +---@param callback fun(response: string|nil, error: string|nil) Callback function +local function make_request(body, callback) + local api_key = get_api_key() + if not api_key then + callback(nil, "Claude API key not configured") + return + end + + local json_body = vim.json.encode(body) + + -- Use curl for HTTP request (plenary.curl alternative) + local cmd = { + "curl", + "-s", + "-X", "POST", + API_URL, + "-H", "Content-Type: application/json", + "-H", "x-api-key: " .. api_key, + "-H", "anthropic-version: 2023-06-01", + "-d", json_body, + } + + vim.fn.jobstart(cmd, { + stdout_buffered = true, + on_stdout = function(_, data) + if not data or #data == 0 or (data[1] == "" and #data == 1) then + return + end + + local response_text = table.concat(data, "\n") + local ok, response = pcall(vim.json.decode, response_text) + + if not ok then + vim.schedule(function() + callback(nil, "Failed to parse Claude response") + end) + return + end + + if response.error then + vim.schedule(function() + callback(nil, response.error.message or "Claude API error") + end) + return + end + + if response.content and response.content[1] and response.content[1].text then + local code = llm.extract_code(response.content[1].text) + vim.schedule(function() + callback(code, nil) + end) + else + vim.schedule(function() + callback(nil, "No content in Claude response") + end) + end + end, + on_stderr = function(_, data) + if data and #data > 0 and data[1] ~= "" then + vim.schedule(function() + callback(nil, "Claude API request failed: " .. table.concat(data, "\n")) + end) + end + end, + on_exit = function(_, code) + if code ~= 0 then + vim.schedule(function() + callback(nil, "Claude API request failed with code: " .. code) + end) + end + end, + }) +end + +--- Generate code using Claude API +---@param prompt string The user's prompt +---@param context table Context information +---@param callback fun(response: string|nil, error: string|nil) Callback function +function M.generate(prompt, context, callback) + utils.notify("Sending request to Claude...", vim.log.levels.INFO) + + local body = build_request_body(prompt, context) + make_request(body, function(response, err) + if err then + utils.notify(err, vim.log.levels.ERROR) + callback(nil, err) + else + utils.notify("Code generated successfully", vim.log.levels.INFO) + callback(response, nil) + end + end) +end + +--- Check if Claude is properly configured +---@return boolean, string? Valid status and optional error message +function M.validate() + local api_key = get_api_key() + if not api_key or api_key == "" then + return false, "Claude API key not configured" + end + return true +end + +return M diff --git a/lua/codetyper/llm/init.lua b/lua/codetyper/llm/init.lua new file mode 100644 index 0000000..c03ae4e --- /dev/null +++ b/lua/codetyper/llm/init.lua @@ -0,0 +1,101 @@ +---@mod codetyper.llm LLM interface for Codetyper.nvim + +local M = {} + +local utils = require("codetyper.utils") + +--- Get the appropriate LLM client based on configuration +---@return table LLM client module +function M.get_client() + local codetyper = require("codetyper") + local config = codetyper.get_config() + + if config.llm.provider == "claude" then + return require("codetyper.llm.claude") + elseif config.llm.provider == "ollama" then + return require("codetyper.llm.ollama") + else + error("Unknown LLM provider: " .. config.llm.provider) + end +end + +--- Generate code from a prompt +---@param prompt string The user's prompt +---@param context table Context information (file content, language, etc.) +---@param callback fun(response: string|nil, error: string|nil) Callback function +function M.generate(prompt, context, callback) + local client = M.get_client() + client.generate(prompt, context, callback) +end + +--- Build the system prompt for code generation +---@param context table Context information +---@return string System prompt +function M.build_system_prompt(context) + local prompts = require("codetyper.prompts") + + -- Select appropriate system prompt based on context + local prompt_type = context.prompt_type or "code_generation" + local system_prompts = prompts.system + + local system = system_prompts[prompt_type] or system_prompts.code_generation + + -- Substitute variables + system = system:gsub("{{language}}", context.language or "unknown") + system = system:gsub("{{filepath}}", context.file_path or "unknown") + + if context.file_content then + system = system .. "\n\nExisting file content:\n```\n" .. context.file_content .. "\n```" + end + + return system +end + +--- Build context for LLM request +---@param target_path string Path to target file +---@param prompt_type string Type of prompt +---@return table Context object +function M.build_context(target_path, prompt_type) + local content = utils.read_file(target_path) + local ext = vim.fn.fnamemodify(target_path, ":e") + + -- Map extension to language + local lang_map = { + ts = "TypeScript", + tsx = "TypeScript React", + js = "JavaScript", + jsx = "JavaScript React", + py = "Python", + lua = "Lua", + go = "Go", + rs = "Rust", + rb = "Ruby", + java = "Java", + c = "C", + cpp = "C++", + cs = "C#", + } + + return { + file_content = content, + language = lang_map[ext] or ext, + extension = ext, + prompt_type = prompt_type, + file_path = target_path, + } +end + +--- Parse LLM response and extract code +---@param response string Raw LLM response +---@return string Extracted code +function M.extract_code(response) + -- Remove markdown code blocks if present + local code = response:gsub("```%w*\n?", ""):gsub("\n?```", "") + + -- Trim whitespace + code = code:match("^%s*(.-)%s*$") + + return code +end + +return M diff --git a/lua/codetyper/llm/ollama.lua b/lua/codetyper/llm/ollama.lua new file mode 100644 index 0000000..00f16c2 --- /dev/null +++ b/lua/codetyper/llm/ollama.lua @@ -0,0 +1,173 @@ +---@mod codetyper.llm.ollama Ollama API client for Codetyper.nvim + +local M = {} + +local utils = require("codetyper.utils") +local llm = require("codetyper.llm") + +--- Get Ollama host from config +---@return string Host URL +local function get_host() + local codetyper = require("codetyper") + local config = codetyper.get_config() + + return config.llm.ollama.host +end + +--- Get model from config +---@return string Model name +local function get_model() + local codetyper = require("codetyper") + local config = codetyper.get_config() + + return config.llm.ollama.model +end + +--- Build request body for Ollama API +---@param prompt string User prompt +---@param context table Context information +---@return table Request body +local function build_request_body(prompt, context) + local system_prompt = llm.build_system_prompt(context) + + return { + model = get_model(), + system = system_prompt, + prompt = prompt, + stream = false, + options = { + temperature = 0.2, + num_predict = 4096, + }, + } +end + +--- Make HTTP request to Ollama API +---@param body table Request body +---@param callback fun(response: string|nil, error: string|nil) Callback function +local function make_request(body, callback) + local host = get_host() + local url = host .. "/api/generate" + local json_body = vim.json.encode(body) + + local cmd = { + "curl", + "-s", + "-X", "POST", + url, + "-H", "Content-Type: application/json", + "-d", json_body, + } + + vim.fn.jobstart(cmd, { + stdout_buffered = true, + on_stdout = function(_, data) + if not data or #data == 0 or (data[1] == "" and #data == 1) then + return + end + + local response_text = table.concat(data, "\n") + local ok, response = pcall(vim.json.decode, response_text) + + if not ok then + vim.schedule(function() + callback(nil, "Failed to parse Ollama response") + end) + return + end + + if response.error then + vim.schedule(function() + callback(nil, response.error or "Ollama API error") + end) + return + end + + if response.response then + local code = llm.extract_code(response.response) + vim.schedule(function() + callback(code, nil) + end) + else + vim.schedule(function() + callback(nil, "No response from Ollama") + end) + end + end, + on_stderr = function(_, data) + if data and #data > 0 and data[1] ~= "" then + vim.schedule(function() + callback(nil, "Ollama API request failed: " .. table.concat(data, "\n")) + end) + end + end, + on_exit = function(_, code) + if code ~= 0 then + vim.schedule(function() + callback(nil, "Ollama API request failed with code: " .. code) + end) + end + end, + }) +end + +--- Generate code using Ollama API +---@param prompt string The user's prompt +---@param context table Context information +---@param callback fun(response: string|nil, error: string|nil) Callback function +function M.generate(prompt, context, callback) + utils.notify("Sending request to Ollama...", vim.log.levels.INFO) + + local body = build_request_body(prompt, context) + make_request(body, function(response, err) + if err then + utils.notify(err, vim.log.levels.ERROR) + callback(nil, err) + else + utils.notify("Code generated successfully", vim.log.levels.INFO) + callback(response, nil) + end + end) +end + +--- Check if Ollama is reachable +---@param callback fun(ok: boolean, error: string|nil) Callback function +function M.health_check(callback) + local host = get_host() + + local cmd = { "curl", "-s", host .. "/api/tags" } + + vim.fn.jobstart(cmd, { + stdout_buffered = true, + on_stdout = function(_, data) + if data and #data > 0 and data[1] ~= "" then + vim.schedule(function() + callback(true, nil) + end) + end + end, + on_exit = function(_, code) + if code ~= 0 then + vim.schedule(function() + callback(false, "Cannot connect to Ollama at " .. host) + end) + end + end, + }) +end + +--- Check if Ollama is properly configured +---@return boolean, string? Valid status and optional error message +function M.validate() + local host = get_host() + if not host or host == "" then + return false, "Ollama host not configured" + end + local model = get_model() + if not model or model == "" then + return false, "Ollama model not configured" + end + return true +end + +return M diff --git a/lua/codetyper/parser.lua b/lua/codetyper/parser.lua new file mode 100644 index 0000000..e476c4d --- /dev/null +++ b/lua/codetyper/parser.lua @@ -0,0 +1,183 @@ +---@mod codetyper.parser Parser for /@ @/ prompt tags + +local M = {} + +local utils = require("codetyper.utils") + +--- Find all prompts in buffer content +---@param content string Buffer content +---@param open_tag string Opening tag +---@param close_tag string Closing tag +---@return CoderPrompt[] List of found prompts +function M.find_prompts(content, open_tag, close_tag) + local prompts = {} + local escaped_open = utils.escape_pattern(open_tag) + local escaped_close = utils.escape_pattern(close_tag) + + local lines = vim.split(content, "\n", { plain = true }) + local in_prompt = false + local current_prompt = nil + local prompt_content = {} + + for line_num, line in ipairs(lines) do + if not in_prompt then + -- Look for opening tag + local start_col = line:find(escaped_open) + if start_col then + in_prompt = true + current_prompt = { + start_line = line_num, + start_col = start_col, + content = "", + } + -- Get content after opening tag on same line + local after_tag = line:sub(start_col + #open_tag) + local end_col = after_tag:find(escaped_close) + if end_col then + -- Single line prompt + current_prompt.content = after_tag:sub(1, end_col - 1) + current_prompt.end_line = line_num + current_prompt.end_col = start_col + #open_tag + end_col + #close_tag - 2 + table.insert(prompts, current_prompt) + in_prompt = false + current_prompt = nil + else + table.insert(prompt_content, after_tag) + end + end + else + -- Look for closing tag + local end_col = line:find(escaped_close) + if end_col then + -- Found closing tag + local before_tag = line:sub(1, end_col - 1) + table.insert(prompt_content, before_tag) + current_prompt.content = table.concat(prompt_content, "\n") + current_prompt.end_line = line_num + current_prompt.end_col = end_col + #close_tag - 1 + table.insert(prompts, current_prompt) + in_prompt = false + current_prompt = nil + prompt_content = {} + else + table.insert(prompt_content, line) + end + end + end + + return prompts +end + +--- Find prompts in a buffer +---@param bufnr number Buffer number +---@return CoderPrompt[] List of found prompts +function M.find_prompts_in_buffer(bufnr) + local codetyper = require("codetyper") + local config = codetyper.get_config() + + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local content = table.concat(lines, "\n") + + return M.find_prompts(content, config.patterns.open_tag, config.patterns.close_tag) +end + +--- Get prompt at cursor position +---@param bufnr? number Buffer number (default: current) +---@return CoderPrompt|nil Prompt at cursor or nil +function M.get_prompt_at_cursor(bufnr) + bufnr = bufnr or vim.api.nvim_get_current_buf() + local cursor = vim.api.nvim_win_get_cursor(0) + local line = cursor[1] + local col = cursor[2] + 1 -- Convert to 1-indexed + + local prompts = M.find_prompts_in_buffer(bufnr) + + for _, prompt in ipairs(prompts) do + if line >= prompt.start_line and line <= prompt.end_line then + if line == prompt.start_line and col < prompt.start_col then + goto continue + end + if line == prompt.end_line and col > prompt.end_col then + goto continue + end + return prompt + end + ::continue:: + end + + return nil +end + +--- Get the last closed prompt in buffer +---@param bufnr? number Buffer number (default: current) +---@return CoderPrompt|nil Last prompt or nil +function M.get_last_prompt(bufnr) + bufnr = bufnr or vim.api.nvim_get_current_buf() + local prompts = M.find_prompts_in_buffer(bufnr) + + if #prompts > 0 then + return prompts[#prompts] + end + + return nil +end + +--- Extract the prompt type from content +---@param content string Prompt content +---@return "refactor" | "add" | "document" | "explain" | "generic" Prompt type +function M.detect_prompt_type(content) + local lower = content:lower() + + if lower:match("refactor") then + return "refactor" + elseif lower:match("add") or lower:match("create") or lower:match("implement") then + return "add" + elseif lower:match("document") or lower:match("comment") or lower:match("jsdoc") then + return "document" + elseif lower:match("explain") or lower:match("what") or lower:match("how") then + return "explain" + end + + return "generic" +end + +--- Clean prompt content (trim whitespace, normalize newlines) +---@param content string Raw prompt content +---@return string Cleaned content +function M.clean_prompt(content) + -- Trim leading/trailing whitespace + content = content:match("^%s*(.-)%s*$") + -- Normalize multiple newlines + content = content:gsub("\n\n\n+", "\n\n") + return content +end + +--- Check if line contains a closing tag +---@param line string Line to check +---@param close_tag string Closing tag +---@return boolean +function M.has_closing_tag(line, close_tag) + return line:find(utils.escape_pattern(close_tag)) ~= nil +end + +--- Check if buffer has any unclosed prompts +---@param bufnr? number Buffer number (default: current) +---@return boolean +function M.has_unclosed_prompts(bufnr) + bufnr = bufnr or vim.api.nvim_get_current_buf() + local codetyper = require("codetyper") + local config = codetyper.get_config() + + local lines = vim.api.nvim_buf_get_lines(bufnr, 0, -1, false) + local content = table.concat(lines, "\n") + + local escaped_open = utils.escape_pattern(config.patterns.open_tag) + local escaped_close = utils.escape_pattern(config.patterns.close_tag) + + local _, open_count = content:gsub(escaped_open, "") + local _, close_count = content:gsub(escaped_close, "") + + return open_count > close_count +end + +return M diff --git a/lua/codetyper/prompts/ask.lua b/lua/codetyper/prompts/ask.lua new file mode 100644 index 0000000..01473a7 --- /dev/null +++ b/lua/codetyper/prompts/ask.lua @@ -0,0 +1,128 @@ +---@mod codetyper.prompts.ask Ask/explanation prompts for Codetyper.nvim +--- +--- These prompts are used for the Ask panel and code explanations. + +local M = {} + +--- Prompt for explaining code +M.explain_code = [[Please explain the following code: + +{{code}} + +Provide: +1. A high-level overview of what it does +2. Explanation of key parts +3. Any potential issues or improvements +]] + +--- Prompt for explaining a specific function +M.explain_function = [[Explain this function in detail: + +{{code}} + +Include: +1. What the function does +2. Parameters and their purposes +3. Return value +4. Any side effects +5. Usage examples +]] + +--- Prompt for explaining an error +M.explain_error = [[I'm getting this error: + +{{error}} + +In this code: + +{{code}} + +Please explain: +1. What the error means +2. Why it's happening +3. How to fix it +]] + +--- Prompt for code review +M.code_review = [[Please review this code: + +{{code}} + +Provide feedback on: +1. Code quality and readability +2. Potential bugs or issues +3. Performance considerations +4. Security concerns (if applicable) +5. Suggested improvements +]] + +--- Prompt for explaining a concept +M.explain_concept = [[Explain the following programming concept: + +{{concept}} + +Include: +1. Definition and purpose +2. When and why to use it +3. Simple code examples +4. Common pitfalls to avoid +]] + +--- Prompt for comparing approaches +M.compare_approaches = [[Compare these different approaches: + +{{approaches}} + +Analyze: +1. Pros and cons of each +2. Performance implications +3. Maintainability +4. When to use each approach +]] + +--- Prompt for debugging help +M.debug_help = [[Help me debug this issue: + +Problem: {{problem}} + +Code: +{{code}} + +What I've tried: +{{attempts}} + +Please help identify the issue and suggest a solution. +]] + +--- Prompt for architecture advice +M.architecture_advice = [[I need advice on this architecture decision: + +{{question}} + +Context: +{{context}} + +Please provide: +1. Recommended approach +2. Reasoning +3. Potential alternatives +4. Things to consider +]] + +--- Generic ask prompt +M.generic = [[USER QUESTION: {{question}} + +{{#if files}} +ATTACHED FILE CONTENTS: +{{files}} +{{/if}} + +{{#if context}} +ADDITIONAL CONTEXT: +{{context}} +{{/if}} + +Please provide a helpful, accurate response. +]] + +return M diff --git a/lua/codetyper/prompts/code.lua b/lua/codetyper/prompts/code.lua new file mode 100644 index 0000000..1341d79 --- /dev/null +++ b/lua/codetyper/prompts/code.lua @@ -0,0 +1,93 @@ +---@mod codetyper.prompts.code Code generation prompts for Codetyper.nvim +--- +--- These prompts are used for generating new code. + +local M = {} + +--- Prompt template for creating a new function +M.create_function = [[Create a function with the following requirements: + +{{description}} + +Requirements: +- Follow the coding style of the existing file +- Include proper error handling +- Use appropriate types (if applicable) +- Make it efficient and readable +]] + +--- Prompt template for creating a new class/module +M.create_class = [[Create a class/module with the following requirements: + +{{description}} + +Requirements: +- Follow OOP best practices +- Include constructor/initialization +- Implement proper encapsulation +- Add necessary methods as described +]] + +--- Prompt template for implementing an interface/trait +M.implement_interface = [[Implement the following interface/trait: + +{{description}} + +Requirements: +- Implement all required methods +- Follow the interface contract exactly +- Handle edge cases appropriately +]] + +--- Prompt template for creating a React component +M.create_react_component = [[Create a React component with the following requirements: + +{{description}} + +Requirements: +- Use functional components with hooks +- Include proper TypeScript types (if .tsx) +- Follow React best practices +- Make it reusable and composable +]] + +--- Prompt template for creating an API endpoint +M.create_api_endpoint = [[Create an API endpoint with the following requirements: + +{{description}} + +Requirements: +- Include input validation +- Proper error handling and status codes +- Follow RESTful conventions +- Include appropriate middleware +]] + +--- Prompt template for creating a utility function +M.create_utility = [[Create a utility function: + +{{description}} + +Requirements: +- Pure function (no side effects) if possible +- Handle edge cases +- Efficient implementation +- Well-typed (if applicable) +]] + +--- Prompt template for generic code generation +M.generic = [[Generate code based on the following description: + +{{description}} + +Context: +- Language: {{language}} +- File: {{filepath}} + +Requirements: +- Match existing code style +- Follow best practices +- Handle errors appropriately +]] + +return M diff --git a/lua/codetyper/prompts/document.lua b/lua/codetyper/prompts/document.lua new file mode 100644 index 0000000..dfe8680 --- /dev/null +++ b/lua/codetyper/prompts/document.lua @@ -0,0 +1,136 @@ +---@mod codetyper.prompts.document Documentation prompts for Codetyper.nvim +--- +--- These prompts are used for generating documentation. + +local M = {} + +--- Prompt for adding JSDoc comments +M.jsdoc = [[Add JSDoc documentation to this code: + +{{code}} + +Requirements: +- Document all functions and methods +- Include @param for all parameters +- Include @returns for return values +- Add @throws if exceptions are thrown +- Include @example where helpful +- Use @typedef for complex types +]] + +--- Prompt for adding Python docstrings +M.python_docstring = [[Add docstrings to this Python code: + +{{code}} + +Requirements: +- Use Google-style docstrings +- Document all functions and classes +- Include Args, Returns, Raises sections +- Add Examples where helpful +- Include type hints in docstrings +]] + +--- Prompt for adding LuaDoc comments +M.luadoc = [[Add LuaDoc/EmmyLua annotations to this Lua code: + +{{code}} + +Requirements: +- Use ---@param for parameters +- Use ---@return for return values +- Use ---@class for table structures +- Use ---@field for class fields +- Add descriptions for all items +]] + +--- Prompt for adding Go documentation +M.godoc = [[Add GoDoc comments to this Go code: + +{{code}} + +Requirements: +- Start comments with the name being documented +- Document all exported functions, types, and variables +- Keep comments concise but complete +- Follow Go documentation conventions +]] + +--- Prompt for adding README documentation +M.readme = [[Generate README documentation for this code: + +{{code}} + +Include: +- Project description +- Installation instructions +- Usage examples +- API documentation +- Contributing guidelines +]] + +--- Prompt for adding inline comments +M.inline_comments = [[Add helpful inline comments to this code: + +{{code}} + +Guidelines: +- Explain complex logic +- Document non-obvious decisions +- Don't state the obvious +- Keep comments concise +- Use TODO/FIXME where appropriate +]] + +--- Prompt for adding API documentation +M.api_docs = [[Generate API documentation for this code: + +{{code}} + +Include for each endpoint/function: +- Description +- Parameters with types +- Return value with type +- Example request/response +- Error cases +]] + +--- Prompt for adding type definitions +M.type_definitions = [[Generate type definitions for this code: + +{{code}} + +Requirements: +- Define interfaces/types for all data structures +- Include optional properties where appropriate +- Add JSDoc/docstring descriptions +- Export all types that should be public +]] + +--- Prompt for changelog entry +M.changelog = [[Generate a changelog entry for these changes: + +{{changes}} + +Format: +- Use conventional changelog format +- Categorize as Added/Changed/Fixed/Removed +- Be concise but descriptive +- Include breaking changes prominently +]] + +--- Generic documentation prompt +M.generic = [[Add documentation to this code: + +{{code}} + +Language: {{language}} + +Requirements: +- Use appropriate documentation format for the language +- Document all public APIs +- Include parameter and return descriptions +- Add examples where helpful +]] + +return M diff --git a/lua/codetyper/prompts/init.lua b/lua/codetyper/prompts/init.lua new file mode 100644 index 0000000..79df632 --- /dev/null +++ b/lua/codetyper/prompts/init.lua @@ -0,0 +1,56 @@ +---@mod codetyper.prompts Prompt templates for Codetyper.nvim +--- +--- This module provides all prompt templates used by the plugin. +--- Prompts are organized by functionality and can be customized. + +local M = {} + +-- Load all prompt modules +M.system = require("codetyper.prompts.system") +M.code = require("codetyper.prompts.code") +M.ask = require("codetyper.prompts.ask") +M.refactor = require("codetyper.prompts.refactor") +M.document = require("codetyper.prompts.document") + +--- Get a prompt by category and name +---@param category string Category name (system, code, ask, refactor, document) +---@param name string Prompt name +---@param vars? table Variables to substitute in the prompt +---@return string Formatted prompt +function M.get(category, name, vars) + local prompts = M[category] + if not prompts then + error("Unknown prompt category: " .. category) + end + + local prompt = prompts[name] + if not prompt then + error("Unknown prompt: " .. category .. "." .. name) + end + + -- Substitute variables if provided + if vars then + for key, value in pairs(vars) do + prompt = prompt:gsub("{{" .. key .. "}}", tostring(value)) + end + end + + return prompt +end + +--- List all available prompts +---@return table Available prompts by category +function M.list() + local result = {} + for category, prompts in pairs(M) do + if type(prompts) == "table" and category ~= "list" and category ~= "get" then + result[category] = {} + for name, _ in pairs(prompts) do + table.insert(result[category], name) + end + end + end + return result +end + +return M diff --git a/lua/codetyper/prompts/refactor.lua b/lua/codetyper/prompts/refactor.lua new file mode 100644 index 0000000..b64db8b --- /dev/null +++ b/lua/codetyper/prompts/refactor.lua @@ -0,0 +1,128 @@ +---@mod codetyper.prompts.refactor Refactoring prompts for Codetyper.nvim +--- +--- These prompts are used for code refactoring operations. + +local M = {} + +--- Prompt for general refactoring +M.general = [[Refactor this code to improve its quality: + +{{code}} + +Focus on: +- Readability +- Maintainability +- Following best practices +- Keeping the same functionality +]] + +--- Prompt for extracting a function +M.extract_function = [[Extract a function from this code: + +{{code}} + +The function should: +{{description}} + +Requirements: +- Give it a meaningful name +- Include proper parameters +- Return appropriate values +]] + +--- Prompt for simplifying code +M.simplify = [[Simplify this code while maintaining functionality: + +{{code}} + +Goals: +- Reduce complexity +- Remove redundancy +- Improve readability +- Keep all existing behavior +]] + +--- Prompt for converting to async/await +M.async_await = [[Convert this code to use async/await: + +{{code}} + +Requirements: +- Convert all promises to async/await +- Maintain error handling +- Keep the same functionality +]] + +--- Prompt for adding error handling +M.add_error_handling = [[Add proper error handling to this code: + +{{code}} + +Requirements: +- Handle all potential errors +- Use appropriate error types +- Add meaningful error messages +- Don't change core functionality +]] + +--- Prompt for improving performance +M.optimize_performance = [[Optimize this code for better performance: + +{{code}} + +Focus on: +- Algorithm efficiency +- Memory usage +- Reducing unnecessary operations +- Maintaining readability +]] + +--- Prompt for converting to TypeScript +M.convert_to_typescript = [[Convert this JavaScript code to TypeScript: + +{{code}} + +Requirements: +- Add proper type annotations +- Use interfaces where appropriate +- Handle null/undefined properly +- Maintain all functionality +]] + +--- Prompt for applying design pattern +M.apply_pattern = [[Refactor this code to use the {{pattern}} pattern: + +{{code}} + +Requirements: +- Properly implement the pattern +- Maintain existing functionality +- Improve code organization +]] + +--- Prompt for splitting a large function +M.split_function = [[Split this large function into smaller, focused functions: + +{{code}} + +Goals: +- Single responsibility per function +- Clear function names +- Proper parameter passing +- Maintain all functionality +]] + +--- Prompt for removing code smells +M.remove_code_smells = [[Refactor this code to remove code smells: + +{{code}} + +Look for and fix: +- Long methods +- Duplicated code +- Magic numbers +- Deep nesting +- Other anti-patterns +]] + +return M diff --git a/lua/codetyper/prompts/system.lua b/lua/codetyper/prompts/system.lua new file mode 100644 index 0000000..25a3fdb --- /dev/null +++ b/lua/codetyper/prompts/system.lua @@ -0,0 +1,96 @@ +---@mod codetyper.prompts.system System prompts for Codetyper.nvim +--- +--- These are the base system prompts that define the AI's behavior. + +local M = {} + +--- Base system prompt for code generation +M.code_generation = [[You are an expert code generation assistant integrated into Neovim via Codetyper.nvim. +Your task is to generate high-quality, production-ready code based on the user's prompt. + +CRITICAL RULES: +1. Output ONLY the code - no explanations, no markdown code blocks, no comments about what you did +2. Match the coding style, conventions, and patterns of the existing file +3. Use proper indentation and formatting for the language +4. Follow best practices for the specific language/framework +5. Preserve existing functionality unless explicitly asked to change it +6. Use meaningful variable and function names +7. Handle edge cases and errors appropriately + +Language: {{language}} +File: {{filepath}} +]] + +--- System prompt for code explanation/ask +M.ask = [[You are a helpful coding assistant integrated into Neovim via Codetyper.nvim. +You help developers understand code, explain concepts, and answer programming questions. + +GUIDELINES: +1. Be concise but thorough in your explanations +2. Use code examples when helpful +3. Reference the provided code context in your explanations +4. Format responses in markdown for readability +5. If you don't know something, say so honestly +6. Break down complex concepts into understandable parts +7. Provide practical, actionable advice + +IMPORTANT: When file contents are provided, analyze them carefully and base your response on the actual code. +]] + +--- System prompt for refactoring +M.refactor = [[You are an expert code refactoring assistant integrated into Neovim via Codetyper.nvim. +Your task is to refactor code while maintaining its functionality. + +CRITICAL RULES: +1. Output ONLY the refactored code - no explanations +2. Preserve ALL existing functionality +3. Improve code quality, readability, and maintainability +4. Follow SOLID principles and best practices +5. Keep the same coding style as the original +6. Do not add new features unless explicitly requested +7. Optimize performance where possible without sacrificing readability + +Language: {{language}} +]] + +--- System prompt for documentation +M.document = [[You are a documentation expert integrated into Neovim via Codetyper.nvim. +Your task is to generate clear, comprehensive documentation for code. + +CRITICAL RULES: +1. Output ONLY the documentation/comments - ready to be inserted into code +2. Use the appropriate documentation format for the language: + - JavaScript/TypeScript: JSDoc + - Python: Docstrings (Google or NumPy style) + - Lua: LuaDoc/EmmyLua + - Go: GoDoc + - Rust: RustDoc + - Java: Javadoc +3. Document all parameters, return values, and exceptions +4. Include usage examples where helpful +5. Be concise but complete + +Language: {{language}} +]] + +--- System prompt for test generation +M.test = [[You are a test generation expert integrated into Neovim via Codetyper.nvim. +Your task is to generate comprehensive unit tests for the provided code. + +CRITICAL RULES: +1. Output ONLY the test code - no explanations +2. Use the appropriate testing framework for the language: + - JavaScript/TypeScript: Jest or Vitest + - Python: pytest + - Lua: busted or plenary + - Go: testing package + - Rust: built-in tests +3. Cover happy paths, edge cases, and error scenarios +4. Use descriptive test names +5. Follow AAA pattern: Arrange, Act, Assert +6. Mock external dependencies appropriately + +Language: {{language}} +]] + +return M diff --git a/lua/codetyper/tree.lua b/lua/codetyper/tree.lua new file mode 100644 index 0000000..3425280 --- /dev/null +++ b/lua/codetyper/tree.lua @@ -0,0 +1,245 @@ +---@mod codetyper.tree Project tree logging for Codetyper.nvim + +local M = {} + +local utils = require("codetyper.utils") + +--- Name of the coder folder +local CODER_FOLDER = ".coder" + +--- Name of the tree log file +local TREE_LOG_FILE = "tree.log" + +--- Get the path to the .coder folder +---@return string|nil Path to .coder folder or nil +function M.get_coder_folder() + local root = utils.get_project_root() + if not root then + return nil + end + return root .. "/" .. CODER_FOLDER +end + +--- Get the path to the tree.log file +---@return string|nil Path to tree.log or nil +function M.get_tree_log_path() + local coder_folder = M.get_coder_folder() + if not coder_folder then + return nil + end + return coder_folder .. "/" .. TREE_LOG_FILE +end + +--- Ensure .coder folder exists +---@return boolean Success status +function M.ensure_coder_folder() + local coder_folder = M.get_coder_folder() + if not coder_folder then + return false + end + return utils.ensure_dir(coder_folder) +end + +--- Build tree structure recursively +---@param path string Directory path +---@param prefix string Prefix for tree lines +---@param ignore_patterns table Patterns to ignore +---@return string[] Tree lines +local function build_tree(path, prefix, ignore_patterns) + local lines = {} + local entries = {} + + -- Get directory entries + local handle = vim.loop.fs_scandir(path) + if not handle then + return lines + end + + while true do + local name, type = vim.loop.fs_scandir_next(handle) + if not name then + break + end + + -- Check if should be ignored + local should_ignore = false + for _, pattern in ipairs(ignore_patterns) do + if name:match(pattern) then + should_ignore = true + break + end + end + + if not should_ignore then + table.insert(entries, { name = name, type = type }) + end + end + + -- Sort entries (directories first, then alphabetically) + table.sort(entries, function(a, b) + if a.type == "directory" and b.type ~= "directory" then + return true + elseif a.type ~= "directory" and b.type == "directory" then + return false + end + return a.name < b.name + end) + + -- Build tree lines + for i, entry in ipairs(entries) do + local is_last = i == #entries + local connector = is_last and "└── " or "├── " + local child_prefix = is_last and " " or "│ " + + local icon = "" + if entry.type == "directory" then + icon = "📁 " + else + -- File type icons + local ext = entry.name:match("%.([^%.]+)$") + local icons = { + lua = "🌙 ", + ts = "📘 ", + tsx = "⚛️ ", + js = "📒 ", + jsx = "⚛️ ", + py = "🐍 ", + go = "🐹 ", + rs = "🦀 ", + md = "📝 ", + json = "📋 ", + yaml = "📋 ", + yml = "📋 ", + html = "🌐 ", + css = "🎨 ", + scss = "🎨 ", + } + icon = icons[ext] or "📄 " + end + + table.insert(lines, prefix .. connector .. icon .. entry.name) + + if entry.type == "directory" then + local child_path = path .. "/" .. entry.name + local child_lines = build_tree(child_path, prefix .. child_prefix, ignore_patterns) + for _, line in ipairs(child_lines) do + table.insert(lines, line) + end + end + end + + return lines +end + +--- Generate project tree +---@return string Tree content +function M.generate_tree() + local root = utils.get_project_root() + if not root then + return "-- Could not determine project root --" + end + + local project_name = vim.fn.fnamemodify(root, ":t") + local timestamp = os.date("%Y-%m-%d %H:%M:%S") + + -- Patterns to ignore + local ignore_patterns = { + "^%.", -- Hidden files/folders + "^node_modules$", + "^__pycache__$", + "^%.git$", + "^%.coder$", + "^dist$", + "^build$", + "^target$", + "^vendor$", + "%.coder%.", -- Coder files + } + + local lines = { + "# Project Tree: " .. project_name, + "# Generated: " .. timestamp, + "# By: Codetyper.nvim", + "", + "📦 " .. project_name, + } + + local tree_lines = build_tree(root, "", ignore_patterns) + for _, line in ipairs(tree_lines) do + table.insert(lines, line) + end + + table.insert(lines, "") + table.insert(lines, "# Total files tracked by Codetyper") + + return table.concat(lines, "\n") +end + +--- Update the tree.log file +---@return boolean Success status +function M.update_tree_log() + -- Ensure .coder folder exists + if not M.ensure_coder_folder() then + return false + end + + local tree_log_path = M.get_tree_log_path() + if not tree_log_path then + return false + end + + local tree_content = M.generate_tree() + + if utils.write_file(tree_log_path, tree_content) then + -- Silent update, no notification needed for every file change + return true + end + + return false +end + +--- Initialize tree logging (called on setup) +function M.setup() + -- Create initial tree log + M.update_tree_log() +end + +--- Get file statistics from tree +---@return table Statistics { files: number, directories: number } +function M.get_stats() + local root = utils.get_project_root() + if not root then + return { files = 0, directories = 0 } + end + + local stats = { files = 0, directories = 0 } + + local function count_recursive(path) + local handle = vim.loop.fs_scandir(path) + if not handle then + return + end + + while true do + local name, type = vim.loop.fs_scandir_next(handle) + if not name then + break + end + + -- Skip hidden and special folders + if not name:match("^%.") and name ~= "node_modules" and not name:match("%.coder%.") then + if type == "directory" then + stats.directories = stats.directories + 1 + count_recursive(path .. "/" .. name) + else + stats.files = stats.files + 1 + end + end + end + end + + count_recursive(root) + return stats +end + +return M diff --git a/lua/codetyper/types.lua b/lua/codetyper/types.lua new file mode 100644 index 0000000..e49b4d4 --- /dev/null +++ b/lua/codetyper/types.lua @@ -0,0 +1,44 @@ +---@mod codetyper.types Type definitions for Codetyper.nvim + +---@class CoderConfig +---@field llm LLMConfig LLM provider configuration +---@field window WindowConfig Window configuration +---@field patterns PatternConfig Pattern configuration +---@field auto_gitignore boolean Auto-manage .gitignore + +---@class LLMConfig +---@field provider "claude" | "ollama" The LLM provider to use +---@field claude ClaudeConfig Claude-specific configuration +---@field ollama OllamaConfig Ollama-specific configuration + +---@class ClaudeConfig +---@field api_key string | nil Claude API key (or env var ANTHROPIC_API_KEY) +---@field model string Claude model to use + +---@class OllamaConfig +---@field host string Ollama host URL +---@field model string Ollama model to use + +---@class WindowConfig +---@field width number Width of the coder window (percentage or columns) +---@field position "left" | "right" Position of the coder window +---@field border string Border style for floating windows + +---@class PatternConfig +---@field open_tag string Opening tag for prompts +---@field close_tag string Closing tag for prompts +---@field file_pattern string Pattern for coder files + +---@class CoderPrompt +---@field content string The prompt content between tags +---@field start_line number Starting line number +---@field end_line number Ending line number +---@field start_col number Starting column +---@field end_col number Ending column + +---@class CoderFile +---@field coder_path string Path to the .coder.* file +---@field target_path string Path to the target file +---@field filetype string The filetype/extension + +return {} diff --git a/lua/codetyper/utils.lua b/lua/codetyper/utils.lua new file mode 100644 index 0000000..243b07f --- /dev/null +++ b/lua/codetyper/utils.lua @@ -0,0 +1,125 @@ +---@mod codetyper.utils Utility functions for Codetyper.nvim + +local M = {} + +--- Get the project root directory +---@return string|nil Root directory path or nil if not found +function M.get_project_root() + -- Try to find common root indicators + local markers = { ".git", ".gitignore", "package.json", "Cargo.toml", "go.mod", "pyproject.toml" } + + local current = vim.fn.getcwd() + + for _, marker in ipairs(markers) do + local found = vim.fn.findfile(marker, current .. ";") + if found ~= "" then + return vim.fn.fnamemodify(found, ":p:h") + end + found = vim.fn.finddir(marker, current .. ";") + if found ~= "" then + return vim.fn.fnamemodify(found, ":p:h") + end + end + + return current +end + +--- Check if a file is a coder file +---@param filepath string File path to check +---@return boolean +function M.is_coder_file(filepath) + return filepath:match("%.coder%.") ~= nil +end + +--- Get the target file path from a coder file path +---@param coder_path string Path to the coder file +---@return string Target file path +function M.get_target_path(coder_path) + -- Convert index.coder.ts -> index.ts + return coder_path:gsub("%.coder%.", ".") +end + +--- Get the coder file path from a target file path +---@param target_path string Path to the target file +---@return string Coder file path +function M.get_coder_path(target_path) + -- Convert index.ts -> index.coder.ts + local dir = vim.fn.fnamemodify(target_path, ":h") + local name = vim.fn.fnamemodify(target_path, ":t:r") + local ext = vim.fn.fnamemodify(target_path, ":e") + + if dir == "." then + return name .. ".coder." .. ext + end + return dir .. "/" .. name .. ".coder." .. ext +end + +--- Check if a file exists +---@param filepath string File path to check +---@return boolean +function M.file_exists(filepath) + local stat = vim.loop.fs_stat(filepath) + return stat ~= nil +end + +--- Read file contents +---@param filepath string File path to read +---@return string|nil Contents or nil if error +function M.read_file(filepath) + local file = io.open(filepath, "r") + if not file then + return nil + end + local content = file:read("*all") + file:close() + return content +end + +--- Write content to file +---@param filepath string File path to write +---@param content string Content to write +---@return boolean Success status +function M.write_file(filepath, content) + local file = io.open(filepath, "w") + if not file then + return false + end + file:write(content) + file:close() + return true +end + +--- Create directory if it doesn't exist +---@param dirpath string Directory path +---@return boolean Success status +function M.ensure_dir(dirpath) + if vim.fn.isdirectory(dirpath) == 0 then + return vim.fn.mkdir(dirpath, "p") == 1 + end + return true +end + +--- Notify user with proper formatting +---@param msg string Message to display +---@param level? number Vim log level (default: INFO) +function M.notify(msg, level) + level = level or vim.log.levels.INFO + vim.notify("[Codetyper] " .. msg, level) +end + +--- Get buffer filetype +---@param bufnr? number Buffer number (default: current) +---@return string Filetype +function M.get_filetype(bufnr) + bufnr = bufnr or 0 + return vim.bo[bufnr].filetype +end + +--- Escape pattern special characters +---@param str string String to escape +---@return string Escaped string +function M.escape_pattern(str) + return str:gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1") +end + +return M diff --git a/lua/codetyper/window.lua b/lua/codetyper/window.lua new file mode 100644 index 0000000..e7fa506 --- /dev/null +++ b/lua/codetyper/window.lua @@ -0,0 +1,177 @@ +---@mod codetyper.window Window management for Codetyper.nvim + +local M = {} + +local utils = require("codetyper.utils") + +---@type number|nil Current coder window ID +M._coder_win = nil + +---@type number|nil Current target window ID +M._target_win = nil + +---@type number|nil Current coder buffer ID +M._coder_buf = nil + +---@type number|nil Current target buffer ID +M._target_buf = nil + +--- Calculate window width based on configuration +---@param config CoderConfig Plugin configuration +---@return number Width in columns +local function calculate_width(config) + local width = config.window.width + if width <= 1 then + -- Percentage of total width + return math.floor(vim.o.columns * width) + end + return math.floor(width) +end + +--- Open the coder split view +---@param target_path string Path to the target file +---@param coder_path string Path to the coder file +---@return boolean Success status +function M.open_split(target_path, coder_path) + local codetyper = require("codetyper") + local config = codetyper.get_config() + + -- Ensure coder file exists, create if not + if not utils.file_exists(coder_path) then + local dir = vim.fn.fnamemodify(coder_path, ":h") + utils.ensure_dir(dir) + utils.write_file(coder_path, "") + + -- Ensure gitignore is updated when creating a new coder file + local gitignore = require("codetyper.gitignore") + gitignore.ensure_ignored() + end + + -- Store current window as target window + M._target_win = vim.api.nvim_get_current_win() + M._target_buf = vim.api.nvim_get_current_buf() + + -- Open target file if not already open + if vim.fn.expand("%:p") ~= target_path then + vim.cmd("edit " .. vim.fn.fnameescape(target_path)) + M._target_buf = vim.api.nvim_get_current_buf() + end + + -- Calculate width + local width = calculate_width(config) + + -- Create the coder split + if config.window.position == "left" then + vim.cmd("topleft vsplit " .. vim.fn.fnameescape(coder_path)) + else + vim.cmd("botright vsplit " .. vim.fn.fnameescape(coder_path)) + end + + -- Store coder window reference + M._coder_win = vim.api.nvim_get_current_win() + M._coder_buf = vim.api.nvim_get_current_buf() + + -- Set coder window width + vim.api.nvim_win_set_width(M._coder_win, width) + + -- Set up window options for coder window + vim.wo[M._coder_win].number = true + vim.wo[M._coder_win].relativenumber = true + vim.wo[M._coder_win].wrap = true + vim.wo[M._coder_win].signcolumn = "yes" + + -- Focus on target window (right side) by default + if config.window.position == "left" then + vim.api.nvim_set_current_win(M._target_win) + end + + utils.notify("Coder view opened: " .. vim.fn.fnamemodify(coder_path, ":t")) + + return true +end + +--- Close the coder split view +---@return boolean Success status +function M.close_split() + if M._coder_win and vim.api.nvim_win_is_valid(M._coder_win) then + vim.api.nvim_win_close(M._coder_win, false) + M._coder_win = nil + M._coder_buf = nil + utils.notify("Coder view closed") + return true + end + return false +end + +--- Toggle the coder split view +---@param target_path? string Path to the target file +---@param coder_path? string Path to the coder file +function M.toggle_split(target_path, coder_path) + if M._coder_win and vim.api.nvim_win_is_valid(M._coder_win) then + M.close_split() + else + if target_path and coder_path then + M.open_split(target_path, coder_path) + else + utils.notify("No file specified for coder view", vim.log.levels.WARN) + end + end +end + +--- Check if coder view is currently open +---@return boolean +function M.is_open() + return M._coder_win ~= nil and vim.api.nvim_win_is_valid(M._coder_win) +end + +--- Get current coder window ID +---@return number|nil +function M.get_coder_win() + return M._coder_win +end + +--- Get current target window ID +---@return number|nil +function M.get_target_win() + return M._target_win +end + +--- Get current coder buffer ID +---@return number|nil +function M.get_coder_buf() + return M._coder_buf +end + +--- Get current target buffer ID +---@return number|nil +function M.get_target_buf() + return M._target_buf +end + +--- Focus on the coder window +function M.focus_coder() + if M._coder_win and vim.api.nvim_win_is_valid(M._coder_win) then + vim.api.nvim_set_current_win(M._coder_win) + end +end + +--- Focus on the target window +function M.focus_target() + if M._target_win and vim.api.nvim_win_is_valid(M._target_win) then + vim.api.nvim_set_current_win(M._target_win) + end +end + +--- Sync scroll between windows (optional feature) +---@param enable boolean Enable or disable sync scroll +function M.sync_scroll(enable) + if not M.is_open() then + return + end + + local value = enable and "scrollbind" or "noscrollbind" + vim.wo[M._coder_win][value] = enable + vim.wo[M._target_win][value] = enable +end + +return M diff --git a/plugin/codetyper.lua b/plugin/codetyper.lua new file mode 100644 index 0000000..720eb32 --- /dev/null +++ b/plugin/codetyper.lua @@ -0,0 +1,121 @@ +-- Codetyper.nvim - AI-powered coding partner for Neovim +-- Plugin loader + +-- Prevent loading twice +if vim.g.loaded_codetyper then + return +end +vim.g.loaded_codetyper = true + +-- Minimum Neovim version check +if vim.fn.has("nvim-0.8.0") == 0 then + vim.api.nvim_err_writeln("Codetyper.nvim requires Neovim 0.8.0 or higher") + return +end + +-- Initialize .coder folder and tree.log on project open +vim.api.nvim_create_autocmd("VimEnter", { + callback = function() + -- Delay slightly to ensure cwd is set + vim.defer_fn(function() + local tree = require("codetyper.tree") + tree.setup() + + -- Also ensure gitignore is updated + local gitignore = require("codetyper.gitignore") + gitignore.ensure_ignored() + end, 100) + end, + desc = "Initialize Codetyper .coder folder on startup", +}) + +-- Also initialize on directory change +vim.api.nvim_create_autocmd("DirChanged", { + callback = function() + vim.defer_fn(function() + local tree = require("codetyper.tree") + tree.setup() + + local gitignore = require("codetyper.gitignore") + gitignore.ensure_ignored() + end, 100) + end, + desc = "Initialize Codetyper .coder folder on directory change", +}) + +-- Auto-initialize when opening a coder file (for nvim-tree, telescope, etc.) +vim.api.nvim_create_autocmd({ "BufRead", "BufNewFile", "BufEnter" }, { + pattern = "*.coder.*", + callback = function() + -- Initialize plugin if not already done + local codetyper = require("codetyper") + if not codetyper.is_initialized() then + codetyper.setup() + end + end, + desc = "Auto-initialize Codetyper when opening coder files", +}) + +-- Lazy-load the plugin on first command usage +vim.api.nvim_create_user_command("Coder", function(opts) + require("codetyper").setup() + -- Re-execute the command now that plugin is loaded + vim.cmd("Coder " .. (opts.args or "")) +end, { + nargs = "?", + complete = function() + return { + "open", "close", "toggle", "process", "status", "focus", + "tree", "tree-view", "reset", "gitignore", + "ask", "ask-close", "ask-toggle", "ask-clear", + } + end, + desc = "Codetyper.nvim commands", +}) + +-- Lazy-load aliases +vim.api.nvim_create_user_command("CoderOpen", function() + require("codetyper").setup() + vim.cmd("CoderOpen") +end, { desc = "Open Coder view" }) + +vim.api.nvim_create_user_command("CoderClose", function() + require("codetyper").setup() + vim.cmd("CoderClose") +end, { desc = "Close Coder view" }) + +vim.api.nvim_create_user_command("CoderToggle", function() + require("codetyper").setup() + vim.cmd("CoderToggle") +end, { desc = "Toggle Coder view" }) + +vim.api.nvim_create_user_command("CoderProcess", function() + require("codetyper").setup() + vim.cmd("CoderProcess") +end, { desc = "Process prompt and generate code" }) + +vim.api.nvim_create_user_command("CoderTree", function() + require("codetyper").setup() + vim.cmd("CoderTree") +end, { desc = "Refresh tree.log" }) + +vim.api.nvim_create_user_command("CoderTreeView", function() + require("codetyper").setup() + vim.cmd("CoderTreeView") +end, { desc = "View tree.log" }) + +-- Ask panel commands +vim.api.nvim_create_user_command("CoderAsk", function() + require("codetyper").setup() + vim.cmd("CoderAsk") +end, { desc = "Open Ask panel" }) + +vim.api.nvim_create_user_command("CoderAskToggle", function() + require("codetyper").setup() + vim.cmd("CoderAskToggle") +end, { desc = "Toggle Ask panel" }) + +vim.api.nvim_create_user_command("CoderAskClear", function() + require("codetyper").setup() + vim.cmd("CoderAskClear") +end, { desc = "Clear Ask history" })