Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
b68259f
feat: implement SSH tunnel infrastructure for remote editing
claude[bot] Aug 25, 2025
5f9757c
refactor: simplify SSH tunnel infrastructure based on code review
claude[bot] Aug 25, 2025
ecace5d
feat: complete SSH file detection and mode selection
claude[bot] Aug 25, 2025
251a28b
feat: implement complete SSH tunnel infrastructure
claude[bot] Aug 25, 2025
27bcc8f
fix: correct SSH tunnel architecture - local bridge as forwarder, rem…
claude[bot] Aug 25, 2025
d1a2448
feat: implement stdio-to-unix-socket transparent forwarder for local …
claude[bot] Aug 25, 2025
cb2bbc9
feat: implement SSH path conversion for remote LSP operations
claude[bot] Aug 25, 2025
2a50f6f
docs: update documentation with comprehensive SSH tunnel infrastructure
claude[bot] Aug 25, 2025
2f523c4
fix: correct SCP deployment command for SSH tunnel setup
claude[bot] Aug 25, 2025
9c91ad8
feat: add comprehensive debug logging to SSH tunnel operations
claude[bot] Aug 25, 2025
9f98a01
fix: SSH tunnel creation using socat for Unix socket forwarding
claude[bot] Aug 25, 2025
e876e07
feat: add remote binary existence check to avoid unnecessary deployments
claude[bot] Aug 25, 2025
56beb87
Remove socat dependency from SSH tunnel infrastructure
claude[bot] Aug 25, 2025
0531f0f
fix: SSH tunnel socket creation logic for direct SSH forwarding
claude[bot] Aug 25, 2025
88ae6d3
fix: SSH tunnel architecture - implement proper stdio-to-socket forwa…
claude[bot] Aug 25, 2025
8fbc60b
fix: SSH path conversion for LSP operations
claude[bot] Aug 26, 2025
4ea6928
feat: implement SSH path conversion in goto handler
claude[bot] Aug 26, 2025
bdf62cc
feat: simplify SSH remote architecture - 81% size reduction
claude[bot] Aug 26, 2025
1da7ffe
feat: implement SSH Master architecture for simplified remote editing
claude[bot] Aug 26, 2025
5dfd89a
feat: implement SSH Master architecture for simplified remote editing
claude[bot] Aug 26, 2025
d8842ff
feat: cleanup unused Clone derives and consolidate yac_remote archite…
claude[bot] Aug 26, 2025
d1ebbd8
cleanup: remove unused UnixSocketTransport implementation
claude[bot] Aug 26, 2025
6e94bf3
refactor: move SSH path handling from vim to handler layer
claude[bot] Aug 26, 2025
372b19a
fix: use job_start for SSH Master with ControlPersist=no
claude[bot] Aug 26, 2025
211078f
Break on transport error instead of continuing
loyalpartner Aug 26, 2025
cc74c4b
feat(connection-pool): implement multi-host LSP connection architecture
Aug 27, 2025
9e59b92
fix(vim): use negative IDs for vim channel commands per protocol spec
Aug 27, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
524 changes: 1 addition & 523 deletions CLAUDE.md

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -92,6 +93,7 @@ inoremap <silent> <C-Space> <C-o>:LspComplete<CR> " 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
Expand All @@ -100,12 +102,18 @@ inoremap <silent> <C-Space> <C-o>:LspComplete<CR> " 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'`
Expand Down
1 change: 0 additions & 1 deletion crates/lsp-bridge/src/handlers/call_hierarchy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ impl CallHierarchyInfo {
}
}

#[derive(Clone)]
pub struct CallHierarchyHandler {
lsp_registry: Arc<LspRegistry>,
}
Expand Down
28 changes: 27 additions & 1 deletion crates/lsp-bridge/src/handlers/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,35 @@ impl Location {
}
}

/// SSH path conversion utilities
pub fn extract_ssh_path(file_path: &str) -> (Option<String>, 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<String> {
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()))
}
Expand Down
9 changes: 7 additions & 2 deletions crates/lsp-bridge/src/handlers/file_open.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -58,8 +60,11 @@ impl Handler for FileOpenHandler {
_ctx: &mut dyn vim::VimContext,
input: Self::Input,
) -> Result<Option<Self::Output>> {
// 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
Expand Down
1 change: 0 additions & 1 deletion crates/lsp-bridge/src/handlers/file_search.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,6 @@ pub struct FileItem {
pub score: f64, // For relevance scoring
}

#[derive(Clone)]
pub struct FileSearchHandler {
_lsp_registry: Arc<LspRegistry>,
file_cache: Arc<OnceCell<Vec<PathBuf>>>,
Expand Down
12 changes: 7 additions & 5 deletions crates/lsp-bridge/src/handlers/goto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -45,7 +45,6 @@ impl GotoType {
// Linus-style: Location 要么完整存在,要么不存在
pub type GotoResponse = Option<Location>;

#[derive(Clone)]
pub struct GotoHandler {
lsp_registry: Arc<LspRegistry>,
goto_type: GotoType,
Expand Down Expand Up @@ -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)],
Expand Down
63 changes: 27 additions & 36 deletions crates/lsp-bridge/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<dyn std::error::Error>> {
use std::fs::OpenOptions;
Expand All @@ -41,21 +44,17 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
.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
Expand All @@ -82,57 +81,49 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
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(())
}
Loading