1use std::{
2 path::{Path, PathBuf},
3 sync::Arc,
4};
5
6use anyhow::{Context as _, Result};
7use askpass::EncryptedPassword;
8use auto_update::AutoUpdater;
9use editor::Editor;
10use extension_host::ExtensionStore;
11use futures::channel::oneshot;
12use gpui::{
13 AnyWindowHandle, App, AsyncApp, DismissEvent, Entity, EventEmitter, Focusable, FontFeatures,
14 ParentElement as _, PromptLevel, Render, SharedString, Task, TextStyleRefinement, WeakEntity,
15};
16
17use language::{CursorShape, Point};
18use markdown::{Markdown, MarkdownElement, MarkdownStyle};
19use release_channel::ReleaseChannel;
20use remote::{
21 ConnectionIdentifier, DockerConnectionOptions, RemoteClient, RemoteConnection,
22 RemoteConnectionOptions, RemotePlatform, SshConnectionOptions,
23};
24use semver::Version;
25pub use settings::SshConnection;
26use settings::{DevContainerConnection, ExtendingVec, RegisterSetting, Settings, WslConnection};
27use theme::ThemeSettings;
28use ui::{
29 ActiveTheme, Color, CommonAnimationExt, Context, InteractiveElement, IntoElement, KeyBinding,
30 LabelCommon, ListItem, Styled, Window, prelude::*,
31};
32use util::paths::PathWithPosition;
33use workspace::{AppState, ModalView, Workspace};
34
35#[derive(RegisterSetting)]
36pub struct SshSettings {
37 pub ssh_connections: ExtendingVec<SshConnection>,
38 pub wsl_connections: ExtendingVec<WslConnection>,
39 /// Whether to read ~/.ssh/config for ssh connection sources.
40 pub read_ssh_config: bool,
41}
42
43impl SshSettings {
44 pub fn ssh_connections(&self) -> impl Iterator<Item = SshConnection> + use<> {
45 self.ssh_connections.clone().0.into_iter()
46 }
47
48 pub fn wsl_connections(&self) -> impl Iterator<Item = WslConnection> + use<> {
49 self.wsl_connections.clone().0.into_iter()
50 }
51
52 pub fn fill_connection_options_from_settings(&self, options: &mut SshConnectionOptions) {
53 for conn in self.ssh_connections() {
54 if conn.host == options.host
55 && conn.username == options.username
56 && conn.port == options.port
57 {
58 options.nickname = conn.nickname;
59 options.upload_binary_over_ssh = conn.upload_binary_over_ssh.unwrap_or_default();
60 options.args = Some(conn.args);
61 options.port_forwards = conn.port_forwards;
62 break;
63 }
64 }
65 }
66
67 pub fn connection_options_for(
68 &self,
69 host: String,
70 port: Option<u16>,
71 username: Option<String>,
72 ) -> SshConnectionOptions {
73 let mut options = SshConnectionOptions {
74 host,
75 port,
76 username,
77 ..Default::default()
78 };
79 self.fill_connection_options_from_settings(&mut options);
80 options
81 }
82}
83
84#[derive(Clone, PartialEq)]
85pub enum Connection {
86 Ssh(SshConnection),
87 Wsl(WslConnection),
88 DevContainer(DevContainerConnection),
89}
90
91impl From<Connection> for RemoteConnectionOptions {
92 fn from(val: Connection) -> Self {
93 match val {
94 Connection::Ssh(conn) => RemoteConnectionOptions::Ssh(conn.into()),
95 Connection::Wsl(conn) => RemoteConnectionOptions::Wsl(conn.into()),
96 Connection::DevContainer(conn) => {
97 RemoteConnectionOptions::Docker(DockerConnectionOptions {
98 name: conn.name.to_string(),
99 container_id: conn.container_id.to_string(),
100 upload_binary_over_docker_exec: false,
101 })
102 }
103 }
104 }
105}
106
107impl From<SshConnection> for Connection {
108 fn from(val: SshConnection) -> Self {
109 Connection::Ssh(val)
110 }
111}
112
113impl From<WslConnection> for Connection {
114 fn from(val: WslConnection) -> Self {
115 Connection::Wsl(val)
116 }
117}
118
119impl Settings for SshSettings {
120 fn from_settings(content: &settings::SettingsContent) -> Self {
121 let remote = &content.remote;
122 Self {
123 ssh_connections: remote.ssh_connections.clone().unwrap_or_default().into(),
124 wsl_connections: remote.wsl_connections.clone().unwrap_or_default().into(),
125 read_ssh_config: remote.read_ssh_config.unwrap(),
126 }
127 }
128}
129
130pub struct RemoteConnectionPrompt {
131 connection_string: SharedString,
132 nickname: Option<SharedString>,
133 is_wsl: bool,
134 is_devcontainer: bool,
135 status_message: Option<SharedString>,
136 prompt: Option<(Entity<Markdown>, oneshot::Sender<EncryptedPassword>)>,
137 cancellation: Option<oneshot::Sender<()>>,
138 editor: Entity<Editor>,
139}
140
141impl Drop for RemoteConnectionPrompt {
142 fn drop(&mut self) {
143 if let Some(cancel) = self.cancellation.take() {
144 cancel.send(()).ok();
145 }
146 }
147}
148
149pub struct RemoteConnectionModal {
150 pub prompt: Entity<RemoteConnectionPrompt>,
151 paths: Vec<PathBuf>,
152 finished: bool,
153}
154
155impl RemoteConnectionPrompt {
156 pub(crate) fn new(
157 connection_string: String,
158 nickname: Option<String>,
159 is_wsl: bool,
160 is_devcontainer: bool,
161 window: &mut Window,
162 cx: &mut Context<Self>,
163 ) -> Self {
164 Self {
165 connection_string: connection_string.into(),
166 nickname: nickname.map(|nickname| nickname.into()),
167 is_wsl,
168 is_devcontainer,
169 editor: cx.new(|cx| Editor::single_line(window, cx)),
170 status_message: None,
171 cancellation: None,
172 prompt: None,
173 }
174 }
175
176 pub fn set_cancellation_tx(&mut self, tx: oneshot::Sender<()>) {
177 self.cancellation = Some(tx);
178 }
179
180 fn set_prompt(
181 &mut self,
182 prompt: String,
183 tx: oneshot::Sender<EncryptedPassword>,
184 window: &mut Window,
185 cx: &mut Context<Self>,
186 ) {
187 let theme = ThemeSettings::get_global(cx);
188
189 let refinement = TextStyleRefinement {
190 font_family: Some(theme.buffer_font.family.clone()),
191 font_features: Some(FontFeatures::disable_ligatures()),
192 font_size: Some(theme.buffer_font_size(cx).into()),
193 color: Some(cx.theme().colors().editor_foreground),
194 background_color: Some(gpui::transparent_black()),
195 ..Default::default()
196 };
197
198 self.editor.update(cx, |editor, cx| {
199 if prompt.contains("yes/no") {
200 editor.set_masked(false, cx);
201 } else {
202 editor.set_masked(true, cx);
203 }
204 editor.set_text_style_refinement(refinement);
205 editor.set_cursor_shape(CursorShape::Block, cx);
206 });
207
208 let markdown = cx.new(|cx| Markdown::new_text(prompt.into(), cx));
209 self.prompt = Some((markdown, tx));
210 self.status_message.take();
211 window.focus(&self.editor.focus_handle(cx));
212 cx.notify();
213 }
214
215 pub fn set_status(&mut self, status: Option<String>, cx: &mut Context<Self>) {
216 self.status_message = status.map(|s| s.into());
217 cx.notify();
218 }
219
220 pub fn confirm(&mut self, window: &mut Window, cx: &mut Context<Self>) {
221 if let Some((_, tx)) = self.prompt.take() {
222 self.status_message = Some("Connecting".into());
223
224 self.editor.update(cx, |editor, cx| {
225 let pw = editor.text(cx);
226 if let Ok(secure) = EncryptedPassword::try_from(pw.as_ref()) {
227 tx.send(secure).ok();
228 }
229 editor.clear(window, cx);
230 });
231 }
232 }
233}
234
235impl Render for RemoteConnectionPrompt {
236 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
237 let theme = ThemeSettings::get_global(cx);
238
239 let mut text_style = window.text_style();
240 let refinement = TextStyleRefinement {
241 font_family: Some(theme.buffer_font.family.clone()),
242 font_features: Some(FontFeatures::disable_ligatures()),
243 font_size: Some(theme.buffer_font_size(cx).into()),
244 color: Some(cx.theme().colors().editor_foreground),
245 background_color: Some(gpui::transparent_black()),
246 ..Default::default()
247 };
248
249 text_style.refine(&refinement);
250 let markdown_style = MarkdownStyle {
251 base_text_style: text_style,
252 selection_background_color: cx.theme().colors().element_selection_background,
253 ..Default::default()
254 };
255
256 v_flex()
257 .key_context("PasswordPrompt")
258 .p_2()
259 .size_full()
260 .text_buffer(cx)
261 .when_some(self.status_message.clone(), |el, status_message| {
262 el.child(
263 h_flex()
264 .gap_2()
265 .child(
266 Icon::new(IconName::ArrowCircle)
267 .color(Color::Muted)
268 .with_rotate_animation(2),
269 )
270 .child(
271 div()
272 .text_ellipsis()
273 .overflow_x_hidden()
274 .child(format!("{}…", status_message)),
275 ),
276 )
277 })
278 .when_some(self.prompt.as_ref(), |el, prompt| {
279 el.child(
280 div()
281 .size_full()
282 .overflow_hidden()
283 .child(MarkdownElement::new(prompt.0.clone(), markdown_style))
284 .child(self.editor.clone()),
285 )
286 .when(window.capslock().on, |el| {
287 el.child(Label::new("⚠️ ⇪ is on"))
288 })
289 })
290 }
291}
292
293impl RemoteConnectionModal {
294 pub fn new(
295 connection_options: &RemoteConnectionOptions,
296 paths: Vec<PathBuf>,
297 window: &mut Window,
298 cx: &mut Context<Self>,
299 ) -> Self {
300 let (connection_string, nickname, is_wsl, is_devcontainer) = match connection_options {
301 RemoteConnectionOptions::Ssh(options) => (
302 options.connection_string(),
303 options.nickname.clone(),
304 false,
305 false,
306 ),
307 RemoteConnectionOptions::Wsl(options) => {
308 (options.distro_name.clone(), None, true, false)
309 }
310 RemoteConnectionOptions::Docker(options) => (options.name.clone(), None, false, true),
311 };
312 Self {
313 prompt: cx.new(|cx| {
314 RemoteConnectionPrompt::new(
315 connection_string,
316 nickname,
317 is_wsl,
318 is_devcontainer,
319 window,
320 cx,
321 )
322 }),
323 finished: false,
324 paths,
325 }
326 }
327
328 fn confirm(&mut self, _: &menu::Confirm, window: &mut Window, cx: &mut Context<Self>) {
329 self.prompt
330 .update(cx, |prompt, cx| prompt.confirm(window, cx))
331 }
332
333 pub fn finished(&mut self, cx: &mut Context<Self>) {
334 self.finished = true;
335 cx.emit(DismissEvent);
336 }
337
338 fn dismiss(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
339 if let Some(tx) = self
340 .prompt
341 .update(cx, |prompt, _cx| prompt.cancellation.take())
342 {
343 tx.send(()).ok();
344 }
345 self.finished(cx);
346 }
347}
348
349pub(crate) struct SshConnectionHeader {
350 pub(crate) connection_string: SharedString,
351 pub(crate) paths: Vec<PathBuf>,
352 pub(crate) nickname: Option<SharedString>,
353 pub(crate) is_wsl: bool,
354 pub(crate) is_devcontainer: bool,
355}
356
357impl RenderOnce for SshConnectionHeader {
358 fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
359 let theme = cx.theme();
360
361 let mut header_color = theme.colors().text;
362 header_color.fade_out(0.96);
363
364 let (main_label, meta_label) = if let Some(nickname) = self.nickname {
365 (nickname, Some(format!("({})", self.connection_string)))
366 } else {
367 (self.connection_string, None)
368 };
369
370 let icon = if self.is_wsl {
371 IconName::Linux
372 } else if self.is_devcontainer {
373 IconName::Box
374 } else {
375 IconName::Server
376 };
377
378 h_flex()
379 .px(DynamicSpacing::Base12.rems(cx))
380 .pt(DynamicSpacing::Base08.rems(cx))
381 .pb(DynamicSpacing::Base04.rems(cx))
382 .rounded_t_sm()
383 .w_full()
384 .gap_1p5()
385 .child(Icon::new(icon).size(IconSize::Small))
386 .child(
387 h_flex()
388 .gap_1()
389 .overflow_x_hidden()
390 .child(
391 div()
392 .max_w_96()
393 .overflow_x_hidden()
394 .text_ellipsis()
395 .child(Headline::new(main_label).size(HeadlineSize::XSmall)),
396 )
397 .children(
398 meta_label.map(|label| {
399 Label::new(label).color(Color::Muted).size(LabelSize::Small)
400 }),
401 )
402 .child(div().overflow_x_hidden().text_ellipsis().children(
403 self.paths.into_iter().map(|path| {
404 Label::new(path.to_string_lossy().into_owned())
405 .size(LabelSize::Small)
406 .color(Color::Muted)
407 }),
408 )),
409 )
410 }
411}
412
413impl Render for RemoteConnectionModal {
414 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl ui::IntoElement {
415 let nickname = self.prompt.read(cx).nickname.clone();
416 let connection_string = self.prompt.read(cx).connection_string.clone();
417 let is_wsl = self.prompt.read(cx).is_wsl;
418 let is_devcontainer = self.prompt.read(cx).is_devcontainer;
419
420 let theme = cx.theme().clone();
421 let body_color = theme.colors().editor_background;
422
423 v_flex()
424 .elevation_3(cx)
425 .w(rems(34.))
426 .border_1()
427 .border_color(theme.colors().border)
428 .key_context("SshConnectionModal")
429 .track_focus(&self.focus_handle(cx))
430 .on_action(cx.listener(Self::dismiss))
431 .on_action(cx.listener(Self::confirm))
432 .child(
433 SshConnectionHeader {
434 paths: self.paths.clone(),
435 connection_string,
436 nickname,
437 is_wsl,
438 is_devcontainer,
439 }
440 .render(window, cx),
441 )
442 .child(
443 div()
444 .w_full()
445 .bg(body_color)
446 .border_y_1()
447 .border_color(theme.colors().border_variant)
448 .child(self.prompt.clone()),
449 )
450 .child(
451 div().w_full().py_1().child(
452 ListItem::new("li-devcontainer-go-back")
453 .inset(true)
454 .spacing(ui::ListItemSpacing::Sparse)
455 .start_slot(Icon::new(IconName::Close).color(Color::Muted))
456 .child(Label::new("Cancel"))
457 .end_slot(
458 KeyBinding::for_action_in(&menu::Cancel, &self.focus_handle(cx), cx)
459 .size(rems_from_px(12.)),
460 )
461 .on_click(cx.listener(|this, _, window, cx| {
462 this.dismiss(&menu::Cancel, window, cx);
463 })),
464 ),
465 )
466 }
467}
468
469impl Focusable for RemoteConnectionModal {
470 fn focus_handle(&self, cx: &gpui::App) -> gpui::FocusHandle {
471 self.prompt.read(cx).editor.focus_handle(cx)
472 }
473}
474
475impl EventEmitter<DismissEvent> for RemoteConnectionModal {}
476
477impl ModalView for RemoteConnectionModal {
478 fn on_before_dismiss(
479 &mut self,
480 _window: &mut Window,
481 _: &mut Context<Self>,
482 ) -> workspace::DismissDecision {
483 workspace::DismissDecision::Dismiss(self.finished)
484 }
485
486 fn fade_out_background(&self) -> bool {
487 true
488 }
489}
490
491#[derive(Clone)]
492pub struct RemoteClientDelegate {
493 window: AnyWindowHandle,
494 ui: WeakEntity<RemoteConnectionPrompt>,
495 known_password: Option<EncryptedPassword>,
496}
497
498impl remote::RemoteClientDelegate for RemoteClientDelegate {
499 fn ask_password(
500 &self,
501 prompt: String,
502 tx: oneshot::Sender<EncryptedPassword>,
503 cx: &mut AsyncApp,
504 ) {
505 let mut known_password = self.known_password.clone();
506 if let Some(password) = known_password.take() {
507 tx.send(password).ok();
508 } else {
509 self.window
510 .update(cx, |_, window, cx| {
511 self.ui.update(cx, |modal, cx| {
512 modal.set_prompt(prompt, tx, window, cx);
513 })
514 })
515 .ok();
516 }
517 }
518
519 fn set_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
520 self.update_status(status, cx)
521 }
522
523 fn download_server_binary_locally(
524 &self,
525 platform: RemotePlatform,
526 release_channel: ReleaseChannel,
527 version: Option<Version>,
528 cx: &mut AsyncApp,
529 ) -> Task<anyhow::Result<PathBuf>> {
530 let this = self.clone();
531 cx.spawn(async move |cx| {
532 AutoUpdater::download_remote_server_release(
533 release_channel,
534 version.clone(),
535 platform.os,
536 platform.arch,
537 move |status, cx| this.set_status(Some(status), cx),
538 cx,
539 )
540 .await
541 .with_context(|| {
542 format!(
543 "Downloading remote server binary (version: {}, os: {}, arch: {})",
544 version
545 .as_ref()
546 .map(|v| format!("{}", v))
547 .unwrap_or("unknown".to_string()),
548 platform.os,
549 platform.arch,
550 )
551 })
552 })
553 }
554
555 fn get_download_url(
556 &self,
557 platform: RemotePlatform,
558 release_channel: ReleaseChannel,
559 version: Option<Version>,
560 cx: &mut AsyncApp,
561 ) -> Task<Result<Option<String>>> {
562 cx.spawn(async move |cx| {
563 AutoUpdater::get_remote_server_release_url(
564 release_channel,
565 version,
566 platform.os,
567 platform.arch,
568 cx,
569 )
570 .await
571 })
572 }
573}
574
575impl RemoteClientDelegate {
576 fn update_status(&self, status: Option<&str>, cx: &mut AsyncApp) {
577 self.window
578 .update(cx, |_, _, cx| {
579 self.ui.update(cx, |modal, cx| {
580 modal.set_status(status.map(|s| s.to_string()), cx);
581 })
582 })
583 .ok();
584 }
585}
586
587pub fn connect(
588 unique_identifier: ConnectionIdentifier,
589 connection_options: RemoteConnectionOptions,
590 ui: Entity<RemoteConnectionPrompt>,
591 window: &mut Window,
592 cx: &mut App,
593) -> Task<Result<Option<Entity<RemoteClient>>>> {
594 let window = window.window_handle();
595 let known_password = match &connection_options {
596 RemoteConnectionOptions::Ssh(ssh_connection_options) => ssh_connection_options
597 .password
598 .as_deref()
599 .and_then(|pw| pw.try_into().ok()),
600 _ => None,
601 };
602 let (tx, rx) = oneshot::channel();
603 ui.update(cx, |ui, _cx| ui.set_cancellation_tx(tx));
604
605 let delegate = Arc::new(RemoteClientDelegate {
606 window,
607 ui: ui.downgrade(),
608 known_password,
609 });
610
611 cx.spawn(async move |cx| {
612 let connection = remote::connect(connection_options, delegate.clone(), cx).await?;
613 cx.update(|cx| remote::RemoteClient::new(unique_identifier, connection, rx, delegate, cx))?
614 .await
615 })
616}
617
618pub async fn open_remote_project(
619 connection_options: RemoteConnectionOptions,
620 paths: Vec<PathBuf>,
621 app_state: Arc<AppState>,
622 open_options: workspace::OpenOptions,
623 cx: &mut AsyncApp,
624) -> Result<()> {
625 let created_new_window = open_options.replace_window.is_none();
626 let window = if let Some(window) = open_options.replace_window {
627 window
628 } else {
629 let workspace_position = cx
630 .update(|cx| {
631 // todo: These paths are wrong they may have column and line information
632 workspace::remote_workspace_position_from_db(connection_options.clone(), &paths, cx)
633 })?
634 .await
635 .context("fetching ssh workspace position from db")?;
636
637 let mut options =
638 cx.update(|cx| (app_state.build_window_options)(workspace_position.display, cx))?;
639 options.window_bounds = workspace_position.window_bounds;
640
641 cx.open_window(options, |window, cx| {
642 let project = project::Project::local(
643 app_state.client.clone(),
644 app_state.node_runtime.clone(),
645 app_state.user_store.clone(),
646 app_state.languages.clone(),
647 app_state.fs.clone(),
648 None,
649 cx,
650 );
651 cx.new(|cx| {
652 let mut workspace = Workspace::new(None, project, app_state.clone(), window, cx);
653 workspace.centered_layout = workspace_position.centered_layout;
654 workspace
655 })
656 })?
657 };
658
659 loop {
660 let (cancel_tx, cancel_rx) = oneshot::channel();
661 let delegate = window.update(cx, {
662 let paths = paths.clone();
663 let connection_options = connection_options.clone();
664 move |workspace, window, cx| {
665 window.activate_window();
666 workspace.toggle_modal(window, cx, |window, cx| {
667 RemoteConnectionModal::new(&connection_options, paths, window, cx)
668 });
669
670 let ui = workspace
671 .active_modal::<RemoteConnectionModal>(cx)?
672 .read(cx)
673 .prompt
674 .clone();
675
676 ui.update(cx, |ui, _cx| {
677 ui.set_cancellation_tx(cancel_tx);
678 });
679
680 Some(Arc::new(RemoteClientDelegate {
681 window: window.window_handle(),
682 ui: ui.downgrade(),
683 known_password: if let RemoteConnectionOptions::Ssh(options) =
684 &connection_options
685 {
686 options
687 .password
688 .as_deref()
689 .and_then(|pw| EncryptedPassword::try_from(pw).ok())
690 } else {
691 None
692 },
693 }))
694 }
695 })?;
696
697 let Some(delegate) = delegate else { break };
698
699 let remote_connection =
700 match remote::connect(connection_options.clone(), delegate.clone(), cx).await {
701 Ok(connection) => connection,
702 Err(e) => {
703 window
704 .update(cx, |workspace, _, cx| {
705 if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
706 ui.update(cx, |modal, cx| modal.finished(cx))
707 }
708 })
709 .ok();
710 log::error!("Failed to open project: {e:#}");
711 let response = window
712 .update(cx, |_, window, cx| {
713 window.prompt(
714 PromptLevel::Critical,
715 match connection_options {
716 RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",
717 RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",
718 RemoteConnectionOptions::Docker(_) => {
719 "Failed to connect to Dev Container"
720 }
721 },
722 Some(&format!("{e:#}")),
723 &["Retry", "Cancel"],
724 cx,
725 )
726 })?
727 .await;
728
729 if response == Ok(0) {
730 continue;
731 }
732
733 if created_new_window {
734 window
735 .update(cx, |_, window, _| window.remove_window())
736 .ok();
737 }
738 break;
739 }
740 };
741
742 let (paths, paths_with_positions) =
743 determine_paths_with_positions(&remote_connection, paths.clone()).await;
744
745 let opened_items = cx
746 .update(|cx| {
747 workspace::open_remote_project_with_new_connection(
748 window,
749 remote_connection,
750 cancel_rx,
751 delegate.clone(),
752 app_state.clone(),
753 paths.clone(),
754 cx,
755 )
756 })?
757 .await;
758
759 window
760 .update(cx, |workspace, _, cx| {
761 if let Some(ui) = workspace.active_modal::<RemoteConnectionModal>(cx) {
762 ui.update(cx, |modal, cx| modal.finished(cx))
763 }
764 })
765 .ok();
766
767 match opened_items {
768 Err(e) => {
769 log::error!("Failed to open project: {e:#}");
770 let response = window
771 .update(cx, |_, window, cx| {
772 window.prompt(
773 PromptLevel::Critical,
774 match connection_options {
775 RemoteConnectionOptions::Ssh(_) => "Failed to connect over SSH",
776 RemoteConnectionOptions::Wsl(_) => "Failed to connect to WSL",
777 RemoteConnectionOptions::Docker(_) => {
778 "Failed to connect to Dev Container"
779 }
780 },
781 Some(&format!("{e:#}")),
782 &["Retry", "Cancel"],
783 cx,
784 )
785 })?
786 .await;
787 if response == Ok(0) {
788 continue;
789 }
790
791 if created_new_window {
792 window
793 .update(cx, |_, window, _| window.remove_window())
794 .ok();
795 }
796 }
797
798 Ok(items) => {
799 for (item, path) in items.into_iter().zip(paths_with_positions) {
800 let Some(item) = item else {
801 continue;
802 };
803 let Some(row) = path.row else {
804 continue;
805 };
806 if let Some(active_editor) = item.downcast::<Editor>() {
807 window
808 .update(cx, |_, window, cx| {
809 active_editor.update(cx, |editor, cx| {
810 let row = row.saturating_sub(1);
811 let col = path.column.unwrap_or(0).saturating_sub(1);
812 editor.go_to_singleton_buffer_point(
813 Point::new(row, col),
814 window,
815 cx,
816 );
817 });
818 })
819 .ok();
820 }
821 }
822 }
823 }
824
825 window
826 .update(cx, |workspace, _, cx| {
827 if let Some(client) = workspace.project().read(cx).remote_client() {
828 ExtensionStore::global(cx)
829 .update(cx, |store, cx| store.register_remote_client(client, cx));
830 }
831 })
832 .ok();
833
834 break;
835 }
836
837 // Already showed the error to the user
838 Ok(())
839}
840
841pub(crate) async fn determine_paths_with_positions(
842 remote_connection: &Arc<dyn RemoteConnection>,
843 mut paths: Vec<PathBuf>,
844) -> (Vec<PathBuf>, Vec<PathWithPosition>) {
845 let mut paths_with_positions = Vec::<PathWithPosition>::new();
846 for path in &mut paths {
847 if let Some(path_str) = path.to_str() {
848 let path_with_position = PathWithPosition::parse_str(&path_str);
849 if path_with_position.row.is_some() {
850 if !path_exists(&remote_connection, &path).await {
851 *path = path_with_position.path.clone();
852 paths_with_positions.push(path_with_position);
853 continue;
854 }
855 }
856 }
857 paths_with_positions.push(PathWithPosition::from_path(path.clone()))
858 }
859 (paths, paths_with_positions)
860}
861
862async fn path_exists(connection: &Arc<dyn RemoteConnection>, path: &Path) -> bool {
863 let Ok(command) = connection.build_command(
864 Some("test".to_string()),
865 &["-e".to_owned(), path.to_string_lossy().to_string()],
866 &Default::default(),
867 None,
868 None,
869 ) else {
870 return false;
871 };
872 let Ok(mut child) = util::command::new_smol_command(command.program)
873 .args(command.args)
874 .envs(command.env)
875 .spawn()
876 else {
877 return false;
878 };
879 child.status().await.is_ok_and(|status| status.success())
880}