Skip to content

Commit cef3d42

Browse files
pccBraydn
authored andcommitted
feat(multi-client): Split the Editor data structure to support multiple clients
This commit is the first step towards allowing the editor to support multiple clients, where a "client" can be thought of as a window displaying the editor UI. This change may support the following use cases: - Client-server architecture with multiple terminal windows displaying the UI with shared buffers (helix-editor#312). This will be the initial use case. - Helix mode for other editors. - Multiple windows for a GUI. With this change, the Editor owns multiple EditorClients, with each EditorClient holding the data for a particular client, such as its views and state related to user input, such as the mode and repeat count. Clients are referenced using a ClientId which is used as a key for per-client data. For the time being, the Application class only creates one client, but in a followup change (which is still work-in-progress and is available at [1]) the Application will be modified to implement the client-server support. Apologies for the size of this change, I couldn't see a good way to split it up due to the many interdependencies. Probably the easiest way to review it is to download it locally and use `git show -w --color-words`. Note that only a minimum amount of per-client state has been moved to the EditorClient; for example, the state for the status bar is still shared. The remaining state will be moved in followup changes. [1] https://github.com/pcc/helix/tree/client-server-rebase
1 parent 9789b27 commit cef3d42

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+2298
-1505
lines changed

helix-term/src/application.rs

Lines changed: 33 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ use helix_view::{
1414
graphics::Rect,
1515
theme,
1616
tree::Layout,
17-
Align, Editor,
17+
Align, ClientId, Editor,
1818
};
1919
use serde_json::json;
2020
use tui::backend::Backend;
@@ -62,6 +62,7 @@ pub struct Application {
6262
compositor: Compositor,
6363
terminal: Terminal,
6464
pub editor: Editor,
65+
pub client_id: ClientId,
6566

6667
config: Arc<ArcSwap<Config>>,
6768

@@ -115,14 +116,15 @@ impl Application {
115116
let config = Arc::new(ArcSwap::from_pointee(config));
116117
let handlers = handlers::setup(config.clone());
117118
let mut editor = Editor::new(
118-
area,
119119
Arc::new(theme_loader),
120120
Arc::new(ArcSwap::from_pointee(lang_loader)),
121121
Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
122122
&config.editor
123123
})),
124124
handlers,
125125
);
126+
let client_id = editor.add_client(area);
127+
editor.most_recent_client_id = Some(client_id);
126128
Self::load_configured_theme(&mut editor, &config.load());
127129

128130
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
@@ -133,15 +135,15 @@ impl Application {
133135

134136
if args.load_tutor {
135137
let path = helix_loader::runtime_file(Path::new("tutor"));
136-
editor.open(&path, Action::VerticalSplit)?;
138+
editor.open(client_id, &path, Action::VerticalSplit)?;
137139
// Unset path to prevent accidentally saving to the original tutor file.
138-
doc_mut!(editor).set_path(None);
140+
doc_mut!(editor, client_id).set_path(None);
139141
} else if !args.files.is_empty() {
140142
let mut files_it = args.files.into_iter().peekable();
141143

142144
// If the first file is a directory, skip it and open a picker
143145
if let Some((first, _)) = files_it.next_if(|(p, _)| p.is_dir()) {
144-
let picker = ui::file_picker(&editor, first);
146+
let picker = ui::file_picker(&editor, client_id, first);
145147
compositor.push(Box::new(overlaid(picker)));
146148
}
147149

@@ -167,7 +169,7 @@ impl Application {
167169
None => Action::Load,
168170
};
169171
let old_id = editor.document_id_by_path(&file);
170-
let doc_id = match editor.open(&file, action) {
172+
let doc_id = match editor.open(client_id, &file, action) {
171173
// Ignore irregular files during application init.
172174
Err(DocumentOpenError::IrregularFile) => {
173175
nr_of_files -= 1;
@@ -185,8 +187,8 @@ impl Application {
185187
// NOTE: this isn't necessarily true anymore. If
186188
// `--vsplit` or `--hsplit` are used, the file which is
187189
// opened last is focused on.
188-
let view_id = editor.tree.focus;
189-
let doc = doc_mut!(editor, &doc_id);
190+
let view_id = client!(editor, client_id).tree.focus;
191+
let doc = doc_with_id_mut!(editor, &doc_id);
190192
let selection = pos
191193
.into_iter()
192194
.map(|coords| {
@@ -199,7 +201,7 @@ impl Application {
199201

200202
// if all files were invalid, replace with empty buffer
201203
if nr_of_files == 0 {
202-
editor.new_file(Action::VerticalSplit);
204+
editor.new_file(client_id, Action::VerticalSplit);
203205
} else {
204206
editor.set_status(format!(
205207
"Loaded {} file{}.",
@@ -208,18 +210,18 @@ impl Application {
208210
));
209211
// align the view to center after all files are loaded,
210212
// does not affect views without pos since it is at the top
211-
let (view, doc) = current!(editor);
213+
let (_client, view, doc) = current!(editor, client_id);
212214
align_view(doc, view, Align::Center);
213215
}
214216
} else {
215-
editor.new_file(Action::VerticalSplit);
217+
editor.new_file(client_id, Action::VerticalSplit);
216218
}
217219
} else if stdin().is_tty() || cfg!(feature = "integration") {
218-
editor.new_file(Action::VerticalSplit);
220+
editor.new_file(client_id, Action::VerticalSplit);
219221
} else {
220222
editor
221-
.new_file_from_stdin(Action::VerticalSplit)
222-
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
223+
.new_file_from_stdin(client_id, Action::VerticalSplit)
224+
.unwrap_or_else(|_| editor.new_file(client_id, Action::VerticalSplit));
223225
}
224226

225227
#[cfg(windows)]
@@ -238,6 +240,7 @@ impl Application {
238240
compositor,
239241
terminal,
240242
editor,
243+
client_id,
241244
config,
242245
signals,
243246
jobs: Jobs::new(),
@@ -255,6 +258,7 @@ impl Application {
255258

256259
let mut cx = crate::compositor::Context {
257260
editor: &mut self.editor,
261+
client_id: self.client_id,
258262
jobs: &mut self.jobs,
259263
scroll: None,
260264
};
@@ -272,7 +276,7 @@ impl Application {
272276
let surface = self.terminal.current_buffer_mut();
273277

274278
self.compositor.render(area, surface, &mut cx);
275-
let (pos, kind) = self.compositor.cursor(area, &self.editor);
279+
let (pos, kind) = self.compositor.cursor(area, &self.editor, self.client_id);
276280
// reset cursor cache
277281
self.editor.cursor_cache.reset();
278282

@@ -380,8 +384,8 @@ impl Application {
380384

381385
// reset view position in case softwrap was enabled/disabled
382386
let scrolloff = self.editor.config().scrolloff;
383-
for (view, _) in self.editor.tree.views() {
384-
let doc = doc_mut!(self.editor, &view.doc);
387+
for view in self.editor.views.iter() {
388+
let doc = doc_with_id_mut!(self.editor, &view.doc);
385389
view.ensure_cursor_in_view(doc, scrolloff);
386390
}
387391
}
@@ -534,6 +538,7 @@ impl Application {
534538
pub async fn handle_idle_timeout(&mut self) {
535539
let mut cx = crate::compositor::Context {
536540
editor: &mut self.editor,
541+
client_id: ClientId::default(),
537542
jobs: &mut self.jobs,
538543
scroll: None,
539544
};
@@ -613,7 +618,7 @@ impl Application {
613618
helix_event::request_redraw();
614619
}
615620
EditorEvent::DebuggerEvent((id, payload)) => {
616-
let needs_render = self.editor.handle_debugger_message(id, payload).await;
621+
let needs_render = self.editor.handle_debugger_message(self.editor.most_recent_client_id.unwrap(), id, payload).await;
617622
if needs_render {
618623
self.render().await;
619624
}
@@ -638,6 +643,7 @@ impl Application {
638643
pub async fn handle_terminal_events(&mut self, event: std::io::Result<CrosstermEvent>) {
639644
let mut cx = crate::compositor::Context {
640645
editor: &mut self.editor,
646+
client_id: self.client_id,
641647
jobs: &mut self.jobs,
642648
scroll: None,
643649
};
@@ -916,9 +922,11 @@ impl Application {
916922
let language_server = language_server!();
917923
if language_server.is_initialized() {
918924
let offset_encoding = language_server.offset_encoding();
919-
let res = self
920-
.editor
921-
.apply_workspace_edit(offset_encoding, &params.edit);
925+
let res = self.editor.apply_workspace_edit(
926+
self.editor.most_recent_client_id.unwrap(),
927+
offset_encoding,
928+
&params.edit,
929+
);
922930

923931
Ok(json!(lsp::ApplyWorkspaceEditResponse {
924932
applied: res.is_ok(),
@@ -1072,19 +1080,20 @@ impl Application {
10721080
_ => helix_view::editor::Action::VerticalSplit,
10731081
};
10741082

1075-
let doc_id = match self.editor.open(path, action) {
1083+
let most_recent_client = self.editor.most_recent_client_id.unwrap();
1084+
let doc_id = match self.editor.open(most_recent_client, path, action) {
10761085
Ok(id) => id,
10771086
Err(err) => {
10781087
log::error!("failed to open path: {:?}: {:?}", uri, err);
10791088
return lsp::ShowDocumentResult { success: false };
10801089
}
10811090
};
10821091

1083-
let doc = doc_mut!(self.editor, &doc_id);
1092+
let doc = doc_with_id_mut!(self.editor, &doc_id);
10841093
if let Some(range) = selection {
10851094
// TODO: convert inside server
10861095
if let Some(new_range) = lsp_range_to_range(doc.text(), range, offset_encoding) {
1087-
let view = view_mut!(self.editor);
1096+
let view = client_view_mut!(self.editor, most_recent_client);
10881097

10891098
// we flip the range so that the cursor sits on the start of the symbol
10901099
// (for example start of the function).

0 commit comments

Comments
 (0)