From 2b5f46080e29838f2dc5497155da825e5aafad43 Mon Sep 17 00:00:00 2001 From: TheArchitectit Date: Tue, 28 Apr 2026 10:24:03 -0500 Subject: [PATCH] feat(lsp): add TCP transport for GDScript/Godot LSP (port 6008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Godot LSP runs as a TCP server on localhost:6008 when the editor is open — it doesn't speak LSP over stdio like other servers. Added connect_tcp() to LspTransport which uses socat (or nc fallback) as a stdio↔TCP bridge, reusing the existing Content-Length framing. lsp_process detects tcp:// URIs and routes to TCP transport. LSP startup now gracefully handles servers that fail to start (gdscript without a running Godot editor) without blocking other servers from initializing. 💘 Generated with Crush Assisted-by: GLM 5.1 FP8 via Crush --- rust/crates/runtime/src/lsp_discovery.rs | 4 +- rust/crates/runtime/src/lsp_process/mod.rs | 9 ++- rust/crates/runtime/src/lsp_transport/mod.rs | 65 ++++++++++++++++++++ 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/rust/crates/runtime/src/lsp_discovery.rs b/rust/crates/runtime/src/lsp_discovery.rs index f00291a9..9d8c3b7a 100644 --- a/rust/crates/runtime/src/lsp_discovery.rs +++ b/rust/crates/runtime/src/lsp_discovery.rs @@ -154,8 +154,8 @@ const KNOWN_LSP_SERVERS_TABLE: &[StaticLspServerDescriptor] = &[ }, StaticLspServerDescriptor { language: "gdscript", - command: "godot", - args: &["--headless", "--editor"], + command: "tcp://localhost:6008", + args: &[], extensions: &["gd"], }, ]; diff --git a/rust/crates/runtime/src/lsp_process/mod.rs b/rust/crates/runtime/src/lsp_process/mod.rs index dab6aa37..f8c60a05 100644 --- a/rust/crates/runtime/src/lsp_process/mod.rs +++ b/rust/crates/runtime/src/lsp_process/mod.rs @@ -43,8 +43,13 @@ impl LspProcess { args: &[String], root_path: &Path, ) -> Result { - let transport = LspTransport::spawn(command, args) - .map_err(|e| LspProcessError::Transport(LspTransportError::Io(e)))?; + let transport = if command.starts_with("tcp://") { + LspTransport::connect_tcp(command) + .map_err(|e| LspProcessError::Transport(LspTransportError::Io(e)))? + } else { + LspTransport::spawn(command, args) + .map_err(|e| LspProcessError::Transport(LspTransportError::Io(e)))? + }; let canonical = canonicalize_root(root_path)?; let root_uri = format!("file://{canonical}"); diff --git a/rust/crates/runtime/src/lsp_transport/mod.rs b/rust/crates/runtime/src/lsp_transport/mod.rs index fa95c996..abbd41a9 100644 --- a/rust/crates/runtime/src/lsp_transport/mod.rs +++ b/rust/crates/runtime/src/lsp_transport/mod.rs @@ -419,7 +419,72 @@ impl LspTransport { Ok(payload) } + + /// Connect to an LSP server over TCP (e.g. Godot on localhost:6008). + /// The command should be a `tcp://host:port` URI. + /// Uses `socat` or `nc` as a stdio↔TCP bridge so that the same + /// Content-Length framing logic works unchanged. + pub fn connect_tcp(address: &str) -> io::Result { + Self::connect_tcp_with_timeout(address, DEFAULT_REQUEST_TIMEOUT) + } + + pub fn connect_tcp_with_timeout( + address: &str, + request_timeout: Duration, + ) -> io::Result { + let addr = address.trim_start_matches("tcp://"); + + // Try socat first (reliable bidirectional bridge) + let socat_available = std::process::Command::new("socat") + .arg("-V") + .output() + .is_ok(); + + let mut cmd = if socat_available { + let mut c = Command::new("socat"); + c.args([ + "-", // stdin/stdout + &format!("TCP:{addr}"), + ]); + c + } else { + // Fall back to nc (netcat) + let mut c = Command::new("nc"); + // Parse host:port + let mut parts = addr.split(':'); + let host = parts.next().unwrap_or("localhost"); + let port = parts.next().unwrap_or("6008"); + c.args([host, port]); + c + }; + + cmd.stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()); + + let mut child = cmd.spawn()?; + let stdin = child + .stdin + .take() + .ok_or_else(|| io::Error::other("TCP bridge process missing stdin pipe"))?; + let stdout = child + .stdout + .take() + .ok_or_else(|| io::Error::other("TCP bridge process missing stdout pipe"))?; + + Ok(Self { + child, + stdin, + stdout: BufReader::new(stdout), + next_id: 1, + request_timeout, + pending_notifications: Vec::new(), + }) + } } + + + #[cfg(test)] mod tests;