diff --git a/CLAUDE.md b/CLAUDE.md index 2162ab94..d7c1b8a0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,525 +1,3 @@ # CLAUDE.md -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Project Overview - -This is yac.vim - a minimal LSP bridge for Vim written in Rust. Despite the name "YAC" (Yet Another Code completion), this is specifically a lightweight LSP bridge, not a completion system. - -The project consists of three main components: -1. **vim crate**: A comprehensive vim client library with v4 specification support -2. **lsp-bridge**: A Rust binary with modular LSP handlers (~3.4k lines total) that bridges Vim and LSP servers -3. **Vim Plugin**: VimScript files (~1.7k lines) that provide comprehensive Vim integration - -**IMPORTANT**: The current implementation uses vim crate v4 with unified message processing, featuring dual request/notification semantics, clean `Option` response handling, and elimination of redundant protocol metadata. - -## README vs Reality - -**⚠️ CRITICAL DISCREPANCY WARNING ⚠️** - -The README.md file describes a completely different architecture than what's actually implemented: - -| README Claims | Actual Implementation | -|---------------|----------------------| -| TCP server-client architecture | stdin/stdout process communication | -| `:YACStart`, `:YACStatus` commands | `:YacDefinition`, `:YacHover`, `:YacComplete` commands | -| Multi-editor server support | One process per Vim instance | -| Performance benchmarks (800ms→200ms) | No benchmarks performed | -| Config files in `~/.config/yac-vim/` | No config file support | -| `test_simple.sh`, `run_simple_tests.sh` | These scripts don't exist | - -**For accurate information about the current implementation, trust this CLAUDE.md file and the actual source code, not the README.** - -## Development Environment Setup - -### Pre-commit Hooks (Required for Quality Assurance) - -This project uses pre-commit hooks to ensure code quality. Claude Code should install them before making any changes: - -```bash -# Install pre-commit hooks using the setup script -./scripts/setup-hooks.sh - -# Or configure Git to use the scripts directory directly (preferred) -git config core.hooksPath scripts -``` - -**What the hooks check:** -- `cargo fmt --check` - Ensures code is properly formatted -- `cargo clippy -- -D warnings` - Catches common mistakes and enforces best practices - -**For Claude Code users**: Run the setup command immediately after checkout to ensure all commits meet quality standards. - -## Build and Development Commands - -### Building -```bash -# Build the project -cargo build --release - -# Build debug version -cargo build -``` - -### Testing -```bash -# Run Rust unit/integration tests -cargo test - -# Manual testing with development vimrc -vim -u vimrc test_data/src/lib.rs - -# Test goto definition manually: -# 1. Open test_data/src/lib.rs in Vim -# 2. Navigate to a symbol (e.g., User::new usage on line ~31) -# 3. Press 'gd' to jump to definition -# 4. Should jump to the struct definition - -# Test other features: -# - Press 'K' for hover information -# - Press 'gD' for goto declaration -# - Use :YacComplete for completion -``` - -### Development and Debugging -```bash -# Build and run with debug logging -cargo build --release -RUST_LOG=debug ./target/release/lsp-bridge - -# Check logs -tail -f /tmp/lsp-bridge.log - -# NOTE: The binary runs as a stdin/stdout filter, not a standalone server -# It's designed to be launched by Vim via job_start() -``` - -## Architecture - -### Core Components - -**Workspace Structure:** -- `crates/lsp-bridge/` - Main bridge binary (~3.4k lines total) - - `src/handlers/` - 19 modular LSP request handlers (~2.7k lines) - - `src/main.rs` - Entry point and vim crate integration (~124 lines) - - `src/lsp_registry.rs` - Core LSP bridge logic and data structures (~388 lines) -- `crates/lsp-client/` - LSP client library with JSON-RPC handling -- `crates/vim/` - vim crate v4 implementation with unified VimMessage protocol and VimContext trait -- `vim/` - Vim plugin files (~1.7k lines VimScript total) -- `test_data/` - Test Rust project for development -- `tests/vim/` - Vim integration tests -- `docs/` - Requirements and design documentation - -**Communication Flow:** -``` -# Requests (need response): -Vim Plugin (ch_sendexpr) → vim crate v4 (JSON-RPC) → lsp-bridge → LSP Server - -# Notifications (fire-and-forget): -Vim Plugin (ch_sendraw) → vim crate v4 (JSON array) → lsp-bridge → LSP Server -``` - -**Process Model:** -- Vim launches `lsp-bridge` as a child process using `job_start()` with `'mode': 'json'` -- Each Vim instance has its own `lsp-bridge` process (no shared server) -- Communication uses vim crate v4's unified message processing with dual JSON-RPC/notification protocols -- Process terminates when Vim closes or `:YacStop` is called - -### Unified Request/Notification Architecture - -The system uses vim crate v4 with unified message processing supporting both request/response and notification patterns: - -**Dual Request/Notification API:** -```vim -" 1. Request pattern - expects response -function! s:request(method, params, callback_func) - call ch_sendexpr(s:job, jsonrpc_msg, {'callback': a:callback_func}) -endfunction - -" 2. Notification pattern - fire-and-forget -function! s:notify(method, params) - call ch_sendraw(s:job, json_encode([jsonrpc_msg]) . "\n") -endfunction - -" 3. LSP goto methods now use notifications for immediate action -function! yac_bridge#goto_definition() - call s:notify('goto_definition', {'file': expand('%:p'), 'line': line('.')-1, 'column': col('.')-1}) -endfunction -``` - -**Vim Crate v4 Message Types:** -```rust -/// Unified Vim message types - handles both JSON-RPC and Vim channel protocols -pub enum VimMessage { - // JSON-RPC messages (vim-to-client) - Request { id: u64, method: String, params: Value }, - Response { id: i64, result: Value }, - Notification { method: String, params: Value }, - - // Vim channel commands (client-to-vim) - Call { func: String, args: Vec, id: u64 }, - CallAsync { func: String, args: Vec }, - Expr { expr: String, id: u64 }, - // ... more command types -} - -// Clean Option response - no redundant metadata -pub struct Location { - pub file: String, // Complete location data - pub line: u32, // or nothing at all - pub column: u32, -} -pub type GotoResponse = Option; -``` - -**Protocol Semantics:** -```json -// Request message (JSON-RPC) -[1, "method": "goto_definition", "params": {"file": "/path/file.rs", "line": 31, "column": 26}] - -// Notification message (JSON array) -[{"method": "goto_definition", "params": {"file": "/path/file.rs", "line": 31, "column": 26}}] - -// Response data (Option semantics) -[1, {"file": "/path/file.rs", "line": 31, "column": 26}] // Success -[1, {"error": "No definition found"}] // No definition found -``` - -### Key Implementation Details - -1. **Unified message processing**: vim crate v4 handles both JSON-RPC requests and notification arrays intelligently -2. **Dual semantics**: Clear separation between requests (need response) and notifications (fire-and-forget) -3. **Option responses**: Data either exists completely or not at all, no partial states -4. **Handler trait integration**: All handlers now take `&mut Vim` parameter for direct vim interaction -5. **Protocol intelligence**: Automatic encoding/parsing based on message type eliminates protocol confusion -6. **Silent error handling**: Empty responses are handled silently, no explicit error checking needed -7. **Auto-initialization**: LSP servers start when files are opened (`BufReadPost`/`BufNewFile`) -8. **Workspace detection**: Automatically finds `Cargo.toml` for workspace root -9. **JSON channel mode**: Vim uses `'mode': 'json'` for vim crate v4 communication -10. **Type-safe responses**: Rust type system guarantees response data integrity - -## Plugin Configuration - -### Development Setup -The `vimrc` file provides test configuration: -```vim -let g:yac_bridge_command = ['./target/release/lsp-bridge'] -let g:yac_bridge_auto_start = 1 -``` - -### Auto-Completion Settings -```vim -let g:yac_bridge_auto_complete = 1 " Enable auto-completion (default: 1) -let g:yac_bridge_auto_complete_delay = 200 " Delay in milliseconds (default: 200) -let g:yac_bridge_auto_complete_min_chars = 1 " Minimum characters to trigger (default: 1) -``` - -**Smart Delay Strategy**: -- First trigger: Uses configured delay (default 200ms) -- Subsequent filtering: Uses 50ms delay for responsive filtering -- Existing completion data is filtered locally without LSP requests - -### Default Key Mappings -```vim -nnoremap gd :YacDefinition -nnoremap gD :YacDeclaration -nnoremap gy :YacTypeDefinition -nnoremap gi :YacImplementation -nnoremap gr :YacReferences -nnoremap K :YacHover -" Manual completion trigger -inoremap :YacComplete -``` - -### Available Commands -```vim -:YacStart " Start LSP bridge process -:YacStop " Stop LSP bridge process -:YacDefinition " Jump to symbol definition -:YacDeclaration " Jump to symbol declaration -:YacTypeDefinition " Jump to type definition -:YacImplementation " Jump to implementation -:YacHover " Show hover information -:YacComplete " Trigger completion manually -:YacReferences " Find all references -:YacInlayHints " Show inlay hints for current file -:YacClearInlayHints " Clear displayed inlay hints -:YacOpenLog " Open LSP bridge log file -:YacDebugToggle " Toggle debug mode for message logging -:YacDebugStatus " Show debug status, pending requests, and log locations -:YacClearPendingRequests " Clear stale pending requests (30s+ timeout) -``` - -### Log Viewing Commands -```vim -:YacOpenLog " Open log viewer in a new buffer -:YacClearLog " Clear current log file -``` - -**Log Features**: -- Each lsp-bridge process has isolated log file: `/tmp/lsp-bridge-.log` -- Press 'r' in log buffer to refresh content - -### Debug Mode Commands -```vim -:YacDebugToggle " Enable/disable debug logging -:YacDebugStatus " Show current debug state and log paths -``` - -**Debug Features**: -- **Command Send/Receive Logging**: Shows outgoing commands and incoming responses -- **Channel Communication Logging**: Logs to `/tmp/vim_channel.log` when debug enabled -- **Request Correlation**: Tracks request/response pairs with unique IDs -- **Process Restart**: Automatically restarts process when debug is enabled to capture logs - -## Current Functionality - -### Notification System - -**Dual Request/Response Architecture:** -- **Requests** (`s:request`): For operations needing responses (completion, hover, etc.) -- **Notifications** (`s:notify`): For fire-and-forget operations (goto definition, diagnostics) - -**Key Characteristics:** -- **Request Transport**: Uses `ch_sendexpr()` with callback handlers -- **Notification Transport**: Uses `ch_sendraw()` with JSON array format -- **Protocol Intelligence**: vim crate v4 automatically detects and parses message types -- **Immediate Action**: Notifications trigger immediate LSP server-side actions without waiting - -**Example Usage:** -```vim -" Fire-and-forget notification for goto definition -call s:notify('goto_definition', {'file': expand('%:p'), 'line': line('.')-1, 'column': col('.')-1}) - -" Request with response callback for hover information -call s:request('hover', {'file': expand('%:p'), 'line': line('.')-1, 'column': col('.')-1}, 's:handle_hover_response') -``` - -**Debug Features:** -- `[SEND]` prefix for requests in debug output -- `[NOTIFY]` prefix for notifications in debug output -- Separate logging paths track request vs notification flows - -### Implemented Features ✅ -- `file_open` command - Initialize file in LSP server -- `goto_definition` command - Jump to symbol definitions using notification-based immediate action -- `goto_declaration` command - Jump to symbol declarations using notification-based immediate action -- `hover` command - Show documentation/type information in floating popup -- `completion` command - Advanced code completion with: - - **Auto-trigger**: Automatically shows completions while typing (300ms delay) - - **Smart context**: Only triggers in appropriate contexts (not in strings/comments) - - **Keyboard navigation**: Ctrl+P/Ctrl+N, arrow keys - - **Visual selection**: ▶ marker for current selection - - **Confirmation**: Enter/Tab to accept, Esc to cancel - - **Type-based colors**: Function=blue, Variable=green, etc. - - **Match highlighting**: [brackets] around matching characters -- `inlay_hints` command - Display inline type annotations and parameter names: - - **Type hints**: Show variable types (`: i32`) after declarations - - **Parameter hints**: Show parameter names (`count: 5`) in function calls - - **Text properties**: Uses Vim 8.1+ text properties for optimal display - - **Fallback support**: Falls back to match highlighting for older Vim versions - - **Customizable styling**: Separate highlight groups for types and parameters -- Auto-initialization on file open (`BufReadPost`/`BufNewFile` for `*.rs` files) -- Silent "no definition found" and "no declaration found" handling -- Workspace root detection for `rust-analyzer` (searches for `Cargo.toml`) -- Popup window support for Vim 8.1+ - -### Language Support -- **Rust**: Full support via `rust-analyzer` -- **Other languages**: Framework exists but not implemented - -### Planned Features -- Multi-language support (Python, TypeScript, Go, etc.) -- Configuration file support -- More LSP features (references, symbols, diagnostics) - -## Development Principles - -The codebase follows strict simplicity constraints and Linus Torvalds' engineering philosophy: - -### Core Design Philosophy -- **Good Taste**: "Bad programmers worry about the code. Good programmers worry about data structures" -- **Eliminate Special Cases**: Use proper data structures to eliminate conditional complexity -- **Option Pattern**: Data either exists completely (Location) or not at all (None) -- **No Partial States**: Avoid invalid combinations like `file` existing but `line` missing - -### Implementation Constraints -- **Code organization**: Structured into focused modules (vim crate: ~900 lines, handlers: ~2800 lines, vim plugin: ~1660 lines) -- **No over-engineering**: "Make it work, make it right, make it fast" -- **Unix philosophy**: Do one thing (LSP bridging) and do it well -- **Data-driven design**: Let type system enforce correctness rather than runtime checks - -### Recent Architecture Evolution -- **v0.1**: Unified request tracking with complex dispatch logic (~150 lines of complexity) -- **v0.2**: Individual callback handlers with `Option` semantics (simplified to ~50 lines) -- **v0.3**: vim crate v4 implementation with unified message processing and dual request/notification patterns -- **Protocol cleanup**: Removed redundant `action` fields, embraced data presence as signal -- **Handler modernization**: All handlers now use `&mut Vim` parameter for direct vim interaction - -The current v4 implementation prioritizes architectural clarity and protocol intelligence following "good taste" principles over micro-optimizations. - -### Handler Organization - -**Structured Handler Directory:** -``` -crates/lsp-bridge/src/handlers/ -├── mod.rs - Module exports and documentation -├── definition.rs - goto_definition, goto_declaration, etc. -└── file_open.rs - File initialization and LSP setup -``` - -**Benefits of Handler Organization:** -- **Clear separation of concerns**: Each handler file focuses on specific LSP functionality -- **Easy extensibility**: Adding new LSP features requires creating new handler files -- **Better maintainability**: Related code is grouped together logically -- **Import clarity**: `use handlers::{DefinitionHandler, FileOpenHandler}` vs scattered individual imports - -### VimContext Integration - -**Interface Segregation Pattern:** -The vim crate v4 provides a clean `VimContext` trait that handlers use for vim interaction: - -```rust -#[async_trait] -pub trait VimContext: Send + Sync { - async fn call(&mut self, func: &str, args: Vec) -> Result; - async fn call_async(&mut self, func: &str, args: Vec) -> Result<()>; - async fn expr(&mut self, expr: &str) -> Result; - async fn ex(&mut self, command: &str) -> Result<()>; - async fn normal(&mut self, keys: &str) -> Result<()>; - // ... more vim operations -} -``` - -**Handler Integration:** -```rust -#[async_trait] -impl Handler for GotoHandler { - async fn handle(&self, ctx: &mut dyn VimContext, input: Self::Input) -> Result> { - // Direct vim interaction - no complex callback setup needed - ctx.ex(format!("edit {}", location.file).as_str()).await.ok(); - ctx.call_async("cursor", vec![json!(location.line + 1), json!(location.column + 1)]).await.ok(); - Ok(None) - } -} -``` - -**Benefits:** -- **Direct Action**: Handlers can immediately perform vim actions (edit files, move cursor) -- **No Callbacks**: Eliminates complex vim-side response handling for simple operations -- **Interface Segregation**: Handlers only get vim operations they need, not transport internals -- **Type Safety**: Rust async/await with proper error handling - -## Testing and Debugging - -### Manual Testing -```bash -# Start development environment -vim -u vimrc - -# Test goto definition manually: -# 1. Open test_data/src/lib.rs -# 2. Navigate to User::new usage -# 3. Press 'gd' to jump to definition -# 4. Press 'gD' to jump to declaration - -# Run automated Vim integration tests: -vim -u vimrc -c 'source tests/vim/goto_definition.vim' -vim -u vimrc -c 'source tests/vim/declaration_test.vim' -vim -u vimrc -c 'source tests/vim/completion_test.vim' - -# Auto-completion testing (manual only): -vim -u vimrc -c 'source tests/vim/auto_complete_demo.vim' -# Then manually type in INSERT mode to test auto-completion -``` - -### Auto-Completion Testing -Auto-completion must be tested manually due to the nature of interactive events: - -1. **Setup**: `vim -u vimrc -c 'source tests/vim/auto_complete_demo.vim'` -2. **Test typing**: Enter insert mode and type `HashMap::`, `Vec::`, etc. -3. **Verify features**: - - 300ms delay before popup appears - - ▶ selection indicator and [match] highlighting - - Ctrl+P/N navigation, Enter/Tab confirmation - - Smart context detection (no completion in strings/comments) - -### Debug Information -- LSP bridge logs: `/tmp/lsp-bridge-.log` -- Vim debug logs: Use `:YacDebugToggle` to enable -- Channel logs: `/tmp/vim_channel.log` when debug mode enabled -- Enable Rust debug with `RUST_LOG=debug` - -**Debug Usage Example:** -```vim -:YacDebugToggle " Enable debug mode -:YacDefinition " Will show: -" YacDebug[SEND]: goto_definition -> lib.rs:31:26 -" YacDebug[JSON]: {"method": "goto_definition", "params": {...}} -" YacDebug[RECV]: goto_definition response: {"file": "/path/file.rs", "line": 31, "column": 26} -``` - -The test data includes a simple Rust project structure for validating LSP functionality. - -### Common Issues -1. **"lsp-bridge not running"**: The process failed to start, check if binary exists at path -2. **No response from LSP**: Check `/tmp/lsp-bridge.log` for LSP server errors -3. **No definition found**: This is silently handled (expected for some symbols) -4. **Popup not showing**: Requires Vim 8.1+ for popup support, falls back to echo on older versions - -### Troubleshooting Commands -```bash -# Check if binary exists and is executable -ls -la ./target/release/lsp-bridge - -# Test binary manually (it expects JSON-RPC format) -echo '[1, {"method":"goto_definition","params":{"command":"goto_definition","file":"/path/to/file.rs","line":0,"column":0}}]' | ./target/release/lsp-bridge - -# Check rust-analyzer is installed -which rust-analyzer -``` - -## Code Development and Review Philosophy - -This project follows Linus Torvalds' engineering philosophy for both code development and code review. For detailed methodology and guidelines, see [docs/linus-persona.md](docs/linus-persona.md). - -### Core Development Principles - -**Apply Linus-style thinking to ALL code development:** - -1. **"Good Taste" First**: Before writing any code, ask: - - Can I eliminate special cases through better data structures? - - Are there repetitive patterns that indicate poor abstraction? - - Can 10 lines become 3 lines through better design? - -2. **Never Break Userspace**: All changes must maintain backward compatibility - - JSON protocol interface must remain stable - - Vim plugin commands must continue working - - No breaking changes to existing functionality - -3. **Pragmatic Implementation**: - - Solve real problems, not theoretical ones - - Use the simplest approach that works - - Avoid over-engineering and premature abstraction - -4. **Simplicity Obsession**: - - Functions should do one thing well - - Maximum 3 levels of indentation - - Eliminate code duplication through better data structures - - "Bad programmers worry about the code. Good programmers worry about data structures." - -### Linus-Style Development Process - -**Before implementing any feature:** - -1. **Data Structure Analysis**: What are the core data relationships? Can better structures eliminate complexity? -2. **Special Case Elimination**: Identify all conditional branches - can they be eliminated through redesign? -3. **Complexity Minimization**: Can this be implemented with fewer concepts? -4. **Breaking Change Check**: Will this affect any existing functionality? -5. **Practical Validation**: Is this solving a real problem users actually have? - -### Code Review Guidelines -- **Good Taste**: Eliminate special cases through better data structures -- **Backward Compatibility**: Never break existing functionality -- **Pragmatism**: Solve real problems, not theoretical ones -- **Simplicity**: Keep complexity to a minimum, avoid deep nesting +linux style [docs/linus-persona.md](docs/linus-persona.md) diff --git a/README.md b/README.md index 4e585405..5879369b 100644 --- a/README.md +++ b/README.md @@ -4,8 +4,9 @@ A minimal LSP bridge for Vim written in Rust. Despite the name "YAC" (Yet Anothe ## 🚀 Features -- **Minimal & Fast**: ~760 lines total (380 Rust + 380 VimScript) +- **Minimal & Fast**: ~800 lines total core codebase - **Simple Architecture**: Direct stdin/stdout communication between Vim and LSP servers +- **SSH Remote Editing**: Full SSH tunnel infrastructure for remote LSP operations - **Memory Safe**: Rust compile-time guarantees prevent crashes and memory leaks - **Auto-initialization**: LSP servers start automatically when files are opened - **Silent Error Handling**: Gracefully handles "No definition found" scenarios @@ -92,6 +93,7 @@ inoremap :LspComplete " Manual completion ### Features - **Auto-initialization**: LSP starts automatically when opening `.rs` files +- **SSH Remote Editing**: Seamless remote LSP via SSH tunnels (`scp://user@host//path/file`) - **Code Completion**: Advanced auto-completion with smart context detection - **Hover Information**: Press `K` to show documentation/type information - **Navigation**: Jump to definitions, declarations, implementations, and references @@ -100,12 +102,18 @@ inoremap :LspComplete " Manual completion ## 🏗️ Architecture -yac.vim uses a simple stdin/stdout communication architecture: +yac.vim supports both local and remote LSP operations: +**Local Mode:** ``` Vim Plugin (job_start) → JSON stdin/stdout → lsp-bridge → LSP Server (rust-analyzer) ``` +**SSH Remote Mode:** +``` +Vim → local lsp-bridge (forwarder) → SSH tunnel → remote lsp-bridge → rust-analyzer +``` + ### Process Model - Vim launches `lsp-bridge` as a child process using `job_start()` with `'mode': 'raw'` diff --git a/crates/lsp-bridge/src/handlers/call_hierarchy.rs b/crates/lsp-bridge/src/handlers/call_hierarchy.rs index 4b7872a7..5ba0a93c 100644 --- a/crates/lsp-bridge/src/handlers/call_hierarchy.rs +++ b/crates/lsp-bridge/src/handlers/call_hierarchy.rs @@ -82,7 +82,6 @@ impl CallHierarchyInfo { } } -#[derive(Clone)] pub struct CallHierarchyHandler { lsp_registry: Arc, } diff --git a/crates/lsp-bridge/src/handlers/common.rs b/crates/lsp-bridge/src/handlers/common.rs index e43a70b2..3a23c417 100644 --- a/crates/lsp-bridge/src/handlers/common.rs +++ b/crates/lsp-bridge/src/handlers/common.rs @@ -30,9 +30,35 @@ impl Location { } } +/// SSH path conversion utilities +pub fn extract_ssh_path(file_path: &str) -> (Option, String) { + if file_path.starts_with("scp://") { + // Parse scp://user@host//path/file -> (user@host, /path/file) + if let Some(rest) = file_path.strip_prefix("scp://") { + if let Some(pos) = rest.find("//") { + let ssh_host = &rest[..pos]; + let real_path = &rest[pos + 1..]; // Remove one slash, keep the leading slash + return (Some(ssh_host.to_string()), real_path.to_string()); + } + } + } + (None, file_path.to_string()) +} + +/// Convert SSH path back to scp:// format +pub fn restore_ssh_path(normal_path: &str, ssh_host: Option<&str>) -> String { + if let Some(host) = ssh_host { + format!("scp://{}//{}", host, normal_path) + } else { + normal_path.to_string() + } +} + /// Simple file path to URI conversion - used by most handlers +/// Handles SSH path extraction automatically pub fn file_path_to_uri(file_path: &str) -> Result { - let path = Path::new(file_path); + let (_, real_path) = extract_ssh_path(file_path); + let path = Path::new(&real_path); let canonical = path.canonicalize()?; Ok(format!("file://{}", canonical.display())) } diff --git a/crates/lsp-bridge/src/handlers/file_open.rs b/crates/lsp-bridge/src/handlers/file_open.rs index 61334660..aea74c3e 100644 --- a/crates/lsp-bridge/src/handlers/file_open.rs +++ b/crates/lsp-bridge/src/handlers/file_open.rs @@ -5,6 +5,8 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use vim::Handler; +use super::common::extract_ssh_path; + #[derive(Debug, Deserialize)] #[allow(dead_code)] pub struct FileOpenRequest { @@ -58,8 +60,11 @@ impl Handler for FileOpenHandler { _ctx: &mut dyn vim::VimContext, input: Self::Input, ) -> Result> { - // Open file in appropriate language server - if let Err(e) = self.lsp_registry.open_file(&input.file).await { + // Extract real file path from SSH path if needed + let (_, real_path) = extract_ssh_path(&input.file); + + // Open file in appropriate language server using real path + if let Err(e) = self.lsp_registry.open_file(&real_path).await { return Ok(Some(FileOpenResponse::error(format!( "Failed to open file: {}", e diff --git a/crates/lsp-bridge/src/handlers/file_search.rs b/crates/lsp-bridge/src/handlers/file_search.rs index 129b9100..8423445a 100644 --- a/crates/lsp-bridge/src/handlers/file_search.rs +++ b/crates/lsp-bridge/src/handlers/file_search.rs @@ -65,7 +65,6 @@ pub struct FileItem { pub score: f64, // For relevance scoring } -#[derive(Clone)] pub struct FileSearchHandler { _lsp_registry: Arc, file_cache: Arc>>, diff --git a/crates/lsp-bridge/src/handlers/goto.rs b/crates/lsp-bridge/src/handlers/goto.rs index 0d54c1a3..56082544 100644 --- a/crates/lsp-bridge/src/handlers/goto.rs +++ b/crates/lsp-bridge/src/handlers/goto.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use tracing::debug; use vim::Handler; -use super::common::Location; +use super::common::{extract_ssh_path, restore_ssh_path, Location}; // Base request structure that Vim sends #[derive(Debug, Deserialize)] @@ -45,7 +45,6 @@ impl GotoType { // Linus-style: Location 要么完整存在,要么不存在 pub type GotoResponse = Option; -#[derive(Clone)] pub struct GotoHandler { lsp_registry: Arc, goto_type: GotoType, @@ -183,9 +182,12 @@ impl Handler for GotoHandler { if let Some(lsp_location) = location_result { if let Ok(location) = Location::from_lsp_location(lsp_location) { debug!("location: {:?}", location); - ctx.ex(format!("edit {}", location.file).as_str()) - .await - .ok(); + + // SSH path handling: convert LSP response back to proper format for vim + let (ssh_host, _) = extract_ssh_path(&input.file); + let file_path = restore_ssh_path(&location.file, ssh_host.as_deref()); + + ctx.ex(format!("edit {}", file_path).as_str()).await.ok(); ctx.call_async( "cursor", vec![json!(location.line + 1), json!(location.column + 1)], diff --git a/crates/lsp-bridge/src/main.rs b/crates/lsp-bridge/src/main.rs index 72223b6d..89d1b180 100644 --- a/crates/lsp-bridge/src/main.rs +++ b/crates/lsp-bridge/src/main.rs @@ -26,6 +26,9 @@ use handlers::{ WillSaveHandler, }; +// Removed: Complex stdio-to-socket forwarder +// SSH Master mode eliminates need for socket forwarding - direct SSH stdio connection + #[tokio::main] async fn main() -> Result<(), Box> { use std::fs::OpenOptions; @@ -41,21 +44,17 @@ async fn main() -> Result<(), Box> { .open(&log_path)?; tracing_subscriber::registry() - .with( - fmt::layer() - .with_writer(log_file) - .with_ansi(false) - .with_file(true) - .with_line_number(true), - ) + .with(fmt::layer().with_writer(log_file).with_ansi(false)) .init(); - info!("lsp-bridge starting with log: {}", log_path); + info!("LSP Bridge started with PID: {}", pid); + info!("Log file: {}", log_path); - // Create shared LSP registry for multi-language support let lsp_registry = std::sync::Arc::new(LspRegistry::new()); - // Create vim client with handler + // Simplified: SSH Master mode uses direct stdio communication + // No need for complex socket forwarding - SSH handles the transport + info!("Starting lsp-bridge in stdio mode"); let mut vim = Vim::new_stdio(); // Proactively communicate log file path to Vim via call_async @@ -82,57 +81,49 @@ async fn main() -> Result<(), Box> { GotoHandler::new(lsp_registry.clone(), "goto_implementation").unwrap(); let hover_handler = HoverHandler::new(lsp_registry.clone()); let completion_handler = CompletionHandler::new(lsp_registry.clone()); - // Updated handlers using LspRegistry let references_handler = ReferencesHandler::new(lsp_registry.clone()); let inlay_hints_handler = InlayHintsHandler::new(lsp_registry.clone()); let rename_handler = RenameHandler::new(lsp_registry.clone()); - let document_symbols_handler = DocumentSymbolsHandler::new(lsp_registry.clone()); - let folding_range_handler = FoldingRangeHandler::new(lsp_registry.clone()); - let diagnostics_handler = DiagnosticsHandler::new(lsp_registry.clone()); let code_action_handler = CodeActionHandler::new(lsp_registry.clone()); let execute_command_handler = ExecuteCommandHandler::new(lsp_registry.clone()); + let document_symbols_handler = DocumentSymbolsHandler::new(lsp_registry.clone()); let call_hierarchy_handler = CallHierarchyHandler::new(lsp_registry.clone()); + let folding_range_handler = FoldingRangeHandler::new(lsp_registry.clone()); + let diagnostics_handler = DiagnosticsHandler::new(lsp_registry.clone()); - // Document lifecycle handlers - Updated - let did_save_handler = DidSaveHandler::new(lsp_registry.clone()); + // Document lifecycle handlers let did_change_handler = DidChangeHandler::new(lsp_registry.clone()); - let will_save_handler = WillSaveHandler::new(lsp_registry.clone()); let did_close_handler = DidCloseHandler::new(lsp_registry.clone()); + let did_save_handler = DidSaveHandler::new(lsp_registry.clone()); + let will_save_handler = WillSaveHandler::new(lsp_registry.clone()); - // Register handlers for all supported commands - // Core LSP functionality - Linus style: type-safe dispatch + // Register all handlers using the vim crate API vim.add_handler("file_open", file_open_handler); vim.add_handler("file_search", file_search_handler); + vim.add_handler("goto_definition", definition_handler); + vim.add_handler("goto_declaration", declaration_handler); + vim.add_handler("goto_type_definition", type_definition_handler); + vim.add_handler("goto_implementation", implementation_handler); vim.add_handler("hover", hover_handler); vim.add_handler("completion", completion_handler); vim.add_handler("references", references_handler); vim.add_handler("inlay_hints", inlay_hints_handler); vim.add_handler("rename", rename_handler); + vim.add_handler("code_action", code_action_handler); + vim.add_handler("execute_command", execute_command_handler); vim.add_handler("document_symbols", document_symbols_handler); + vim.add_handler("call_hierarchy", call_hierarchy_handler); vim.add_handler("folding_range", folding_range_handler); vim.add_handler("diagnostics", diagnostics_handler); - vim.add_handler("code_action", code_action_handler); - vim.add_handler("execute_command", execute_command_handler); - vim.add_handler("call_hierarchy_incoming", call_hierarchy_handler.clone()); - vim.add_handler("call_hierarchy_outgoing", call_hierarchy_handler); - // Notification handlers - vim.add_handler("goto_definition", definition_handler); - vim.add_handler("goto_declaration", declaration_handler); - vim.add_handler("goto_type_definition", type_definition_handler); - vim.add_handler("goto_implementation", implementation_handler); - - // Document lifecycle handlers - Updated - vim.add_handler("did_save", did_save_handler); + // Document lifecycle handlers vim.add_handler("did_change", did_change_handler); - vim.add_handler("will_save", will_save_handler); vim.add_handler("did_close", did_close_handler); + vim.add_handler("did_save", did_save_handler); + vim.add_handler("will_save", will_save_handler); - info!("vim client configured, starting message loop..."); - - // Run the vim client - this replaces the manual stdin/stdout loop + // Start the message processing loop vim.run().await?; - info!("lsp-bridge shutting down..."); Ok(()) } diff --git a/crates/vim/src/lib.rs b/crates/vim/src/lib.rs index 9b53c85c..1d6526ba 100644 --- a/crates/vim/src/lib.rs +++ b/crates/vim/src/lib.rs @@ -36,21 +36,22 @@ pub enum VimMessage { }, // Vim channel commands (client-to-vim) - /// Call vim function with response: ["call", func, args, id] + // NOTE: Per Vim channel protocol, client-to-vim commands must use negative IDs + /// Call vim function with response: ["call", func, args, id] (id must be negative) Call { func: String, args: Vec, - id: u64, + id: i64, // Negative ID for client-to-vim commands }, /// Call vim function without response: ["call", func, args] CallAsync { func: String, args: Vec, }, - /// Execute vim expression with response: ["expr", expr, id] + /// Execute vim expression with response: ["expr", expr, id] (id must be negative) Expr { expr: String, - id: u64, + id: i64, // Negative ID for client-to-vim commands }, /// Execute vim expression without response: ["expr", expr] ExprAsync { @@ -126,7 +127,7 @@ impl VimMessage { if arr.len() >= 4 { // ["call", func, args, id] - with response let id = arr[3] - .as_u64() + .as_i64() .ok_or_else(|| Error::msg("Invalid call id"))?; Ok(VimMessage::Call { func, args, id }) } else { @@ -143,7 +144,7 @@ impl VimMessage { if arr.len() >= 3 { // ["expr", expr, id] - with response let id = arr[2] - .as_u64() + .as_i64() .ok_or_else(|| Error::msg("Invalid expr id"))?; Ok(VimMessage::Expr { expr, id }) } else { @@ -369,8 +370,8 @@ impl MessageTransport for StdioTransport { pub struct Vim { transport: Box, handlers: HashMap>, - pending_calls: HashMap>, - next_id: u64, + pending_calls: HashMap>, // Uses negative IDs per Vim protocol + next_id: i64, // Generates negative IDs for client-to-vim commands } impl Vim { @@ -380,25 +381,20 @@ impl Vim { transport: Box::new(StdioTransport::new()), handlers: HashMap::new(), pending_calls: HashMap::new(), - next_id: 1, + next_id: -1, // Start with -1, decrement for each new call (-1, -2, -3, ...) } } - /// Create TCP client (placeholder for future implementation) - pub async fn new_tcp(_addr: &str) -> Result { - // TODO: Implement TCP transport - Err(Error::msg("TCP transport not yet implemented")) - } - /// Type-safe handler registration - compile-time checks pub fn add_handler(&mut self, method: &str, handler: H) { self.handlers .insert(method.to_string(), std::sync::Arc::new(handler)); } - /// Call vim function - channel command + /// Call vim function - channel command + /// Uses negative IDs per Vim channel protocol requirement pub async fn call(&mut self, func: &str, args: Vec) -> Result { - self.next_id += 1; + self.next_id -= 1; // Generate negative ID (-1, -2, -3, ...) let call_id = self.next_id; let (tx, rx) = oneshot::channel(); self.pending_calls.insert(call_id, tx); @@ -415,8 +411,9 @@ impl Vim { } /// Execute vim expression with response + /// Uses negative IDs per Vim channel protocol requirement pub async fn expr(&mut self, expr: &str) -> Result { - self.next_id += 1; + self.next_id -= 1; // Generate negative ID (-1, -2, -3, ...) let expr_id = self.next_id; let (tx, rx) = oneshot::channel(); self.pending_calls.insert(expr_id, tx); @@ -502,7 +499,7 @@ impl Vim { } Err(e) => { error!("Transport error: {}", e); - continue; + break; } } } @@ -579,7 +576,7 @@ impl Vim { /// Handle vim responses - format: [id, result] async fn handle_response(&mut self, id: i64, result: Value) -> Result<()> { // Find and remove the pending call with matching ID - if let Some(sender) = self.pending_calls.remove(&(id as u64)) { + if let Some(sender) = self.pending_calls.remove(&id) { let _ = sender.send(result); } Ok(()) @@ -712,14 +709,14 @@ mod tests { // Test vim channel command parsing // Test call with response: ["call", "func", args, id] - let json = json!(["call", "test_func", ["arg1", 42], 123]); + let json = json!(["call", "test_func", ["arg1", 42], -123]); let msg = VimMessage::parse(&json).unwrap(); match msg { VimMessage::Call { func, args, id } => { assert_eq!(func, "test_func"); assert_eq!(args, vec![json!("arg1"), json!(42)]); - assert_eq!(id, 123); + assert_eq!(id, -123); } _ => panic!("Expected Call"), } @@ -737,13 +734,13 @@ mod tests { } // Test expr with response: ["expr", "expression", id] - let json = json!(["expr", "line('$')", 456]); + let json = json!(["expr", "line('$')", -456]); let msg = VimMessage::parse(&json).unwrap(); match msg { VimMessage::Expr { expr, id } => { assert_eq!(expr, "line('$')"); - assert_eq!(id, 456); + assert_eq!(id, -456); } _ => panic!("Expected Expr"), } @@ -838,11 +835,11 @@ mod tests { let call_msg = VimMessage::Call { func: "test_func".to_string(), args: vec![json!("arg1"), json!(42)], - id: 123, + id: -123, }; let encoded = call_msg.encode(); - let expected = json!(["call", "test_func", [json!("arg1"), json!(42)], 123]); + let expected = json!(["call", "test_func", [json!("arg1"), json!(42)], -123]); assert_eq!( encoded, expected, "VimMessage::Call should encode as [\"call\", func, args, id]" @@ -864,11 +861,11 @@ mod tests { // Test expr command: ["expr", expr, id] let expr_msg = VimMessage::Expr { expr: "line('$')".to_string(), - id: 456, + id: -456, }; let encoded = expr_msg.encode(); - let expected = json!(["expr", "line('$')", 456]); + let expected = json!(["expr", "line('$')", -456]); assert_eq!( encoded, expected, "VimMessage::Expr should encode as [\"expr\", expr, id]" diff --git a/vim/autoload/yac.vim b/vim/autoload/yac.vim index c09ec548..65fb93a5 100644 --- a/vim/autoload/yac.vim +++ b/vim/autoload/yac.vim @@ -52,8 +52,9 @@ let s:completion_icons = { \ 'Event': '󱐋 ' \ } -" 简化状态管理 -let s:job = v:null +" 连接池管理 - 支持多主机并发连接 +let s:job_pool = {} " {'local': job, 'user@host1': job, 'user@host2': job, ...} +let s:current_connection_key = 'local' " 用于调试显示 let s:log_file = '' let s:hover_popup_id = -1 @@ -92,43 +93,88 @@ let s:file_search.current_page = 0 let s:file_search.has_more = v:false let s:file_search.total_count = 0 -" 启动进程 -function! yac#start() abort - if s:job != v:null && job_status(s:job) == 'run' - return +" 获取当前 buffer 应该使用的连接 key +function! s:get_connection_key() abort + if exists('b:yac_ssh_host') + return b:yac_ssh_host + else + return 'local' endif +endfunction - " 开启 channel 日志来调试(仅第一次) - if !exists('s:log_started') - " 启用调试模式时开启详细日志 +" 构建特定连接的 job 命令 +function! s:build_job_command(key) abort + if a:key == 'local' + return get(g:, 'yac_bridge_command', ['./target/release/lsp-bridge']) + else + " SSH 连接命令,使用 ControlPersist 优化 + let l:control_path = '/tmp/yac-' . substitute(a:key, '[^a-zA-Z0-9]', '_', 'g') . '.sock' + return ['ssh', + \ '-o', 'ControlPath=' . l:control_path, + \ '-o', 'ControlMaster=auto', + \ '-o', 'ControlPersist=10m', + \ a:key, './lsp-bridge'] + endif +endfunction + +" 确保对应连接的 job 存在并运行 +function! s:ensure_job() abort + let l:key = s:get_connection_key() + let s:current_connection_key = l:key + + " 检查连接池中是否有有效的 job + if !has_key(s:job_pool, l:key) || job_status(s:job_pool[l:key]) != 'run' + " 开启 channel 日志(仅第一次) + if !exists('s:log_started') + if get(g:, 'lsp_bridge_debug', 0) + call ch_logfile('/tmp/vim_channel.log', 'w') + echom 'YacDebug: Channel logging enabled to /tmp/vim_channel.log' + endif + let s:log_started = 1 + endif + + " 创建新的 job + let l:cmd = s:build_job_command(l:key) + if get(g:, 'lsp_bridge_debug', 0) - call ch_logfile('/tmp/vim_channel.log', 'w') - echom 'YacDebug: Channel logging enabled to /tmp/vim_channel.log' + echom printf('YacDebug: Creating new connection [%s]: %s', l:key, string(l:cmd)) + endif + + let s:job_pool[l:key] = job_start(l:cmd, { + \ 'mode': 'json', + \ 'callback': function('s:handle_response'), + \ 'err_cb': function('s:handle_error'), + \ 'exit_cb': function('s:handle_exit', [l:key]) + \ }) + + if job_status(s:job_pool[l:key]) != 'run' + echoerr printf('Failed to start lsp-bridge for %s', l:key) + if has_key(s:job_pool, l:key) + unlet s:job_pool[l:key] + endif + return v:null endif - let s:log_started = 1 endif + + return s:job_pool[l:key] +endfunction - let s:job = job_start(g:yac_bridge_command, { - \ 'mode': 'json', - \ 'callback': function('s:handle_response'), - \ 'err_cb': function('s:handle_error'), - \ 'exit_cb': function('s:handle_exit') - \ }) - - if job_status(s:job) != 'run' - echoerr 'Failed to start lsp-bridge' - endif +" 启动进程 - 现在使用连接池 +function! yac#start() abort + " 通过 ensure_job 自动管理连接 + return s:ensure_job() != v:null endfunction " 发送命令(使用 ch_sendexpr 和指定的回调handler) function! s:send_command(jsonrpc_msg, callback_func) abort - call yac#start() " 自动启动 - - if s:job != v:null && job_status(s:job) == 'run' + let l:job = s:ensure_job() + + if l:job != v:null && job_status(l:job) == 'run' " 调试模式:记录发送的命令 if get(g:, 'lsp_bridge_debug', 0) let params = get(a:jsonrpc_msg, 'params', {}) - echom printf('YacDebug[SEND]: %s -> %s:%d:%d', + echom printf('YacDebug[SEND][%s]: %s -> %s:%d:%d', + \ s:current_connection_key, \ a:jsonrpc_msg.method, \ fnamemodify(get(params, 'file', ''), ':t'), \ get(params, 'line', -1), get(params, 'column', -1)) @@ -136,9 +182,9 @@ function! s:send_command(jsonrpc_msg, callback_func) abort endif " 使用指定的回调函数 - call ch_sendexpr(s:job, a:jsonrpc_msg, {'callback': a:callback_func}) + call ch_sendexpr(l:job, a:jsonrpc_msg, {'callback': a:callback_func}) else - echoerr 'lsp-bridge not running' + echoerr printf('lsp-bridge not running for %s', s:get_connection_key()) endif endfunction @@ -150,13 +196,14 @@ function! s:request(method, params, callback_func) abort \ 'method': a:method, \ 'params': extend(a:params, {'command': a:method}) \ } - - call yac#start() " 自动启动 - if s:job != v:null && job_status(s:job) == 'run' + let l:job = s:ensure_job() + + if l:job != v:null && job_status(l:job) == 'run' " 调试模式:记录发送的请求 if get(g:, 'lsp_bridge_debug', 0) - echom printf('YacDebug[SEND]: %s -> %s:%d:%d', + echom printf('YacDebug[SEND][%s]: %s -> %s:%d:%d', + \ s:current_connection_key, \ a:method, \ fnamemodify(get(a:params, 'file', ''), ':t'), \ get(a:params, 'line', -1), get(a:params, 'column', -1)) @@ -164,25 +211,26 @@ function! s:request(method, params, callback_func) abort endif " 使用指定的回调函数 - call ch_sendexpr(s:job, jsonrpc_msg, {'callback': a:callback_func}) + call ch_sendexpr(l:job, jsonrpc_msg, {'callback': a:callback_func}) else - echoerr 'lsp-bridge not running' + echoerr printf('lsp-bridge not running for %s', s:get_connection_key()) endif endfunction -" Notification - fire and forget, clear semantics +" Notification - fire and forget, clear semantics function! s:notify(method, params) abort let jsonrpc_msg = { \ 'method': a:method, \ 'params': extend(a:params, {'command': a:method}) \ } - - call yac#start() " 自动启动 - if s:job != v:null && job_status(s:job) == 'run' + let l:job = s:ensure_job() + + if l:job != v:null && job_status(l:job) == 'run' " 调试模式:记录发送的通知 if get(g:, 'lsp_bridge_debug', 0) - echom printf('YacDebug[NOTIFY]: %s -> %s:%d:%d', + echom printf('YacDebug[NOTIFY][%s]: %s -> %s:%d:%d', + \ s:current_connection_key, \ a:method, \ fnamemodify(get(a:params, 'file', ''), ':t'), \ get(a:params, 'line', -1), get(a:params, 'column', -1)) @@ -190,9 +238,9 @@ function! s:notify(method, params) abort endif " 发送通知(不需要回调) - call ch_sendraw(s:job, json_encode([jsonrpc_msg]) . "\n") + call ch_sendraw(l:job, json_encode([jsonrpc_msg]) . "\n") else - echoerr 'lsp-bridge not running' + echoerr printf('lsp-bridge not running for %s', s:get_connection_key()) endif endfunction @@ -237,6 +285,8 @@ function! yac#hover() abort \ }, 's:handle_hover_response') endfunction +" Helper functions removed - now handled by connection pool architecture + function! yac#open_file() abort call s:request('file_open', { \ 'file': expand('%:p'), @@ -252,7 +302,7 @@ function! yac#complete() abort let line = getline('.') let col = col('.') - 1 let triggers = get(g:, 'yac_auto_complete_triggers', ['.', ':', '::']) - + let needs_new_request = 0 for trigger in triggers if col >= len(trigger) && line[col - len(trigger):col - 1] == trigger @@ -260,12 +310,12 @@ function! yac#complete() abort break endif endfor - + if !needs_new_request call s:filter_completions() return endif - + " 关闭现有窗口,将进行新的LSP请求 call s:close_completion_popup() endif @@ -409,7 +459,7 @@ function! yac#auto_complete_trigger() abort let line = getline('.') let col = col('.') - 1 let triggers = get(g:, 'yac_auto_complete_triggers', ['.', ':', '::']) - + let needs_new_request = 0 for trigger in triggers if col >= len(trigger) && line[col - len(trigger):col - 1] == trigger @@ -417,12 +467,12 @@ function! yac#auto_complete_trigger() abort break endif endfor - + if !needs_new_request call s:filter_completions() return endif - + " 关闭现有窗口,将进行新的LSP请求 call s:close_completion_popup() endif @@ -435,7 +485,7 @@ function! yac#auto_complete_trigger() abort " 获取当前行和光标位置 let current_line = getline('.') let col = col('.') - 1 - + " 避免在字符串或注释中触发 if s:in_string_or_comment() return @@ -443,21 +493,21 @@ function! yac#auto_complete_trigger() abort " 获取当前词前缀 let prefix = s:get_current_word_prefix() - + " 检查最小字符数要求 let min_chars = get(g:, 'yac_auto_complete_min_chars', 2) if len(prefix) < min_chars " 检查是否有触发字符 let triggers = get(g:, 'yac_auto_complete_triggers', ['.', ':', '::']) let should_trigger = 0 - + for trigger in triggers if col >= len(trigger) && current_line[col - len(trigger):col - 1] == trigger let should_trigger = 1 break endif endfor - + if !should_trigger return endif @@ -510,7 +560,7 @@ endfunction function! yac#file_search(...) abort " 获取查询字符串(可选参数) let query = a:0 > 0 ? a:1 : '' - + " 如果没有提供查询字符串,使用交互式输入 if empty(query) call s:start_interactive_file_search() @@ -533,7 +583,7 @@ function! s:start_interactive_file_search() abort let s:file_search.current_page = 0 let s:file_search.files = [] let s:file_search.selected = 0 - + " 显示初始搜索(所有文件) call s:request('file_search', { \ 'query': '', @@ -571,15 +621,15 @@ function! s:show_interactive_file_search() abort " 计算窗口尺寸 let max_width = min([s:FILE_SEARCH_MAX_WIDTH, &columns - 4]) let max_height = min([s:FILE_SEARCH_MAX_HEIGHT, &lines - 6]) - + " 准备显示内容 let display_lines = [] - + " 添加搜索提示 call add(display_lines, 'Type to search files (ESC to cancel, Enter to open):') call add(display_lines, 'Query: ' . s:file_search.query . '█') call add(display_lines, repeat('─', max_width - 2)) - + " 添加文件列表 if empty(s:file_search.files) call add(display_lines, 'No files found') @@ -589,20 +639,20 @@ function! s:show_interactive_file_search() abort let file = s:file_search.files[i] let marker = (i == s:file_search.selected) ? '▶ ' : ' ' let relative_path = has_key(file, 'relative_path') ? file.relative_path : fnamemodify(file.path, ':.') - + " 截断过长路径 if len(relative_path) > max_width - 6 let relative_path = '...' . relative_path[-(max_width-9):] endif - + call add(display_lines, marker . relative_path) endfor endif - + " 添加状态信息 if len(s:file_search.files) > 0 - let status = printf('Showing %d/%d files', - \ min([len(s:file_search.files), max_height - 6]), + let status = printf('Showing %d/%d files', + \ min([len(s:file_search.files), max_height - 6]), \ s:file_search.total_count) call add(display_lines, repeat('─', max_width - 2)) call add(display_lines, status) @@ -612,7 +662,7 @@ function! s:show_interactive_file_search() abort if s:file_search.popup_id != -1 && exists('*popup_close') call popup_close(s:file_search.popup_id) endif - + let s:file_search.popup_id = popup_create(display_lines, { \ 'title': ' File Search ', \ 'line': 3, @@ -671,7 +721,7 @@ function! s:interactive_file_search_filter(winid, key) abort call s:update_file_search_with_query() return 1 endif - + return 0 endfunction @@ -679,7 +729,7 @@ endfunction function! s:update_file_search_with_query() abort let s:file_search.current_page = 0 let s:file_search.selected = 0 - + call s:request('file_search', { \ 'query': s:file_search.query, \ 'page': 0, @@ -726,13 +776,14 @@ endfunction " 发送通知(无响应) function! s:send_notification(jsonrpc_msg) abort - call yac#start() " 自动启动 + let l:job = s:ensure_job() - if s:job != v:null && job_status(s:job) == 'run' + if l:job != v:null && job_status(l:job) == 'run' " 调试模式:记录发送的通知 if get(g:, 'lsp_bridge_debug', 0) let params = get(a:jsonrpc_msg, 'params', {}) - echom printf('YacDebug[NOTIFY]: %s -> %s:%d:%d', + echom printf('YacDebug[NOTIFY][%s]: %s -> %s:%d:%d', + \ s:current_connection_key, \ a:jsonrpc_msg.method, \ fnamemodify(get(params, 'file', ''), ':t'), \ get(params, 'line', -1), get(params, 'column', -1)) @@ -740,7 +791,7 @@ function! s:send_notification(jsonrpc_msg) abort endif " 发送通知(不需要回调) - call ch_sendraw(s:job, json_encode([a:jsonrpc_msg]) . "\n") + call ch_sendraw(l:job, json_encode([a:jsonrpc_msg]) . "\n") else echoerr 'lsp-bridge not running' endif @@ -764,7 +815,7 @@ endfunction function! s:in_string_or_comment() abort " 获取当前位置的语法高亮组 let synname = synIDattr(synID(line('.'), col('.'), 1), 'name') - + " 检查是否为字符串或注释的语法组 return synname =~? 'comment\|string\|char' endfunction @@ -929,10 +980,20 @@ function! s:handle_error(channel, msg) abort echoerr 'lsp-bridge: ' . a:msg endfunction -" 处理进程退出(异步回调) -function! s:handle_exit(job, status) abort - echom 'lsp-bridge exited with status: ' . a:status - let s:job = v:null +" 处理进程退出(异步回调) - 支持连接池 +function! s:handle_exit(key, job, status) abort + if a:status != 0 + echohl ErrorMsg + echo printf('LSP connection to %s failed (exit: %d)', a:key, a:status) + echohl None + else + echom printf('LSP connection to %s closed', a:key) + endif + + " 从连接池中移除失败的连接 + if has_key(s:job_pool, a:key) + unlet s:job_pool[a:key] + endif endfunction " Channel回调,只处理服务器主动推送的通知 @@ -961,17 +1022,35 @@ function! yac#set_log_file(log_path) abort endif endfunction -" 停止进程 +" 停止进程 - 支持连接池 function! yac#stop() abort - if s:job != v:null - if get(g:, 'lsp_bridge_debug', 0) - echom 'YacDebug: Stopping lsp-bridge process' + let l:key = s:get_connection_key() + + if has_key(s:job_pool, l:key) + let l:job = s:job_pool[l:key] + if job_status(l:job) == 'run' + if get(g:, 'lsp_bridge_debug', 0) + echom printf('YacDebug: Stopping lsp-bridge process for %s', l:key) + endif + call job_stop(l:job) endif - call job_stop(s:job) - let s:job = v:null + unlet s:job_pool[l:key] endif endfunction +" 停止所有连接 +function! yac#stop_all() abort + for [key, job] in items(s:job_pool) + if job_status(job) == 'run' + if get(g:, 'lsp_bridge_debug', 0) + echom printf('YacDebug: Stopping lsp-bridge process for %s', key) + endif + call job_stop(job) + endif + endfor + let s:job_pool = {} +endfunction + " === Debug 功能 === " 切换调试模式 @@ -984,11 +1063,11 @@ function! yac#debug_toggle() abort echo ' - Channel communication will be logged to /tmp/vim_channel.log' echo ' - Use :YacDebugToggle to disable' - " 如果进程已经运行,重启以启用channel日志 - if s:job != v:null && job_status(s:job) == 'run' - echom 'YacDebug: Restarting process to enable channel logging...' - call yac#stop() - call yac#start() + " 如果有活跃的连接,重启以启用channel日志 + if !empty(s:job_pool) + echom 'YacDebug: Restarting connections to enable channel logging...' + call yac#stop_all() + " 下次调用 LSP 命令时会自动重新启动 endif else echo 'YacDebug: Debug mode DISABLED' @@ -1000,20 +1079,77 @@ endfunction " 显示调试状态 function! yac#debug_status() abort let debug_enabled = get(g:, 'lsp_bridge_debug', 0) - let job_running = (s:job != v:null && job_status(s:job) == 'run') - + let active_connections = len(s:job_pool) + let current_key = s:get_connection_key() + echo 'YacDebug Status:' echo ' Debug Mode: ' . (debug_enabled ? 'ENABLED' : 'DISABLED') - echo ' LSP Process: ' . (job_running ? 'RUNNING' : 'STOPPED') + echo printf(' Active Connections: %d', active_connections) + echo printf(' Current Buffer: %s', current_key) + + if active_connections > 0 + echo ' Connection Details:' + for [key, job] in items(s:job_pool) + let status = job_status(job) + echo printf(' %s: %s', key, status) + endfor + endif + echo ' Channel Log: /tmp/vim_channel.log' . (debug_enabled ? ' (enabled)' : ' (disabled for new connections)') echo ' LSP Log: ' . (empty(s:log_file) ? 'Not available' : s:log_file) echo '' echo 'Commands:' echo ' :YacDebugToggle - Toggle debug mode' echo ' :YacDebugStatus - Show this status' + echo ' :YacConnections - Show connection details' echo ' :YacOpenLog - Open LSP process log' endfunction +" 连接管理功能 +function! yac#connections() abort + if empty(s:job_pool) + echo 'No active LSP connections' + return + endif + + echo 'Active LSP Connections:' + echo '========================' + for [key, job] in items(s:job_pool) + let status = job_status(job) + let job_info = job_info(job) + let pid = has_key(job_info, 'process') ? job_info.process : 'unknown' + let is_current = (key == s:get_connection_key()) ? ' (current)' : '' + echo printf(' %s: %s (PID: %s)%s', key, status, pid, is_current) + endfor + + echo '' + echo printf('Current buffer connection: %s', s:get_connection_key()) +endfunction + +" 自动清理死连接 +function! s:cleanup_dead_connections() abort + let dead_keys = [] + for [key, job] in items(s:job_pool) + if job_status(job) != 'run' + call add(dead_keys, key) + endif + endfor + + for key in dead_keys + if get(g:, 'lsp_bridge_debug', 0) + echom printf('YacDebug: Removing dead connection: %s', key) + endif + unlet s:job_pool[key] + endfor + + return len(dead_keys) +endfunction + +" 手动清理命令 +function! yac#cleanup_connections() abort + let cleaned = s:cleanup_dead_connections() + echo printf('Cleaned up %d dead connections', cleaned) +endfunction " 显示补全结果 function! s:show_completions(items) abort @@ -1114,17 +1250,17 @@ endfunction function! s:format_completion_item(item, marker) abort " 获取图标 let icon = get(s:completion_icons, a:item.kind, '󰉿 ') - + " 基础显示格式 let display = a:marker . icon . a:item.label - + " 添加类型信息(如果存在) if has_key(a:item, 'detail') && !empty(a:item.detail) let display .= ' ' . a:item.detail else let display .= ' (' . a:item.kind . ')' endif - + return display endfunction @@ -1153,20 +1289,20 @@ function! s:fuzzy_match_score(text, pattern) abort if empty(a:pattern) return 1000 " 空模式匹配所有项目,给高分 endif - + let text = tolower(a:text) let pattern = tolower(a:pattern) - + " 精确前缀匹配 - 最高优先级 if text =~# '^' . escape(pattern, '[]^$.*\~') return 2000 + (1000 - len(a:text)) " 越短的匹配越好 endif - + " 连续子序列匹配 let idx = 0 let match_positions = [] let last_pos = -1 - + for char in split(pattern, '\zs') let pos = stridx(text, char, idx) if pos == -1 @@ -1176,29 +1312,29 @@ function! s:fuzzy_match_score(text, pattern) abort let idx = pos + 1 let last_pos = pos endfor - + " 计算评分:基于匹配位置和连续性 let score = 1000 - + " 首字符匹配加分 if match_positions[0] == 0 let score += 500 endif - + " 连续匹配加分 for i in range(1, len(match_positions) - 1) if match_positions[i] == match_positions[i-1] + 1 let score += 100 endif endfor - + " 匹配密度加分(匹配字符占总长度比例) let density = len(pattern) * 100 / len(a:text) let score += density - + " 总长度短的优先(相同匹配情况下) let score -= len(a:text) - + return score endfunction @@ -1218,7 +1354,7 @@ function! s:filter_completions() abort " 按评分排序(降序) call sort(scored_items, {a, b -> b.score - a.score}) - + " 提取排序后的项目 let s:completion.items = [] for scored in scored_items @@ -1504,16 +1640,19 @@ endfunction " 简单打开日志文件 function! yac#open_log() abort - " 检查LSP bridge进程是否运行 - if s:job == v:null || job_status(s:job) != 'run' - echo 'lsp-bridge not running' + " 检查当前 buffer 的 LSP 连接是否运行 + let l:key = s:get_connection_key() + if !has_key(s:job_pool, l:key) || job_status(s:job_pool[l:key]) != 'run' + echo printf('lsp-bridge not running for %s', l:key) return endif + let l:job = s:job_pool[l:key] + " 如果s:log_file未设置,根据进程PID构造日志文件路径 let log_file = s:log_file if empty(log_file) - let job_info = job_info(s:job) + let job_info = job_info(l:job) if has_key(job_info, 'process') && job_info.process > 0 let log_file = '/tmp/lsp-bridge-' . job_info.process . '.log' else @@ -2163,7 +2302,7 @@ endfunction function! s:find_workspace_root() abort let project_files = ['Cargo.toml', 'package.json', '.git', 'pyproject.toml', 'go.mod', 'pom.xml', 'build.gradle', 'Makefile', 'CMakeLists.txt'] let current_dir = expand('%:p:h') - + while current_dir != '/' && current_dir != '' for project_file in project_files if filereadable(current_dir . '/' . project_file) || isdirectory(current_dir . '/' . project_file) @@ -2172,7 +2311,7 @@ function! s:find_workspace_root() abort endfor let current_dir = fnamemodify(current_dir, ':h') endwhile - + " 如果没有找到项目根,使用当前目录 return expand('%:p:h') endfunction @@ -2184,17 +2323,17 @@ function! s:show_file_search_popup() abort call popup_close(s:file_search.popup_id) let s:file_search.popup_id = -1 endif - + if s:file_search.input_popup_id != -1 && exists('*popup_close') call popup_close(s:file_search.input_popup_id) let s:file_search.input_popup_id = -1 endif - + if empty(s:file_search.files) echo "No files found" return endif - + " Debug: 打印文件数据结构 if get(g:, 'lsp_bridge_debug', 0) echom printf('YacDebug: Building display for %d files', len(s:file_search.files)) @@ -2202,15 +2341,15 @@ function! s:show_file_search_popup() abort echom printf('YacDebug: First file structure: %s', string(s:file_search.files[0])) endif endif - + " 准备显示的文件列表 let display_lines = [] let max_width = s:FILE_SEARCH_MAX_WIDTH - + for i in range(len(s:file_search.files)) let file = s:file_search.files[i] let marker = (i == s:file_search.selected) ? '▶ ' : ' ' - + " 显示相对路径,截断过长的路径 " 安全访问relative_path字段 if type(file) == type({}) && has_key(file, 'relative_path') @@ -2223,14 +2362,14 @@ function! s:show_file_search_popup() abort let display_path = string(file) endif endif - + if len(display_path) > max_width - 4 let display_path = '...' . display_path[-(max_width-7):] endif - + call add(display_lines, marker . display_path) endfor - + " Debug: 打印display_lines if get(g:, 'lsp_bridge_debug', 0) echom printf('YacDebug: Built %d display lines', len(display_lines)) @@ -2238,9 +2377,9 @@ function! s:show_file_search_popup() abort echom printf('YacDebug: First display line: "%s"', display_lines[0]) endif endif - + " 添加状态行 - let status = printf('Page %d/%d - %d files total', + let status = printf('Page %d/%d - %d files total', \ s:file_search.current_page + 1, \ (s:file_search.total_count + s:FILE_SEARCH_PAGE_SIZE - 1) / s:FILE_SEARCH_PAGE_SIZE, \ s:file_search.total_count) @@ -2249,18 +2388,18 @@ function! s:show_file_search_popup() abort endif call add(display_lines, '') call add(display_lines, status) - + " 确保我们有内容显示 if empty(display_lines) call add(display_lines, "No files to display") endif - + " 最终调试:显示即将用于popup的完整内容 if get(g:, 'lsp_bridge_debug', 0) echom printf('YacDebug: Final display_lines count: %d', len(display_lines)) echom printf('YacDebug: Creating popup with content: %s', string(display_lines)) endif - + if exists('*popup_create') " 使用 Vim 8.1+ popup let s:file_search.popup_id = popup_create(display_lines, { @@ -2277,15 +2416,15 @@ function! s:show_file_search_popup() abort \ 'cursorline': 1, \ 'mapping': 0 \ }) - + " Debug: 确认popup创建 if get(g:, 'lsp_bridge_debug', 0) echom printf('YacDebug: Popup created with ID: %d', s:file_search.popup_id) endif - + " 创建输入框 call s:show_file_search_input() - + " 确保主 popup 获得焦点以处理键盘输入 if exists('*popup_setoptions') call popup_setoptions(s:file_search.popup_id, {'cursorline': 1}) @@ -2302,7 +2441,7 @@ function! s:show_file_search_input() abort if !exists('*popup_create') return endif - + let s:file_search.input_popup_id = popup_create(['Search: ' . s:file_search.query], { \ 'line': 3, \ 'col': (&columns - 60) / 2, @@ -2321,7 +2460,7 @@ function! s:file_search_filter(winid, key) abort call s:move_file_search_selection(1) return 1 elseif a:key == "\" || a:key == "\" - call s:move_file_search_selection(-1) + call s:move_file_search_selection(-1) return 1 " 回车选择文件 elseif a:key == "\" @@ -2339,7 +2478,7 @@ function! s:file_search_filter(winid, key) abort elseif a:key == "\" || a:key == "\" call s:load_next_file_search_page() return 1 - elseif a:key == "\" || a:key == "\" + elseif a:key == "\" || a:key == "\" call s:load_prev_file_search_page() return 1 " 字母数字键用于搜索 @@ -2357,7 +2496,7 @@ function! s:file_search_filter(winid, key) abort call s:update_file_search_query('') return 1 endif - + return 0 endfunction @@ -2366,25 +2505,25 @@ function! s:update_interactive_file_search_display() abort if s:file_search.popup_id == -1 return endif - + " 计算窗口尺寸 let max_width = min([s:FILE_SEARCH_MAX_WIDTH, &columns - 4]) let max_height = min([s:FILE_SEARCH_MAX_HEIGHT, &lines - 6]) - + " 准备显示内容 let display_lines = [] - + " 添加搜索提示 call add(display_lines, 'Type to search files (ESC to cancel, Enter to open):') call add(display_lines, 'Query: ' . s:file_search.query . '█') call add(display_lines, repeat('─', max_width - 2)) - + " Calculate scrolling window parameters first let available_lines = max_height - 6 " Reserve space for header, status let total_files = len(s:file_search.files) let selected_idx = s:file_search.selected let scroll_offset = 0 - + " 添加文件列表 with scrolling support if empty(s:file_search.files) call add(display_lines, 'No files found') @@ -2405,41 +2544,41 @@ function! s:update_interactive_file_search_display() abort endif endif endif - + " Display files in the visible window let end_idx = min([scroll_offset + available_lines, total_files]) for i in range(scroll_offset, end_idx - 1) let file = s:file_search.files[i] let marker = (i == s:file_search.selected) ? '▶ ' : ' ' let relative_path = has_key(file, 'relative_path') ? file.relative_path : fnamemodify(file.path, ':.') - + " 截断过长路径 if len(relative_path) > max_width - 6 let relative_path = '...' . relative_path[-(max_width-9):] endif - + call add(display_lines, marker . relative_path) endfor endif - + " 添加状态信息 with scroll indicator if len(s:file_search.files) > 0 let visible_count = min([len(s:file_search.files), available_lines]) let status = printf('Showing %d/%d files', visible_count, s:file_search.total_count) - + " Add scroll indicators if there are more files if total_files > available_lines let scroll_info = printf(' [%d-%d]', scroll_offset + 1, min([scroll_offset + available_lines, total_files])) let status .= scroll_info endif - + call add(display_lines, repeat('─', max_width - 2)) call add(display_lines, status) endif " 更新现有popup的内容,保持filter函数连接 call popup_settext(s:file_search.popup_id, display_lines) - + " Set cursor position to highlight selected item in popup if exists('*popup_setoptions') && len(s:file_search.files) > 0 " Calculate the line number of selected item within the popup content (1-indexed) @@ -2455,16 +2594,16 @@ endfunction " 移动文件搜索选择 function! s:move_file_search_selection(direction) abort let new_selected = s:file_search.selected + a:direction - + " 边界检查 if new_selected < 0 let new_selected = 0 elseif new_selected >= len(s:file_search.files) let new_selected = len(s:file_search.files) - 1 endif - + let s:file_search.selected = new_selected - + " 始终使用交互式显示更新 - 使用settext避免重新创建popup " 这样popup窗口位置保持稳定,只更新内容 call s:update_interactive_file_search_display() @@ -2475,13 +2614,13 @@ function! s:update_file_search_display() abort if s:file_search.popup_id == -1 return endif - + " Calculate display window size let max_width = s:FILE_SEARCH_MAX_WIDTH let max_display_lines = s:FILE_SEARCH_WINDOW_SIZE " Use the configured window size let total_files = len(s:file_search.files) let selected_idx = s:file_search.selected - + " Calculate scroll offset to keep selection visible let scroll_offset = 0 if total_files > max_display_lines @@ -2495,24 +2634,24 @@ function! s:update_file_search_display() abort endif endif endif - + " 重新准备显示行 let display_lines = [] - + " Display files in visible window let end_idx = min([scroll_offset + max_display_lines, total_files]) for i in range(scroll_offset, end_idx - 1) let file = s:file_search.files[i] let marker = (i == s:file_search.selected) ? '▶ ' : ' ' - + let display_path = has_key(file, 'relative_path') ? file.relative_path : fnamemodify(file.path, ':.') if len(display_path) > max_width - 4 let display_path = '...' . display_path[-(max_width-7):] endif - + call add(display_lines, marker . display_path) endfor - + " 状态行 with scroll info let status = printf('Page %d/%d - %d files total', \ s:file_search.current_page + 1, @@ -2521,19 +2660,19 @@ function! s:update_file_search_display() abort if s:file_search.has_more let status .= ' (more available)' endif - + " Add scroll indicator if scrolling if total_files > max_display_lines let scroll_info = printf(' [%d-%d]', scroll_offset + 1, min([scroll_offset + max_display_lines, total_files])) let status .= scroll_info endif - + call add(display_lines, '') call add(display_lines, status) - + " 更新popup内容 call popup_settext(s:file_search.popup_id, display_lines) - + " 更新输入框 if s:file_search.input_popup_id != -1 call popup_settext(s:file_search.input_popup_id, ['Search: ' . s:file_search.query]) @@ -2544,7 +2683,7 @@ endfunction function! s:update_file_search_query(new_query) abort let s:file_search.query = a:new_query let s:file_search.current_page = 0 - + " 发送新的搜索请求 call s:request('file_search', { \ 'query': a:new_query, @@ -2559,7 +2698,7 @@ function! s:load_next_file_search_page() abort if !s:file_search.has_more return endif - + let next_page = s:file_search.current_page + 1 call s:request('file_search', { \ 'query': s:file_search.query, @@ -2569,12 +2708,12 @@ function! s:load_next_file_search_page() abort \ }, 's:handle_file_search_response') endfunction -" 加载上一页文件搜索结果 +" 加载上一页文件搜索结果 function! s:load_prev_file_search_page() abort if s:file_search.current_page <= 0 return endif - + let prev_page = s:file_search.current_page - 1 call s:request('file_search', { \ 'query': s:file_search.query, @@ -2589,11 +2728,11 @@ function! s:open_selected_file() abort if empty(s:file_search.files) || s:file_search.selected >= len(s:file_search.files) return endif - + let selected_file = s:file_search.files[s:file_search.selected] - + call s:close_file_search_popup() - + " 记录选择的文件到历史中(发送到 Rust 后端,同步请求确保完成) call s:request('file_search', { \ 'selected_file': selected_file.relative_path, @@ -2601,7 +2740,7 @@ function! s:open_selected_file() abort \ 'page': 0, \ 'page_size': 1 \ }, 's:handle_recent_file_response') - + " 打开文件 execute 'edit ' . fnameescape(selected_file.path) echo 'Opened: ' . selected_file.relative_path @@ -2620,12 +2759,12 @@ function! s:close_file_search_popup() abort call popup_close(s:file_search.popup_id) let s:file_search.popup_id = -1 endif - + if s:file_search.input_popup_id != -1 && exists('*popup_close') call popup_close(s:file_search.input_popup_id) let s:file_search.input_popup_id = -1 endif - + " 重置状态 let s:file_search.files = [] let s:file_search.selected = 0 @@ -2639,13 +2778,13 @@ endfunction function! s:file_search_callback(id, result) abort " Reset search state without calling popup_close (to avoid recursion) let s:file_search.popup_id = -1 - + " Close input popup if it exists if s:file_search.input_popup_id != -1 && exists('*popup_close') call popup_close(s:file_search.input_popup_id) let s:file_search.input_popup_id = -1 endif - + " Reset state let s:file_search.files = [] let s:file_search.selected = 0 @@ -2659,13 +2798,19 @@ endfunction function! s:file_search_command_line_interface() abort echo "File search (command line mode):" echo "Use :YacFileSearch to search files" - + for i in range(min([10, len(s:file_search.files)])) let file = s:file_search.files[i] echo printf("[%d] %s", i+1, file.relative_path) endfor - + if len(s:file_search.files) > 10 echo printf("... and %d more files", len(s:file_search.files) - 10) endif endfunction + +" 启动定时清理任务 +if !exists('s:cleanup_timer') + " 每5分钟清理一次死连接 + let s:cleanup_timer = timer_start(300000, {-> s:cleanup_dead_connections()}, {'repeat': -1}) +endif diff --git a/vim/autoload/yac_remote.vim b/vim/autoload/yac_remote.vim new file mode 100644 index 00000000..f10cefd2 --- /dev/null +++ b/vim/autoload/yac_remote.vim @@ -0,0 +1,107 @@ +" yac_remote.vim - SSH Master architecture for remote editing +" Uses SSH ControlMaster for direct stdio connection to remote lsp-bridge + +if exists('g:loaded_yac_remote') + finish +endif +let g:loaded_yac_remote = 1 + +" Enhanced smart LSP start that detects SSH files +function! yac_remote#enhanced_lsp_start() abort + let l:filepath = expand('%:p') + + " Check if this is an SSH file (scp:// or ssh:// protocol) + if l:filepath =~# '^s\(cp\|sh\)://' + echo "SSH file detected: " . l:filepath + call s:start_ssh_master_mode(l:filepath) + else + " Local file - use standard mode + call yac#start() + call yac#open_file() + endif + + return 1 +endfunction + +" Start SSH Master mode - simplified without blocking +function! s:start_ssh_master_mode(ssh_path) abort + " Parse SSH path: scp://user@host//path/file + let [l:user_host, l:remote_path] = s:parse_ssh_path(a:ssh_path) + + " Deploy lsp-bridge binary if needed + call s:ensure_remote_binary(l:user_host) + + echo printf("Starting remote LSP for %s...", l:user_host) + + " Set up buffer variables for connection pool + let b:yac_original_ssh_path = a:ssh_path + let b:yac_real_path_for_lsp = l:remote_path + let b:yac_ssh_host = l:user_host + + " 连接池会自动管理 SSH 连接,无需手动创建 Master + " Start LSP - connection pool will handle SSH automatically + if yac#start() + call yac#open_file() + return 1 + else + echoerr printf("Failed to start remote LSP for %s", l:user_host) + return 0 + endif +endfunction + +" Parse SSH path into user@host and remote path +function! s:parse_ssh_path(ssh_path) abort + let l:match = matchlist(a:ssh_path, '^s\(cp\|sh\)://\([^@]\+@[^/]\+\)\(//\?\(.*\)\)') + if empty(l:match) + echoerr "Invalid SSH path format: " . a:ssh_path + return ['', ''] + endif + + let l:user_host = l:match[2] + let l:remote_path = l:match[4] + if l:remote_path !~# '^/' + let l:remote_path = '/' . l:remote_path + endif + + return [l:user_host, l:remote_path] +endfunction + +" Deploy lsp-bridge binary to remote host (simplified) +function! s:ensure_remote_binary(user_host) abort + " Check if already exists + if system(printf('ssh %s "test -x ./lsp-bridge"', shellescape(a:user_host))) == 0 + return 1 + endif + + " Build if needed + if !filereadable('./target/release/lsp-bridge') + echo "Building lsp-bridge..." + call system('cargo build --release') + endif + + " Deploy + echo "Deploying to " . a:user_host . "..." + call system(printf('scp ./target/release/lsp-bridge %s:lsp-bridge', shellescape(a:user_host))) + call system(printf('ssh %s "chmod +x lsp-bridge"', shellescape(a:user_host))) + + return 1 +endfunction + +" Get file path for LSP operations - returns converted path for SSH files +function! yac_remote#get_lsp_file_path() abort + return exists('b:yac_real_path_for_lsp') ? b:yac_real_path_for_lsp : expand('%:p') +endfunction + +" Note: get_job_command removed - now handled by connection pool in yac.vim + +" Cleanup command for remote connections - now delegates to connection pool +function! yac_remote#cleanup() abort + echo "Cleaning up remote LSP connections..." + + " Use the connection pool's cleanup + call yac#stop_all() + + " Clean up any stale socket files + call system('rm -f /tmp/yac-*.sock') + echo "Remote LSP cleanup complete" +endfunction diff --git a/vim/plugin/yac.vim b/vim/plugin/yac.vim index 009d30f2..634c5d89 100644 --- a/vim/plugin/yac.vim +++ b/vim/plugin/yac.vim @@ -43,7 +43,12 @@ command! YacToggleDiagnosticVirtualText call yac#toggle_diagnostic_virtual_text( command! YacClearDiagnosticVirtualText call yac#clear_diagnostic_virtual_text() command! YacDebugToggle call yac#debug_toggle() command! YacDebugStatus call yac#debug_status() +command! YacConnections call yac#connections() +command! YacCleanupConnections call yac#cleanup_connections() +command! YacStopAll call yac#stop_all() command! -nargs=? YacFileSearch call yac#file_search() +" Remote editing commands - 简化版本 +command! YacRemoteCleanup call yac_remote#cleanup() " 默认快捷键 nnoremap gd :YacDefinition @@ -65,8 +70,8 @@ nnoremap :YacFileSearch if get(g:, 'lsp_bridge_auto_start', 1) augroup lsp_bridge_auto autocmd! - " 文件打开时启动LSP并打开文档 - autocmd BufReadPost,BufNewFile *.rs call yac#start() | call yac#open_file() + " 智能LSP启动 - 使用SSH Master模式 + autocmd BufReadPost,BufNewFile *.rs call yac_remote#enhanced_lsp_start() " 文档生命周期管理 autocmd BufWritePre *.rs call yac#will_save(1) autocmd BufWritePost *.rs call yac#did_save() @@ -74,5 +79,7 @@ if get(g:, 'lsp_bridge_auto_start', 1) autocmd BufUnload *.rs call yac#did_close() " 自动补全触发 autocmd TextChangedI *.rs call yac#auto_complete_trigger() + " SSH连接清理 - Vim退出时清理SSH Master连接 + autocmd VimLeave * call yac_remote#cleanup() augroup END endif \ No newline at end of file diff --git a/vimrc b/vimrc index 3a8e596f..c22a48db 100644 --- a/vimrc +++ b/vimrc @@ -25,7 +25,8 @@ set runtimepath+=vim " yac-bridge 配置 let g:yac_bridge_command = ['./target/release/lsp-bridge'] -let g:yac_bridge_auto_start = 1 +let g:yac_bridge_auto_start = 0 +" let g:lsp_bridge_debug = 1 " 自动补全配置 (可以修改这些值进行测试) let g:yac_bridge_auto_complete = 1 " 1=启用, 0=禁用自动补全 @@ -50,4 +51,3 @@ endfunction command! YacStatus call YacBridgeStatus() -" let g:yac_bridge_debug = !get(g:, 'yac_bridge_debug', 0)