Skip to content

Commit d75f0c9

Browse files
pccBraydn
authored andcommitted
[WIP] feat(multi-client): Multi-client support for the application
With this change, helix becomes a multi-client application. `hx` will automatically start the server process if necessary, and connect to it. Buffers and language servers run as subprocesses of the server and are used by all clients. The client/server protocol is very simple. The server listens on a Unix socket. The client opens the socket and sends a serialized ClientInfo to the server, which contains the command line arguments, working directory and some other information. The terminal file descriptor (and the stdin file descriptor, if stdin is not a terminal) are then sent to the server using SCM_RIGHTS. The server uses the tty file descriptor to render the UI and receive user input exactly as it does today afer opening /dev/tty, and uses the stdin file descriptor to create a scratch buffer. The client listens for relevant signals such as SIGWINCH, SIGTSTP and SIGCONT. When the client receives a signal, it sends a single byte signal number to the server. The server responds to receiving a byte on the socket similarly to how Helix handles signals today. One exception is that in response to a SIGTSTP, the server sends SIGSTOP to the client's process group instead of its own process group. This way, the server keeps running while the client process (and the other processes in its process group) is suspended. When a client terminates (e.g. after :q), the server sends a single byte exit code to the client, and the client exits using the exit code. TODO: - Unbreak the integration tests. - Unbreak the Windows build. For the time being, the native Windows build will not support multi-client. Windows users who want multi-client can use WSL. - Unbreak per-workspace .helix/config.toml support. - Wait until Helix switches to Termina (helix-editor#13307), and then reimplement this on top of Termina. For now this depends on a forked version of Crossterm which is available at: https://github.com/pcc/crossterm/tree/client-server-rebase From a brief look at the Termina code it seems easier to extend Termina to support this than it was to extend Crossterm.
1 parent cef3d42 commit d75f0c9

File tree

17 files changed

+776
-267
lines changed

17 files changed

+776
-267
lines changed

Cargo.lock

Lines changed: 75 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,6 @@ repository = "https://github.com/helix-editor/helix"
6060
homepage = "https://helix-editor.com"
6161
license = "MPL-2.0"
6262
rust-version = "1.82"
63+
64+
[patch.crates-io]
65+
crossterm = { git = "ssh://github.com/braydnm/crossterm.git" }

helix-core/src/position.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ use std::{
44
ops::{Add, AddAssign, Sub, SubAssign},
55
};
66

7+
use serde::{Deserialize, Serialize};
8+
79
use helix_stdx::rope::RopeSliceExt;
810

911
use crate::{
@@ -16,7 +18,7 @@ use crate::{
1618
};
1719

1820
/// Represents a single point in a text buffer. Zero indexed.
19-
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
21+
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
2022
pub struct Position {
2123
pub row: usize,
2224
pub col: usize,

helix-stdx/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ bitflags.workspace = true
2121
once_cell = "1.21"
2222
regex-automata = "0.4.9"
2323
unicode-segmentation.workspace = true
24+
tokio = { version = "1.38.0", features = ["net"] }
2425

2526
[target.'cfg(windows)'.dependencies]
2627
windows-sys = { version = "0.60", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
2728

2829
[target.'cfg(unix)'.dependencies]
29-
rustix = { version = "1.0", features = ["fs"] }
30+
rustix = { version = "1.0", features = ["fs", "net"] }
3031

3132
[dev-dependencies]
3233
tempfile.workspace = true

helix-stdx/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@ pub mod faccess;
33
pub mod path;
44
pub mod range;
55
pub mod rope;
6+
pub mod socket;
67

78
pub use range::Range;

helix-stdx/src/socket.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
use rustix::fd::AsFd;
2+
use rustix::net::{
3+
recvmsg, sendmsg, RecvAncillaryBuffer, RecvAncillaryMessage, RecvFlags, SendAncillaryBuffer,
4+
SendAncillaryMessage, SendFlags,
5+
};
6+
use std::fs::File;
7+
use std::io::IoSlice;
8+
use std::io::{self, IoSliceMut};
9+
use std::mem::MaybeUninit;
10+
11+
pub fn write_fd<Fd: AsFd>(socket: Fd, file: &File) -> io::Result<()> {
12+
let mut space = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))];
13+
let mut buf = SendAncillaryBuffer::new(&mut space);
14+
let fd_arr = [file.as_fd()];
15+
buf.push(SendAncillaryMessage::ScmRights(&fd_arr));
16+
sendmsg(socket, &[IoSlice::new(&[0])], &mut buf, SendFlags::empty())?;
17+
Ok(())
18+
}
19+
20+
pub fn read_fd<Fd: AsFd>(socket: Fd) -> io::Result<File> {
21+
let mut space = [MaybeUninit::uninit(); rustix::cmsg_space!(ScmRights(1))];
22+
let mut buf = RecvAncillaryBuffer::new(&mut space);
23+
let mut recv_buf = [0];
24+
recvmsg(
25+
socket,
26+
&mut [IoSliceMut::new(&mut recv_buf)],
27+
&mut buf,
28+
RecvFlags::empty(),
29+
)?;
30+
if let Some(RecvAncillaryMessage::ScmRights(mut fd)) = buf.drain().next() {
31+
if let Some(fd) = fd.next() {
32+
return Ok(fd.into());
33+
}
34+
}
35+
Err(io::Error::new(io::ErrorKind::Other, "did not receive fd"))
36+
}

helix-term/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ serde = { version = "1.0", features = ["derive"] }
9090
# ripgrep for global search
9191
grep-regex = "0.1.13"
9292
grep-searcher = "0.1.14"
93+
rmp-serde = "1.3.0"
94+
tokio-util = { version = "0.7.15", features = ["io", "io-util"] }
95+
async-stream = "0.3.6"
9396

9497
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
9598
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }

0 commit comments

Comments
 (0)