feat(lsp): add TCP transport for GDScript/Godot LSP (port 6008)
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 <crush@charm.land>
This commit is contained in:
parent
7cdccc3ca5
commit
2b5f46080e
|
|
@ -154,8 +154,8 @@ const KNOWN_LSP_SERVERS_TABLE: &[StaticLspServerDescriptor] = &[
|
||||||
},
|
},
|
||||||
StaticLspServerDescriptor {
|
StaticLspServerDescriptor {
|
||||||
language: "gdscript",
|
language: "gdscript",
|
||||||
command: "godot",
|
command: "tcp://localhost:6008",
|
||||||
args: &["--headless", "--editor"],
|
args: &[],
|
||||||
extensions: &["gd"],
|
extensions: &["gd"],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,13 @@ impl LspProcess {
|
||||||
args: &[String],
|
args: &[String],
|
||||||
root_path: &Path,
|
root_path: &Path,
|
||||||
) -> Result<Self, LspProcessError> {
|
) -> Result<Self, LspProcessError> {
|
||||||
let transport = LspTransport::spawn(command, args)
|
let transport = if command.starts_with("tcp://") {
|
||||||
.map_err(|e| LspProcessError::Transport(LspTransportError::Io(e)))?;
|
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 canonical = canonicalize_root(root_path)?;
|
||||||
let root_uri = format!("file://{canonical}");
|
let root_uri = format!("file://{canonical}");
|
||||||
|
|
|
||||||
|
|
@ -419,7 +419,72 @@ impl LspTransport {
|
||||||
|
|
||||||
Ok(payload)
|
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> {
|
||||||
|
Self::connect_tcp_with_timeout(address, DEFAULT_REQUEST_TIMEOUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn connect_tcp_with_timeout(
|
||||||
|
address: &str,
|
||||||
|
request_timeout: Duration,
|
||||||
|
) -> io::Result<Self> {
|
||||||
|
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)]
|
#[cfg(test)]
|
||||||
mod tests;
|
mod tests;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue