1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use anyhow::{Context as _, Result};
7use askpass::EncryptedPassword;
8use editor::Editor;
9use extension_host::ExtensionStore;
10use futures::{FutureExt as _, channel::oneshot, select};
11use gpui::{AppContext, AsyncApp, PromptLevel, WindowHandle};
12
13use language::Point;
14use project::trusted_worktrees;
15use remote::{
16 DockerConnectionOptions, Interactive, RemoteConnection, RemoteConnectionOptions,
17 SshConnectionOptions,
18};
19pub use settings::SshConnection;
20use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection};
21use util::paths::PathWithPosition;
22use workspace::{
23 AppState, MultiWorkspace, OpenOptions, SerializedWorkspaceLocation, Workspace,
24 find_existing_workspace,
25};
26
27pub use remote_connection::{
28 RemoteClientDelegate, RemoteConnectionModal, RemoteConnectionPrompt, SshConnectionHeader,
29 connect,
30};
31
32#[derive(RegisterSetting)]
33pub struct RemoteSettings {
34 pub ssh_connections: ExtendingVec<SshConnection>,
35 pub wsl_connections: ExtendingVec<WslConnection>,
36 /// Whether to read ~/.ssh/config for ssh connection sources.
37 pub read_ssh_config: bool,
38}
39
40impl RemoteSettings {
41 pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> + use<> {
42 self.ssh_connections.clone().0.into_iter()
43 }
44
45 pub fn wsl_connections(&self) -> impl Iterator<Item = WslConnection> + use<> {
46 self.wsl_connections.clone().0.into_iter()
47 }
48
49 pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) {
50 for conn in self.ssh_connections() {
51 if conn.host == options.host.to_string()
52 && conn.username == options.username
53 && conn.port == options.port
54 {
55 options.nickname = conn.nickname;
56 options.upload_binary_over_ssh = conn.upload_binary_over_ssh.unwrap_or_default();
57 options.args = Some(conn.args);
58 options.port_forwards = conn.port_forwards;
59 break;
60 }
61 }
62 }
63
64 pub fn connection_options_for(
65 &self,
66 host: String,
67 port: Option<u16>,
68 username: Option<String>,
69 ) -> SshConnectionOptions {
70 let mut options = SshConnectionOptions {
71 host: host.into(),
72 port,
73 username,
74 ..Default::default()
75 };
76 self.fill_connection_options_from_settings(&mut options);
77 options
78 }
79}
80
81#[derive(Clone, PartialEq)]
82pub enum Connection {
83 Ssh(SshConnection),
84 Wsl(WslConnection),
85 DevContainer(DevContainerConnection),
86}
87
88impl From<Connection> for RemoteConnectionOptions {
89 fn from(val: Connection) -> Self {
90 match val {
91 Connection::Ssh(conn) => RemoteConnectionOptions::Ssh(conn.into()),
92 Connection::Wsl(conn) => RemoteConnectionOptions::Wsl(conn.into()),
93 Connection::DevContainer(conn) => {
94 RemoteConnectionOptions::Docker(DockerConnectionOptions {
95 name: conn.name,
96 remote_user: conn.remote_user,
97 container_id: conn.container_id,
98 upload_binary_over_docker_exec: false,
99 use_podman: conn.use_podman,
100 })
101 }
102 }
103 }
104}
105
106impl From<SshConnection> for Connection {
107 fn from(val: SshConnection) -> Self {
108 Connection::Ssh(val)
109 }
110}
111
112impl From<WslConnection> for Connection {
113 fn from(val: WslConnection) -> Self {
114 Connection::Wsl(val)
115 }
116}
117
118impl Settings for RemoteSettings {
119 fn from_settings(content: &settings::SettingsContent) -> Self {
120 let remote = &content.remote;
121 Self {
122 ssh_connections: remote.ssh_connections.clone().unwrap_or_default().into(),
123 wsl_connections: remote.wsl_connections.clone().unwrap_or_default().into(),
124 read_ssh_config: remote.read_ssh_config.unwrap(),
125 }
126 }
127}
128
129pub async fn open_remote_project(
130 connection_options: RemoteConnectionOptions,
131 paths: Vec<PathBuf>,
132 app_state: Arc<AppState>,
133 open_options: workspace::OpenOptions,
134 cx: &mut AsyncApp,
135) -> Result<()> {
136 let created_new_window = open_options.replace_window.is_none();
137
138 let (existing, open_visible) = find_existing_workspace(
139 &paths,
140 &open_options,
141 &SerializedWorkspaceLocation::Remote(connection_options.clone()),
142 cx,
143 )
144 .await;
145
146 if let Some((existing_window, existing_workspace)) = existing {
147 let remote_connection = cx.update(|cx| {
148 existing_workspace
149 .read(cx)
150 .project()
151 .read(cx)
152 .remote_client()
153 .and_then(|client| client.read(cx).remote_connection())
154 });
155
156 if let Some(remote_connection) = remote_connection {
157 let (resolved_paths, paths_with_positions) =
158 determine_paths_with_positions(&remote_connection, paths).await;
159
160 let open_results = existing_window
161 .update(cx, |multi_workspace, window, cx| {
162 window.activate_window();
163 multi_workspace.activate(existing_workspace.clone(), cx);
164 existing_workspace.update(cx, |workspace, cx| {
165 workspace.open_paths(
166 resolved_paths,
167 OpenOptions {
168 visible: Some(open_visible),
169 ..Default::default()
170 },
171 None,
172 window,
173 cx,
174 )
175 })
176 })?
177 .await;
178
179 _ = existing_window.update(cx, |multi_workspace, _, cx| {
180 let workspace = multi_workspace.workspace().clone();
181 workspace.update(cx, |workspace, cx| {
182 for item in open_results.iter().flatten() {
183 if let Err(e) = item {
184 workspace.show_error(&e, cx);
185 }
186 }
187 });
188 });
189
190 let items = open_results
191 .into_iter()
192 .map(|r| r.and_then(|r| r.ok()))
193 .collect::<Vec<_>>();
194 navigate_to_positions(&existing_window, items, &paths_with_positions, cx);
195
196 return Ok(());
197 }
198 // If the remote connection is dead (e.g. server not running after failed reconnect),
199 // fall through to establish a fresh connection instead of showing an error.
200 log::info!(
201 "existing remote workspace found but connection is dead, starting fresh connection"
202 );
203 }
204
205 let (window, initial_workspace) = if let Some(window) = open_options.replace_window {
206 let workspace = window.update(cx, |multi_workspace, _, _| {
207 multi_workspace.workspace().clone()
208 })?;
209 (window, workspace)
210 } else {
211 let workspace_position = cx
212 .update(|cx| {
213 workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
214 })
215 .await
216 .context("fetching remote workspace position from db")?;
217
218 let mut options =
219 cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx));
220 options.window_bounds = workspace_position.window_bounds;
221
222 let window = cx.open_window(options, |window, cx| {
223 let project = project::Project::local(
224 app_state.client.clone(),
225 app_state.node_runtime.clone(),
226 app_state.user_store.clone(),
227 app_state.languages.clone(),
228 app_state.fs.clone(),
229 None,
230 project::LocalProjectFlags {
231 init_worktree_trust: false,
232 ..Default::default()
233 },
234 cx,
235 );
236 let workspace = cx.new(|cx| {
237 let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx);
238 workspace.centered_layout = workspace_position.centered_layout;
239 workspace
240 });
241 cx.new(|cx| MultiWorkspace::new(workspace, window, cx))
242 })?;
243 let workspace = window.update(cx, |multi_workspace, _, _cx| {
244 multi_workspace.workspace().clone()
245 })?;
246 (window, workspace)
247 };
248
249 loop {
250 let (cancel_tx, mut cancel_rx) = oneshot::channel();
251 let delegate = window.update(cx, {
252 let paths = paths.clone();
253 let connection_options = connection_options.clone();
254 let initial_workspace = initial_workspace.clone();
255 move |_multi_workspace: &mut MultiWorkspace, window, cx| {
256 window.activate_window();
257 initial_workspace.update(cx, |workspace, cx| {
258 workspace.hide_modal(window, cx);
259 workspace.toggle_modal(window, cx, |window, cx| {
260 RemoteConnectionModal::new(&connection_options, paths, window, cx)
261 });
262
263 let ui = workspace
264 .active_modal::<RemoteConnectionModal>(cx)?
265 .read(cx)
266 .prompt
267 .clone();
268
269 ui.update(cx, |ui, _cx| {
270 ui.set_cancellation_tx(cancel_tx);
271 });
272
273 Some(Arc::new(RemoteClientDelegate::new(
274 window.window_handle(),
275 ui.downgrade(),
276 if let RemoteConnectionOptions::Ssh(options) = &connection_options {
277 options
278 .password
279 .as_deref()
280 .and_then(|pw| EncryptedPassword::try_from(pw).ok())
281 } else {
282 None
283 },
284 )))
285 })
286 }
287 })?;
288
289 let Some(delegate) = delegate else { break };
290
291 let connection = remote::connect(connection_options.clone(), delegate.clone(), cx);
292 let connection = select! {
293 _ = cancel_rx => {
294 initial_workspace.update(cx, |workspace, cx| {
295 if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
296 ui.update(cx, |modal, cx| modal.finished(cx))
297 }
298 });
299
300 break;
301 },
302 result = connection.fuse() => result,
303 };
304 let remote_connection = match connection {
305 Ok(connection) => connection,
306 Err(e) => {
307 initial_workspace.update(cx, |workspace, cx| {
308 if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
309 ui.update(cx, |modal, cx| modal.finished(cx))
310 }
311 });
312 log::error!("Failed to open project: {e:#}");
313 let response = window
314 .update(cx, |_, window, cx| {
315 window.prompt(
316 PromptLevel::Critical,
317 match connection_options {
318 RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",
319 RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",
320 RemoteConnectionOptions::Docker(_) => {
321 "Failed to connect to Dev Container"
322 }
323 #[cfg(any(test, feature = "test-support"))]
324 RemoteConnectionOptions::Mock(_) => {
325 "Failed to connect to mock server"
326 }
327 },
328 Some(&format!("{e:#}")),
329 &["Retry", "Cancel"],
330 cx,
331 )
332 })?
333 .await;
334
335 if response == Ok(0) {
336 continue;
337 }
338
339 if created_new_window {
340 window
341 .update(cx, |_, window, _| window.remove_window())
342 .ok();
343 }
344 return Ok(());
345 }
346 };
347
348 let (paths, paths_with_positions) =
349 determine_paths_with_positions(&remote_connection, paths.clone()).await;
350
351 let opened_items = cx
352 .update(|cx| {
353 workspace::open_remote_project_with_new_connection(
354 window,
355 remote_connection,
356 cancel_rx,
357 delegate.clone(),
358 app_state.clone(),
359 paths.clone(),
360 cx,
361 )
362 })
363 .await;
364
365 initial_workspace.update(cx, |workspace, cx| {
366 if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
367 ui.update(cx, |modal, cx| modal.finished(cx))
368 }
369 });
370
371 match opened_items {
372 Err(e) => {
373 log::error!("Failed to open project: {e:#}");
374 let response = window
375 .update(cx, |_, window, cx| {
376 window.prompt(
377 PromptLevel::Critical,
378 match connection_options {
379 RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",
380 RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",
381 RemoteConnectionOptions::Docker(_) => {
382 "Failed to connect to Dev Container"
383 }
384 #[cfg(any(test, feature = "test-support"))]
385 RemoteConnectionOptions::Mock(_) => {
386 "Failed to connect to mock server"
387 }
388 },
389 Some(&format!("{e:#}")),
390 &["Retry", "Cancel"],
391 cx,
392 )
393 })?
394 .await;
395 if response == Ok(0) {
396 continue;
397 }
398
399 if created_new_window {
400 window
401 .update(cx, |_, window, _| window.remove_window())
402 .ok();
403 }
404 initial_workspace.update(cx, |workspace, cx| {
405 trusted_worktrees::track_worktree_trust(
406 workspace.project().read(cx).worktree_store(),
407 None,
408 None,
409 None,
410 cx,
411 );
412 });
413 }
414
415 Ok(items) => {
416 navigate_to_positions(&window, items, &paths_with_positions, cx);
417 }
418 }
419
420 break;
421 }
422
423 // Register the remote client with extensions. We use `multi_workspace.workspace()` here
424 // (not `initial_workspace`) because `open_remote_project_inner` activated the new remote
425 // workspace, so the active workspace is now the one with the remote project.
426 window
427 .update(cx, |multi_workspace: &mut MultiWorkspace, _, cx| {
428 let workspace = multi_workspace.workspace().clone();
429 workspace.update(cx, |workspace, cx| {
430 if let Some(client) = workspace.project().read(cx).remote_client() {
431 if let Some(extension_store) = ExtensionStore::try_global(cx) {
432 extension_store
433 .update(cx, |store, cx| store.register_remote_client(client, cx));
434 }
435 }
436 });
437 })
438 .ok();
439 Ok(())
440}
441
442pub fn navigate_to_positions(
443 window: &WindowHandle<MultiWorkspace>,
444 items: impl IntoIterator<Item = Option<Box<dyn workspace::item::ItemHandle>>>,
445 positions: &[PathWithPosition],
446 cx: &mut AsyncApp,
447) {
448 for (item, path) in items.into_iter().zip(positions) {
449 let Some(item) = item else {
450 continue;
451 };
452 let Some(row) = path.row else {
453 continue;
454 };
455 if let Some(active_editor) = item.downcast::<Editor>() {
456 window
457 .update(cx, |_, window, cx| {
458 active_editor.update(cx, |editor, cx| {
459 let row = row.saturating_sub(1);
460 let col = path.column.unwrap_or(0).saturating_sub(1);
461 editor.go_to_singleton_buffer_point(Point::new(row, col), window, cx);
462 });
463 })
464 .ok();
465 }
466 }
467}
468
469pub(crate) async fn determine_paths_with_positions(
470 remote_connection: &Arc<dyn RemoteConnection>,
471 mut paths: Vec<PathBuf>,
472) -> (Vec<PathBuf>, Vec<PathWithPosition>) {
473 let mut paths_with_positions = Vec::<PathWithPosition>::new();
474 for path in &mut paths {
475 if let Some(path_str) = path.to_str() {
476 let path_with_position = PathWithPosition::parse_str(&path_str);
477 if path_with_position.row.is_some() {
478 if !path_exists(&remote_connection, &path).await {
479 *path = path_with_position.path.clone();
480 paths_with_positions.push(path_with_position);
481 continue;
482 }
483 }
484 }
485 paths_with_positions.push(PathWithPosition::from_path(path.clone()))
486 }
487 (paths, paths_with_positions)
488}
489
490async fn path_exists(connection: &Arc<dyn RemoteConnection>, path: &Path) -> bool {
491 let Ok(command) = connection.build_command(
492 Some("test".to_string()),
493 &["-e".to_owned(), path.to_string_lossy().to_string()],
494 &Default::default(),
495 None,
496 None,
497 Interactive::No,
498 ) else {
499 return false;
500 };
501 let Ok(mut child) = util::command::new_command(command.program)
502 .args(command.args)
503 .envs(command.env)
504 .spawn()
505 else {
506 return false;
507 };
508 child.status().await.is_ok_and(|status| status.success())
509}
510
511#[cfg(test)]
512mod tests {
513 use super::*;
514 use extension::ExtensionHostProxy;
515 use fs::FakeFs;
516 use gpui::{AppContext, TestAppContext};
517 use http_client::BlockedHttpClient;
518 use node_runtime::NodeRuntime;
519 use remote::RemoteClient;
520 use remote_server::{HeadlessAppState, HeadlessProject};
521 use serde_json::json;
522 use util::path;
523 use workspace::find_existing_workspace;
524
525 #[gpui::test]
526 async fn test_open_remote_project_with_mock_connection(
527 cx: &mut TestAppContext,
528 server_cx: &mut TestAppContext,
529 ) {
530 let app_state = init_test(cx);
531 let executor = cx.executor();
532
533 cx.update(|cx| {
534 release_channel::init(semver::Version::new(0, 0, 0), cx);
535 });
536 server_cx.update(|cx| {
537 release_channel::init(semver::Version::new(0, 0, 0), cx);
538 });
539
540 let (opts, server_session, connect_guard) = RemoteClient::fake_server(cx, server_cx);
541
542 let remote_fs = FakeFs::new(server_cx.executor());
543 remote_fs
544 .insert_tree(
545 path!("/project"),
546 json!({
547 "src": {
548 "main.rs": "fn main() {}",
549 },
550 "README.md": "# Test Project",
551 }),
552 )
553 .await;
554
555 server_cx.update(HeadlessProject::init);
556 let http_client = Arc::new(BlockedHttpClient);
557 let node_runtime = NodeRuntime::unavailable();
558 let languages = Arc::new(language::LanguageRegistry::new(server_cx.executor()));
559 let proxy = Arc::new(ExtensionHostProxy::new());
560
561 let _headless = server_cx.new(|cx| {
562 HeadlessProject::new(
563 HeadlessAppState {
564 session: server_session,
565 fs: remote_fs.clone(),
566 http_client,
567 node_runtime,
568 languages,
569 extension_host_proxy: proxy,
570 },
571 false,
572 cx,
573 )
574 });
575
576 drop(connect_guard);
577
578 let paths = vec![PathBuf::from(path!("/project"))];
579 let open_options = workspace::OpenOptions::default();
580
581 let mut async_cx = cx.to_async();
582 let result = open_remote_project(opts, paths, app_state, open_options, &mut async_cx).await;
583
584 executor.run_until_parked();
585
586 assert!(result.is_ok(), "open_remote_project should succeed");
587
588 let windows = cx.update(|cx| cx.windows().len());
589 assert_eq!(windows, 1, "Should have opened a window");
590
591 let multi_workspace_handle =
592 cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
593
594 multi_workspace_handle
595 .update(cx, |multi_workspace, _, cx| {
596 let workspace = multi_workspace.workspace().clone();
597 workspace.update(cx, |workspace, cx| {
598 let project = workspace.project().read(cx);
599 assert!(project.is_remote(), "Project should be a remote project");
600 });
601 })
602 .unwrap();
603 }
604
605 #[gpui::test]
606 async fn test_reuse_existing_remote_workspace_window(
607 cx: &mut TestAppContext,
608 server_cx: &mut TestAppContext,
609 ) {
610 let app_state = init_test(cx);
611 let executor = cx.executor();
612
613 cx.update(|cx| {
614 release_channel::init(semver::Version::new(0, 0, 0), cx);
615 });
616 server_cx.update(|cx| {
617 release_channel::init(semver::Version::new(0, 0, 0), cx);
618 });
619
620 let (opts, server_session, connect_guard) = RemoteClient::fake_server(cx, server_cx);
621
622 let remote_fs = FakeFs::new(server_cx.executor());
623 remote_fs
624 .insert_tree(
625 path!("/project"),
626 json!({
627 "src": {
628 "main.rs": "fn main() {}",
629 "lib.rs": "pub fn hello() {}",
630 },
631 "README.md": "# Test Project",
632 }),
633 )
634 .await;
635
636 server_cx.update(HeadlessProject::init);
637 let http_client = Arc::new(BlockedHttpClient);
638 let node_runtime = NodeRuntime::unavailable();
639 let languages = Arc::new(language::LanguageRegistry::new(server_cx.executor()));
640 let proxy = Arc::new(ExtensionHostProxy::new());
641
642 let _headless = server_cx.new(|cx| {
643 HeadlessProject::new(
644 HeadlessAppState {
645 session: server_session,
646 fs: remote_fs.clone(),
647 http_client,
648 node_runtime,
649 languages,
650 extension_host_proxy: proxy,
651 },
652 false,
653 cx,
654 )
655 });
656
657 drop(connect_guard);
658
659 // First open: create a new window for the remote project.
660 let paths = vec![PathBuf::from(path!("/project"))];
661 let mut async_cx = cx.to_async();
662 open_remote_project(
663 opts.clone(),
664 paths,
665 app_state.clone(),
666 workspace::OpenOptions::default(),
667 &mut async_cx,
668 )
669 .await
670 .expect("first open_remote_project should succeed");
671
672 executor.run_until_parked();
673
674 assert_eq!(
675 cx.update(|cx| cx.windows().len()),
676 1,
677 "First open should create exactly one window"
678 );
679
680 let first_window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
681
682 // Verify find_existing_workspace discovers the remote workspace.
683 let search_paths = vec![PathBuf::from(path!("/project/src/lib.rs"))];
684 let (found, _open_visible) = find_existing_workspace(
685 &search_paths,
686 &workspace::OpenOptions::default(),
687 &SerializedWorkspaceLocation::Remote(opts.clone()),
688 &mut async_cx,
689 )
690 .await;
691
692 assert!(
693 found.is_some(),
694 "find_existing_workspace should locate the existing remote workspace"
695 );
696 let (found_window, _found_workspace) = found.unwrap();
697 assert_eq!(
698 found_window, first_window,
699 "find_existing_workspace should return the same window"
700 );
701
702 // Second open with the same connection options should reuse the window.
703 let second_paths = vec![PathBuf::from(path!("/project/src/lib.rs"))];
704 open_remote_project(
705 opts.clone(),
706 second_paths,
707 app_state.clone(),
708 workspace::OpenOptions::default(),
709 &mut async_cx,
710 )
711 .await
712 .expect("second open_remote_project should succeed via reuse");
713
714 executor.run_until_parked();
715
716 assert_eq!(
717 cx.update(|cx| cx.windows().len()),
718 1,
719 "Second open should reuse the existing window, not create a new one"
720 );
721
722 let still_first_window =
723 cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
724 assert_eq!(
725 still_first_window, first_window,
726 "The window handle should be the same after reuse"
727 );
728 }
729
730 #[gpui::test]
731 async fn test_reconnect_when_server_not_running(
732 cx: &mut TestAppContext,
733 server_cx: &mut TestAppContext,
734 ) {
735 let app_state = init_test(cx);
736 let executor = cx.executor();
737
738 cx.update(|cx| {
739 release_channel::init(semver::Version::new(0, 0, 0), cx);
740 });
741 server_cx.update(|cx| {
742 release_channel::init(semver::Version::new(0, 0, 0), cx);
743 });
744
745 let (opts, server_session, connect_guard) = RemoteClient::fake_server(cx, server_cx);
746
747 let remote_fs = FakeFs::new(server_cx.executor());
748 remote_fs
749 .insert_tree(
750 path!("/project"),
751 json!({
752 "src": {
753 "main.rs": "fn main() {}",
754 },
755 }),
756 )
757 .await;
758
759 server_cx.update(HeadlessProject::init);
760 let http_client = Arc::new(BlockedHttpClient);
761 let node_runtime = NodeRuntime::unavailable();
762 let languages = Arc::new(language::LanguageRegistry::new(server_cx.executor()));
763 let proxy = Arc::new(ExtensionHostProxy::new());
764
765 let _headless = server_cx.new(|cx| {
766 HeadlessProject::new(
767 HeadlessAppState {
768 session: server_session,
769 fs: remote_fs.clone(),
770 http_client: http_client.clone(),
771 node_runtime: node_runtime.clone(),
772 languages: languages.clone(),
773 extension_host_proxy: proxy.clone(),
774 },
775 false,
776 cx,
777 )
778 });
779
780 drop(connect_guard);
781
782 // Open the remote project normally.
783 let paths = vec![PathBuf::from(path!("/project"))];
784 let mut async_cx = cx.to_async();
785 open_remote_project(
786 opts.clone(),
787 paths.clone(),
788 app_state.clone(),
789 workspace::OpenOptions::default(),
790 &mut async_cx,
791 )
792 .await
793 .expect("initial open should succeed");
794
795 executor.run_until_parked();
796
797 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
798 let window = cx.update(|cx| cx.windows()[0].downcast::<MultiWorkspace>().unwrap());
799
800 // Force the remote client into ServerNotRunning state (simulates the
801 // scenario where the remote server died and reconnection failed).
802 window
803 .update(cx, |multi_workspace, _, cx| {
804 let workspace = multi_workspace.workspace().clone();
805 workspace.update(cx, |workspace, cx| {
806 let client = workspace
807 .project()
808 .read(cx)
809 .remote_client()
810 .expect("should have remote client");
811 client.update(cx, |client, cx| {
812 client.force_server_not_running(cx);
813 });
814 });
815 })
816 .unwrap();
817
818 executor.run_until_parked();
819
820 // Register a new mock server under the same options so the reconnect
821 // path can establish a fresh connection.
822 let (server_session_2, connect_guard_2) =
823 RemoteClient::fake_server_with_opts(&opts, cx, server_cx);
824
825 let _headless_2 = server_cx.new(|cx| {
826 HeadlessProject::new(
827 HeadlessAppState {
828 session: server_session_2,
829 fs: remote_fs.clone(),
830 http_client,
831 node_runtime,
832 languages,
833 extension_host_proxy: proxy,
834 },
835 false,
836 cx,
837 )
838 });
839
840 drop(connect_guard_2);
841
842 // Simulate clicking "Reconnect": calls open_remote_project with
843 // replace_window pointing to the existing window.
844 let result = open_remote_project(
845 opts,
846 paths,
847 app_state,
848 workspace::OpenOptions {
849 replace_window: Some(window),
850 ..Default::default()
851 },
852 &mut async_cx,
853 )
854 .await;
855
856 executor.run_until_parked();
857
858 assert!(
859 result.is_ok(),
860 "reconnect should succeed but got: {:?}",
861 result.err()
862 );
863
864 // Should still be a single window with a working remote project.
865 assert_eq!(cx.update(|cx| cx.windows().len()), 1);
866
867 window
868 .update(cx, |multi_workspace, _, cx| {
869 let workspace = multi_workspace.workspace().clone();
870 workspace.update(cx, |workspace, cx| {
871 assert!(
872 workspace.project().read(cx).is_remote(),
873 "project should be remote after reconnect"
874 );
875 });
876 })
877 .unwrap();
878 }
879
880 fn init_test(cx: &mut TestAppContext) -> Arc<AppState> {
881 cx.update(|cx| {
882 let state = AppState::test(cx);
883 crate::init(cx);
884 editor::init(cx);
885 state
886 })
887 }
888}