dev_servers.rs

   1use std::collections::HashMap;
   2use std::path::PathBuf;
   3use std::time::Duration;
   4
   5use anyhow::anyhow;
   6use anyhow::Context;
   7use anyhow::Result;
   8use client::Client;
   9use dev_server_projects::{DevServer, DevServerId, DevServerProject, DevServerProjectId};
  10use editor::Editor;
  11use gpui::AsyncWindowContext;
  12use gpui::PathPromptOptions;
  13use gpui::Subscription;
  14use gpui::Task;
  15use gpui::WeakView;
  16use gpui::{
  17    percentage, Animation, AnimationExt, AnyElement, AppContext, DismissEvent, EventEmitter,
  18    FocusHandle, FocusableView, Model, ScrollHandle, Transformation, View, ViewContext,
  19};
  20use markdown::Markdown;
  21use markdown::MarkdownStyle;
  22use project::terminals::wrap_for_ssh;
  23use project::terminals::SshCommand;
  24use rpc::proto::RegenerateDevServerTokenResponse;
  25use rpc::{
  26    proto::{CreateDevServerResponse, DevServerStatus},
  27    ErrorCode, ErrorExt,
  28};
  29use settings::update_settings_file;
  30use settings::Settings;
  31use task::HideStrategy;
  32use task::RevealStrategy;
  33use task::SpawnInTerminal;
  34use terminal_view::terminal_panel::TerminalPanel;
  35use ui::ElevationIndex;
  36use ui::Section;
  37use ui::{
  38    prelude::*, Indicator, List, ListHeader, ListItem, Modal, ModalFooter, ModalHeader,
  39    RadioWithLabel, Tooltip,
  40};
  41use ui_input::{FieldLabelLayout, TextField};
  42use util::ResultExt;
  43use workspace::OpenOptions;
  44use workspace::{notifications::DetachAndPromptErr, AppState, ModalView, Workspace, WORKSPACE_DB};
  45
  46use crate::open_dev_server_project;
  47use crate::ssh_connections::connect_over_ssh;
  48use crate::ssh_connections::open_ssh_project;
  49use crate::ssh_connections::RemoteSettingsContent;
  50use crate::ssh_connections::SshConnection;
  51use crate::ssh_connections::SshConnectionModal;
  52use crate::ssh_connections::SshProject;
  53use crate::ssh_connections::SshPrompt;
  54use crate::ssh_connections::SshSettings;
  55use crate::OpenRemote;
  56
  57pub struct DevServerProjects {
  58    mode: Mode,
  59    focus_handle: FocusHandle,
  60    scroll_handle: ScrollHandle,
  61    dev_server_store: Model<dev_server_projects::Store>,
  62    workspace: WeakView<Workspace>,
  63    project_path_input: View<Editor>,
  64    dev_server_name_input: View<TextField>,
  65    markdown: View<Markdown>,
  66    _dev_server_subscription: Subscription,
  67}
  68
  69#[derive(Default)]
  70struct CreateDevServer {
  71    creating: Option<Task<Option<()>>>,
  72    dev_server_id: Option<DevServerId>,
  73    access_token: Option<String>,
  74    ssh_prompt: Option<View<SshPrompt>>,
  75    kind: NewServerKind,
  76}
  77
  78struct CreateDevServerProject {
  79    dev_server_id: DevServerId,
  80    creating: bool,
  81    _opening: Option<Subscription>,
  82}
  83
  84enum Mode {
  85    Default(Option<CreateDevServerProject>),
  86    CreateDevServer(CreateDevServer),
  87}
  88
  89#[derive(Default, PartialEq, Eq, Clone, Copy)]
  90enum NewServerKind {
  91    DirectSSH,
  92    #[default]
  93    LegacySSH,
  94    Manual,
  95}
  96
  97impl DevServerProjects {
  98    pub fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
  99        workspace.register_action(|workspace, _: &OpenRemote, cx| {
 100            let handle = cx.view().downgrade();
 101            workspace.toggle_modal(cx, |cx| Self::new(cx, handle))
 102        });
 103    }
 104
 105    pub fn open(workspace: View<Workspace>, cx: &mut WindowContext) {
 106        workspace.update(cx, |workspace, cx| {
 107            let handle = cx.view().downgrade();
 108            workspace.toggle_modal(cx, |cx| Self::new(cx, handle))
 109        })
 110    }
 111
 112    pub fn new(cx: &mut ViewContext<Self>, workspace: WeakView<Workspace>) -> Self {
 113        let project_path_input = cx.new_view(|cx| {
 114            let mut editor = Editor::single_line(cx);
 115            editor.set_placeholder_text("Project path (~/work/zed, /workspace/zed, …)", cx);
 116            editor
 117        });
 118        let dev_server_name_input = cx.new_view(|cx| {
 119            TextField::new(cx, "Name", "192.168.0.1").with_label(FieldLabelLayout::Hidden)
 120        });
 121
 122        let focus_handle = cx.focus_handle();
 123        let dev_server_store = dev_server_projects::Store::global(cx);
 124
 125        let subscription = cx.observe(&dev_server_store, |_, _, cx| {
 126            cx.notify();
 127        });
 128
 129        let mut base_style = cx.text_style();
 130        base_style.refine(&gpui::TextStyleRefinement {
 131            color: Some(cx.theme().colors().editor_foreground),
 132            ..Default::default()
 133        });
 134
 135        let markdown_style = MarkdownStyle {
 136            base_text_style: base_style,
 137            code_block: gpui::StyleRefinement {
 138                text: Some(gpui::TextStyleRefinement {
 139                    font_family: Some("Zed Plex Mono".into()),
 140                    ..Default::default()
 141                }),
 142                ..Default::default()
 143            },
 144            link: gpui::TextStyleRefinement {
 145                color: Some(Color::Accent.color(cx)),
 146                ..Default::default()
 147            },
 148            syntax: cx.theme().syntax().clone(),
 149            selection_background_color: cx.theme().players().local().selection,
 150            ..Default::default()
 151        };
 152        let markdown =
 153            cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx, None));
 154
 155        Self {
 156            mode: Mode::Default(None),
 157            focus_handle,
 158            scroll_handle: ScrollHandle::new(),
 159            dev_server_store,
 160            project_path_input,
 161            dev_server_name_input,
 162            markdown,
 163            workspace,
 164            _dev_server_subscription: subscription,
 165        }
 166    }
 167
 168    pub fn create_dev_server_project(
 169        &mut self,
 170        dev_server_id: DevServerId,
 171        cx: &mut ViewContext<Self>,
 172    ) {
 173        let mut path = self.project_path_input.read(cx).text(cx).trim().to_string();
 174
 175        if path.is_empty() {
 176            return;
 177        }
 178
 179        if !path.starts_with('/') && !path.starts_with('~') {
 180            path = format!("~/{}", path);
 181        }
 182
 183        if self
 184            .dev_server_store
 185            .read(cx)
 186            .projects_for_server(dev_server_id)
 187            .iter()
 188            .any(|p| p.paths.iter().any(|p| p == &path))
 189        {
 190            cx.spawn(|_, mut cx| async move {
 191                cx.prompt(
 192                    gpui::PromptLevel::Critical,
 193                    "Failed to create project",
 194                    Some(&format!("{} is already open on this dev server.", path)),
 195                    &["Ok"],
 196                )
 197                .await
 198            })
 199            .detach_and_log_err(cx);
 200            return;
 201        }
 202
 203        let create = {
 204            let path = path.clone();
 205            self.dev_server_store.update(cx, |store, cx| {
 206                store.create_dev_server_project(dev_server_id, path, cx)
 207            })
 208        };
 209
 210        cx.spawn(|this, mut cx| async move {
 211            let result = create.await;
 212            this.update(&mut cx, |this, cx| {
 213                if let Ok(result) = &result {
 214                    if let Some(dev_server_project_id) =
 215                        result.dev_server_project.as_ref().map(|p| p.id)
 216                    {
 217                        let subscription =
 218                            cx.observe(&this.dev_server_store, move |this, store, cx| {
 219                                if let Some(project_id) = store
 220                                    .read(cx)
 221                                    .dev_server_project(DevServerProjectId(dev_server_project_id))
 222                                    .and_then(|p| p.project_id)
 223                                {
 224                                    this.project_path_input.update(cx, |editor, cx| {
 225                                        editor.set_text("", cx);
 226                                    });
 227                                    this.mode = Mode::Default(None);
 228                                    if let Some(app_state) = AppState::global(cx).upgrade() {
 229                                        workspace::join_dev_server_project(
 230                                            DevServerProjectId(dev_server_project_id),
 231                                            project_id,
 232                                            app_state,
 233                                            None,
 234                                            cx,
 235                                        )
 236                                        .detach_and_prompt_err(
 237                                            "Could not join project",
 238                                            cx,
 239                                            |_, _| None,
 240                                        )
 241                                    }
 242                                }
 243                            });
 244
 245                        this.mode = Mode::Default(Some(CreateDevServerProject {
 246                            dev_server_id,
 247                            creating: true,
 248                            _opening: Some(subscription),
 249                        }));
 250                    }
 251                } else {
 252                    this.mode = Mode::Default(Some(CreateDevServerProject {
 253                        dev_server_id,
 254                        creating: false,
 255                        _opening: None,
 256                    }));
 257                }
 258            })
 259            .log_err();
 260            result
 261        })
 262        .detach_and_prompt_err("Failed to create project", cx, move |e, _| {
 263            match e.error_code() {
 264                ErrorCode::DevServerOffline => Some(
 265                    "The dev server is offline. Please log in and check it is connected."
 266                        .to_string(),
 267                ),
 268                ErrorCode::DevServerProjectPathDoesNotExist => {
 269                    Some(format!("The path `{}` does not exist on the server.", path))
 270                }
 271                _ => None,
 272            }
 273        });
 274
 275        self.mode = Mode::Default(Some(CreateDevServerProject {
 276            dev_server_id,
 277            creating: true,
 278            _opening: None,
 279        }));
 280    }
 281
 282    fn create_ssh_server(&mut self, cx: &mut ViewContext<Self>) {
 283        let host = get_text(&self.dev_server_name_input, cx);
 284        if host.is_empty() {
 285            return;
 286        }
 287
 288        let mut host = host.trim_start_matches("ssh ");
 289        let mut username: Option<String> = None;
 290        let mut port: Option<u16> = None;
 291
 292        if let Some((u, rest)) = host.split_once('@') {
 293            host = rest;
 294            username = Some(u.to_string());
 295        }
 296        if let Some((rest, p)) = host.split_once(':') {
 297            host = rest;
 298            port = p.parse().ok()
 299        }
 300
 301        if let Some((rest, p)) = host.split_once(" -p") {
 302            host = rest;
 303            port = p.trim().parse().ok()
 304        }
 305
 306        let connection_options = remote::SshConnectionOptions {
 307            host: host.to_string(),
 308            username: username.clone(),
 309            port,
 310            password: None,
 311        };
 312        let ssh_prompt = cx.new_view(|cx| SshPrompt::new(&connection_options, cx));
 313
 314        let connection = connect_over_ssh(
 315            connection_options.dev_server_identifier(),
 316            connection_options.clone(),
 317            ssh_prompt.clone(),
 318            cx,
 319        )
 320        .prompt_err("Failed to connect", cx, |_, _| None);
 321
 322        let creating = cx.spawn(move |this, mut cx| async move {
 323            match connection.await {
 324                Some(_) => this
 325                    .update(&mut cx, |this, cx| {
 326                        this.add_ssh_server(connection_options, cx);
 327                        this.mode = Mode::Default(None);
 328                        cx.notify()
 329                    })
 330                    .log_err(),
 331                None => this
 332                    .update(&mut cx, |this, cx| {
 333                        this.mode = Mode::CreateDevServer(CreateDevServer {
 334                            kind: NewServerKind::DirectSSH,
 335                            ..Default::default()
 336                        });
 337                        cx.notify()
 338                    })
 339                    .log_err(),
 340            };
 341            None
 342        });
 343        self.mode = Mode::CreateDevServer(CreateDevServer {
 344            kind: NewServerKind::DirectSSH,
 345            ssh_prompt: Some(ssh_prompt.clone()),
 346            creating: Some(creating),
 347            ..Default::default()
 348        });
 349    }
 350
 351    fn create_ssh_project(
 352        &mut self,
 353        ix: usize,
 354        ssh_connection: SshConnection,
 355        cx: &mut ViewContext<Self>,
 356    ) {
 357        let Some(workspace) = self.workspace.upgrade() else {
 358            return;
 359        };
 360
 361        let connection_options = ssh_connection.into();
 362        workspace.update(cx, |_, cx| {
 363            cx.defer(move |workspace, cx| {
 364                workspace.toggle_modal(cx, |cx| SshConnectionModal::new(&connection_options, cx));
 365                let prompt = workspace
 366                    .active_modal::<SshConnectionModal>(cx)
 367                    .unwrap()
 368                    .read(cx)
 369                    .prompt
 370                    .clone();
 371
 372                let connect = connect_over_ssh(
 373                    connection_options.dev_server_identifier(),
 374                    connection_options,
 375                    prompt,
 376                    cx,
 377                )
 378                .prompt_err("Failed to connect", cx, |_, _| None);
 379                cx.spawn(|workspace, mut cx| async move {
 380                    let Some(session) = connect.await else {
 381                        workspace
 382                            .update(&mut cx, |workspace, cx| {
 383                                let weak = cx.view().downgrade();
 384                                workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, weak));
 385                            })
 386                            .log_err();
 387                        return;
 388                    };
 389                    let Ok((app_state, project, paths)) =
 390                        workspace.update(&mut cx, |workspace, cx| {
 391                            let app_state = workspace.app_state().clone();
 392                            let project = project::Project::ssh(
 393                                session,
 394                                app_state.client.clone(),
 395                                app_state.node_runtime.clone(),
 396                                app_state.user_store.clone(),
 397                                app_state.languages.clone(),
 398                                app_state.fs.clone(),
 399                                cx,
 400                            );
 401                            let paths = workspace.prompt_for_open_path(
 402                                PathPromptOptions {
 403                                    files: true,
 404                                    directories: true,
 405                                    multiple: true,
 406                                },
 407                                project::DirectoryLister::Project(project.clone()),
 408                                cx,
 409                            );
 410                            (app_state, project, paths)
 411                        })
 412                    else {
 413                        return;
 414                    };
 415
 416                    let Ok(Some(paths)) = paths.await else {
 417                        workspace
 418                            .update(&mut cx, |workspace, cx| {
 419                                let weak = cx.view().downgrade();
 420                                workspace.toggle_modal(cx, |cx| DevServerProjects::new(cx, weak));
 421                            })
 422                            .log_err();
 423                        return;
 424                    };
 425
 426                    let Some(options) = cx
 427                        .update(|cx| (app_state.build_window_options)(None, cx))
 428                        .log_err()
 429                    else {
 430                        return;
 431                    };
 432
 433                    cx.open_window(options, |cx| {
 434                        cx.activate_window();
 435
 436                        let fs = app_state.fs.clone();
 437                        update_settings_file::<SshSettings>(fs, cx, {
 438                            let paths = paths
 439                                .iter()
 440                                .map(|path| path.to_string_lossy().to_string())
 441                                .collect();
 442                            move |setting, _| {
 443                                if let Some(server) = setting
 444                                    .ssh_connections
 445                                    .as_mut()
 446                                    .and_then(|connections| connections.get_mut(ix))
 447                                {
 448                                    server.projects.push(SshProject { paths })
 449                                }
 450                            }
 451                        });
 452
 453                        let tasks = paths
 454                            .into_iter()
 455                            .map(|path| {
 456                                project.update(cx, |project, cx| {
 457                                    project.find_or_create_worktree(&path, true, cx)
 458                                })
 459                            })
 460                            .collect::<Vec<_>>();
 461                        cx.spawn(|_| async move {
 462                            for task in tasks {
 463                                task.await?;
 464                            }
 465                            Ok(())
 466                        })
 467                        .detach_and_prompt_err(
 468                            "Failed to open path",
 469                            cx,
 470                            |_, _| None,
 471                        );
 472
 473                        cx.new_view(|cx| {
 474                            Workspace::new(None, project.clone(), app_state.clone(), cx)
 475                        })
 476                    })
 477                    .log_err();
 478                })
 479                .detach()
 480            })
 481        })
 482    }
 483
 484    fn create_or_update_dev_server(
 485        &mut self,
 486        kind: NewServerKind,
 487        existing_id: Option<DevServerId>,
 488        access_token: Option<String>,
 489        cx: &mut ViewContext<Self>,
 490    ) {
 491        let name = get_text(&self.dev_server_name_input, cx);
 492        if name.is_empty() {
 493            return;
 494        }
 495
 496        let manual_setup = match kind {
 497            NewServerKind::DirectSSH => unreachable!(),
 498            NewServerKind::LegacySSH => false,
 499            NewServerKind::Manual => true,
 500        };
 501
 502        let ssh_connection_string = if manual_setup {
 503            None
 504        } else if name.contains(' ') {
 505            Some(name.clone())
 506        } else {
 507            Some(format!("ssh {}", name))
 508        };
 509
 510        let dev_server = self.dev_server_store.update(cx, {
 511            let access_token = access_token.clone();
 512            |store, cx| {
 513                let ssh_connection_string = ssh_connection_string.clone();
 514                if let Some(dev_server_id) = existing_id {
 515                    let rename = store.rename_dev_server(
 516                        dev_server_id,
 517                        name.clone(),
 518                        ssh_connection_string,
 519                        cx,
 520                    );
 521                    let token = if let Some(access_token) = access_token {
 522                        Task::ready(Ok(RegenerateDevServerTokenResponse {
 523                            dev_server_id: dev_server_id.0,
 524                            access_token,
 525                        }))
 526                    } else {
 527                        store.regenerate_dev_server_token(dev_server_id, cx)
 528                    };
 529                    cx.spawn(|_, _| async move {
 530                        rename.await?;
 531                        let response = token.await?;
 532                        Ok(CreateDevServerResponse {
 533                            dev_server_id: dev_server_id.0,
 534                            name,
 535                            access_token: response.access_token,
 536                        })
 537                    })
 538                } else {
 539                    store.create_dev_server(name, ssh_connection_string.clone(), cx)
 540                }
 541            }
 542        });
 543
 544        let workspace = self.workspace.clone();
 545        let store = dev_server_projects::Store::global(cx);
 546
 547        let task = cx
 548            .spawn({
 549                |this, mut cx| async move {
 550                    let result = dev_server.await;
 551
 552                    match result {
 553                        Ok(dev_server) => {
 554                            if let Some(ssh_connection_string) = ssh_connection_string {
 555                                this.update(&mut cx, |this, cx| {
 556                                    if let Mode::CreateDevServer(CreateDevServer {
 557                                        access_token,
 558                                        dev_server_id,
 559                                        ..
 560                                    }) = &mut this.mode
 561                                    {
 562                                        access_token.replace(dev_server.access_token.clone());
 563                                        dev_server_id
 564                                            .replace(DevServerId(dev_server.dev_server_id));
 565                                    }
 566                                    cx.notify();
 567                                })?;
 568
 569                                spawn_ssh_task(
 570                                    workspace
 571                                        .upgrade()
 572                                        .ok_or_else(|| anyhow!("workspace dropped"))?,
 573                                    store,
 574                                    DevServerId(dev_server.dev_server_id),
 575                                    ssh_connection_string,
 576                                    dev_server.access_token.clone(),
 577                                    &mut cx,
 578                                )
 579                                .await
 580                                .log_err();
 581                            }
 582
 583                            this.update(&mut cx, |this, cx| {
 584                                this.focus_handle.focus(cx);
 585                                this.mode = Mode::CreateDevServer(CreateDevServer {
 586                                    dev_server_id: Some(DevServerId(dev_server.dev_server_id)),
 587                                    access_token: Some(dev_server.access_token),
 588                                    kind,
 589                                    ..Default::default()
 590                                });
 591                                cx.notify();
 592                            })?;
 593                            Ok(())
 594                        }
 595                        Err(e) => {
 596                            this.update(&mut cx, |this, cx| {
 597                                this.mode = Mode::CreateDevServer(CreateDevServer {
 598                                    dev_server_id: existing_id,
 599                                    access_token: None,
 600                                    kind,
 601                                    ..Default::default()
 602                                });
 603                                cx.notify()
 604                            })
 605                            .log_err();
 606
 607                            Err(e)
 608                        }
 609                    }
 610                }
 611            })
 612            .prompt_err("Failed to create server", cx, |_, _| None);
 613
 614        self.mode = Mode::CreateDevServer(CreateDevServer {
 615            creating: Some(task),
 616            dev_server_id: existing_id,
 617            access_token,
 618            kind,
 619            ..Default::default()
 620        });
 621        cx.notify()
 622    }
 623
 624    fn delete_dev_server(&mut self, id: DevServerId, cx: &mut ViewContext<Self>) {
 625        let store = self.dev_server_store.read(cx);
 626        let prompt = if store.projects_for_server(id).is_empty()
 627            && store
 628                .dev_server(id)
 629                .is_some_and(|server| server.status == DevServerStatus::Offline)
 630        {
 631            None
 632        } else {
 633            Some(cx.prompt(
 634                gpui::PromptLevel::Warning,
 635                "Are you sure?",
 636                Some("This will delete the dev server and all of its remote projects."),
 637                &["Delete", "Cancel"],
 638            ))
 639        };
 640
 641        cx.spawn(|this, mut cx| async move {
 642            if let Some(prompt) = prompt {
 643                if prompt.await? != 0 {
 644                    return Ok(());
 645                }
 646            }
 647
 648            let project_ids: Vec<DevServerProjectId> = this.update(&mut cx, |this, cx| {
 649                this.dev_server_store.update(cx, |store, _| {
 650                    store
 651                        .projects_for_server(id)
 652                        .into_iter()
 653                        .map(|project| project.id)
 654                        .collect()
 655                })
 656            })?;
 657
 658            this.update(&mut cx, |this, cx| {
 659                this.dev_server_store
 660                    .update(cx, |store, cx| store.delete_dev_server(id, cx))
 661            })?
 662            .await?;
 663
 664            for id in project_ids {
 665                WORKSPACE_DB
 666                    .delete_workspace_by_dev_server_project_id(id)
 667                    .await
 668                    .log_err();
 669            }
 670            Ok(())
 671        })
 672        .detach_and_prompt_err("Failed to delete dev server", cx, |_, _| None);
 673    }
 674
 675    fn delete_dev_server_project(&mut self, id: DevServerProjectId, cx: &mut ViewContext<Self>) {
 676        let answer = cx.prompt(
 677            gpui::PromptLevel::Warning,
 678            "Delete this project?",
 679            Some("This will delete the remote project. You can always re-add it later."),
 680            &["Delete", "Cancel"],
 681        );
 682
 683        cx.spawn(|this, mut cx| async move {
 684            let answer = answer.await?;
 685
 686            if answer != 0 {
 687                return Ok(());
 688            }
 689
 690            this.update(&mut cx, |this, cx| {
 691                this.dev_server_store
 692                    .update(cx, |store, cx| store.delete_dev_server_project(id, cx))
 693            })?
 694            .await?;
 695
 696            WORKSPACE_DB
 697                .delete_workspace_by_dev_server_project_id(id)
 698                .await
 699                .log_err();
 700
 701            Ok(())
 702        })
 703        .detach_and_prompt_err("Failed to delete dev server project", cx, |_, _| None);
 704    }
 705
 706    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 707        match &self.mode {
 708            Mode::Default(None) => {}
 709            Mode::Default(Some(create_project)) => {
 710                self.create_dev_server_project(create_project.dev_server_id, cx);
 711            }
 712            Mode::CreateDevServer(state) => {
 713                if let Some(prompt) = state.ssh_prompt.as_ref() {
 714                    prompt.update(cx, |prompt, cx| {
 715                        prompt.confirm(cx);
 716                    });
 717                    return;
 718                }
 719                if state.kind == NewServerKind::DirectSSH {
 720                    self.create_ssh_server(cx);
 721                    return;
 722                }
 723                if state.creating.is_none() || state.dev_server_id.is_some() {
 724                    self.create_or_update_dev_server(
 725                        state.kind,
 726                        state.dev_server_id,
 727                        state.access_token.clone(),
 728                        cx,
 729                    );
 730                }
 731            }
 732        }
 733    }
 734
 735    fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
 736        match &self.mode {
 737            Mode::Default(None) => cx.emit(DismissEvent),
 738            Mode::CreateDevServer(state) if state.ssh_prompt.is_some() => {
 739                self.mode = Mode::CreateDevServer(CreateDevServer {
 740                    kind: NewServerKind::DirectSSH,
 741                    ..Default::default()
 742                });
 743                cx.notify();
 744            }
 745            _ => {
 746                self.mode = Mode::Default(None);
 747                self.focus_handle(cx).focus(cx);
 748                cx.notify();
 749            }
 750        }
 751    }
 752
 753    fn render_dev_server(
 754        &mut self,
 755        dev_server: &DevServer,
 756        create_project: Option<bool>,
 757        cx: &mut ViewContext<Self>,
 758    ) -> impl IntoElement {
 759        let dev_server_id = dev_server.id;
 760        let status = dev_server.status;
 761        let dev_server_name = dev_server.name.clone();
 762        let kind = if dev_server.ssh_connection_string.is_some() {
 763            NewServerKind::LegacySSH
 764        } else {
 765            NewServerKind::Manual
 766        };
 767
 768        v_flex()
 769            .w_full()
 770            .child(
 771                h_flex().group("dev-server").justify_between().child(
 772                    h_flex()
 773                        .gap_2()
 774                        .child(
 775                            div()
 776                                .id(("status", dev_server.id.0))
 777                                .relative()
 778                                .child(Icon::new(IconName::Server).size(IconSize::Small))
 779                                .child(div().absolute().bottom_0().left(rems_from_px(8.0)).child(
 780                                    Indicator::dot().color(match status {
 781                                        DevServerStatus::Online => Color::Created,
 782                                        DevServerStatus::Offline => Color::Hidden,
 783                                    }),
 784                                ))
 785                                .tooltip(move |cx| {
 786                                    Tooltip::text(
 787                                        match status {
 788                                            DevServerStatus::Online => "Online",
 789                                            DevServerStatus::Offline => "Offline",
 790                                        },
 791                                        cx,
 792                                    )
 793                                }),
 794                        )
 795                        .child(
 796                            div()
 797                                .max_w(rems(26.))
 798                                .overflow_hidden()
 799                                .whitespace_nowrap()
 800                                .child(Label::new(dev_server_name.clone())),
 801                        )
 802                        .child(
 803                            h_flex()
 804                                .visible_on_hover("dev-server")
 805                                .gap_1()
 806                                .child(if dev_server.ssh_connection_string.is_some() {
 807                                    let dev_server = dev_server.clone();
 808                                    IconButton::new("reconnect-dev-server", IconName::ArrowCircle)
 809                                        .on_click(cx.listener(move |this, _, cx| {
 810                                            let Some(workspace) = this.workspace.upgrade() else {
 811                                                return;
 812                                            };
 813
 814                                            reconnect_to_dev_server(
 815                                                workspace,
 816                                                dev_server.clone(),
 817                                                cx,
 818                                            )
 819                                            .detach_and_prompt_err(
 820                                                "Failed to reconnect",
 821                                                cx,
 822                                                |_, _| None,
 823                                            );
 824                                        }))
 825                                        .tooltip(|cx| Tooltip::text("Reconnect", cx))
 826                                } else {
 827                                    IconButton::new("edit-dev-server", IconName::Pencil)
 828                                        .on_click(cx.listener(move |this, _, cx| {
 829                                            this.mode = Mode::CreateDevServer(CreateDevServer {
 830                                                dev_server_id: Some(dev_server_id),
 831                                                kind,
 832                                                ..Default::default()
 833                                            });
 834                                            let dev_server_name = dev_server_name.clone();
 835                                            this.dev_server_name_input.update(
 836                                                cx,
 837                                                move |input, cx| {
 838                                                    input.editor().update(cx, move |editor, cx| {
 839                                                        editor.set_text(dev_server_name, cx)
 840                                                    })
 841                                                },
 842                                            )
 843                                        }))
 844                                        .tooltip(|cx| Tooltip::text("Edit dev server", cx))
 845                                })
 846                                .child({
 847                                    let dev_server_id = dev_server.id;
 848                                    IconButton::new("remove-dev-server", IconName::Trash)
 849                                        .on_click(cx.listener(move |this, _, cx| {
 850                                            this.delete_dev_server(dev_server_id, cx)
 851                                        }))
 852                                        .tooltip(|cx| Tooltip::text("Remove dev server", cx))
 853                                }),
 854                        ),
 855                ),
 856            )
 857            .child(
 858                v_flex()
 859                    .w_full()
 860                    .bg(cx.theme().colors().background)
 861                    .border_1()
 862                    .border_color(cx.theme().colors().border_variant)
 863                    .rounded_md()
 864                    .my_1()
 865                    .py_0p5()
 866                    .px_3()
 867                    .child(
 868                        List::new()
 869                            .empty_message("No projects.")
 870                            .children(
 871                                self.dev_server_store
 872                                    .read(cx)
 873                                    .projects_for_server(dev_server.id)
 874                                    .iter()
 875                                    .map(|p| self.render_dev_server_project(p, cx)),
 876                            )
 877                            .when(
 878                                create_project.is_none()
 879                                    && dev_server.status == DevServerStatus::Online,
 880                                |el| {
 881                                    el.child(
 882                                        ListItem::new("new-remote_project")
 883                                            .start_slot(Icon::new(IconName::Plus))
 884                                            .child(Label::new("Open folder…"))
 885                                            .on_click(cx.listener(move |this, _, cx| {
 886                                                this.mode =
 887                                                    Mode::Default(Some(CreateDevServerProject {
 888                                                        dev_server_id,
 889                                                        creating: false,
 890                                                        _opening: None,
 891                                                    }));
 892                                                this.project_path_input
 893                                                    .read(cx)
 894                                                    .focus_handle(cx)
 895                                                    .focus(cx);
 896                                                cx.notify();
 897                                            })),
 898                                    )
 899                                },
 900                            )
 901                            .when_some(create_project, |el, creating| {
 902                                el.child(self.render_create_new_project(creating, cx))
 903                            }),
 904                    ),
 905            )
 906    }
 907
 908    fn render_ssh_connection(
 909        &mut self,
 910        ix: usize,
 911        ssh_connection: SshConnection,
 912        cx: &mut ViewContext<Self>,
 913    ) -> impl IntoElement {
 914        v_flex()
 915            .w_full()
 916            .child(
 917                h_flex().group("ssh-server").justify_between().child(
 918                    h_flex()
 919                        .gap_2()
 920                        .child(
 921                            div()
 922                                .id(("status", ix))
 923                                .relative()
 924                                .child(Icon::new(IconName::Server).size(IconSize::Small)),
 925                        )
 926                        .child(
 927                            div()
 928                                .max_w(rems(26.))
 929                                .overflow_hidden()
 930                                .whitespace_nowrap()
 931                                .child(Label::new(ssh_connection.host.clone())),
 932                        )
 933                        .child(h_flex().visible_on_hover("ssh-server").gap_1().child({
 934                            IconButton::new("remove-dev-server", IconName::Trash)
 935                                .on_click(
 936                                    cx.listener(move |this, _, cx| this.delete_ssh_server(ix, cx)),
 937                                )
 938                                .tooltip(|cx| Tooltip::text("Remove Dev Server", cx))
 939                        })),
 940                ),
 941            )
 942            .child(
 943                v_flex()
 944                    .w_full()
 945                    .bg(cx.theme().colors().background)
 946                    .border_1()
 947                    .border_color(cx.theme().colors().border_variant)
 948                    .rounded_md()
 949                    .my_1()
 950                    .py_0p5()
 951                    .px_3()
 952                    .child(
 953                        List::new()
 954                            .empty_message("No projects.")
 955                            .children(ssh_connection.projects.iter().enumerate().map(|(pix, p)| {
 956                                self.render_ssh_project(ix, &ssh_connection, pix, p, cx)
 957                            }))
 958                            .child(
 959                                ListItem::new("new-remote_project")
 960                                    .start_slot(Icon::new(IconName::Plus))
 961                                    .child(Label::new("Open folder…"))
 962                                    .on_click(cx.listener(move |this, _, cx| {
 963                                        this.create_ssh_project(ix, ssh_connection.clone(), cx);
 964                                    })),
 965                            ),
 966                    ),
 967            )
 968    }
 969
 970    fn render_ssh_project(
 971        &self,
 972        server_ix: usize,
 973        server: &SshConnection,
 974        ix: usize,
 975        project: &SshProject,
 976        cx: &ViewContext<Self>,
 977    ) -> impl IntoElement {
 978        let project = project.clone();
 979        let server = server.clone();
 980        ListItem::new(("remote-project", ix))
 981            .start_slot(Icon::new(IconName::FileTree))
 982            .child(Label::new(project.paths.join(", ")))
 983            .on_click(cx.listener(move |this, _, cx| {
 984                let Some(app_state) = this
 985                    .workspace
 986                    .update(cx, |workspace, _| workspace.app_state().clone())
 987                    .log_err()
 988                else {
 989                    return;
 990                };
 991                let project = project.clone();
 992                let server = server.clone();
 993                cx.spawn(|_, mut cx| async move {
 994                    let result = open_ssh_project(
 995                        server.into(),
 996                        project.paths.into_iter().map(PathBuf::from).collect(),
 997                        app_state,
 998                        OpenOptions::default(),
 999                        &mut cx,
1000                    )
1001                    .await;
1002                    if let Err(e) = result {
1003                        log::error!("Failed to connect: {:?}", e);
1004                        cx.prompt(
1005                            gpui::PromptLevel::Critical,
1006                            "Failed to connect",
1007                            Some(&e.to_string()),
1008                            &["Ok"],
1009                        )
1010                        .await
1011                        .ok();
1012                    }
1013                })
1014                .detach();
1015            }))
1016            .end_hover_slot::<AnyElement>(Some(
1017                IconButton::new("remove-remote-project", IconName::Trash)
1018                    .on_click(
1019                        cx.listener(move |this, _, cx| this.delete_ssh_project(server_ix, ix, cx)),
1020                    )
1021                    .tooltip(|cx| Tooltip::text("Delete remote project", cx))
1022                    .into_any_element(),
1023            ))
1024    }
1025
1026    fn update_settings_file(
1027        &mut self,
1028        cx: &mut ViewContext<Self>,
1029        f: impl FnOnce(&mut RemoteSettingsContent) + Send + Sync + 'static,
1030    ) {
1031        let Some(fs) = self
1032            .workspace
1033            .update(cx, |workspace, _| workspace.app_state().fs.clone())
1034            .log_err()
1035        else {
1036            return;
1037        };
1038        update_settings_file::<SshSettings>(fs, cx, move |setting, _| f(setting));
1039    }
1040
1041    fn delete_ssh_server(&mut self, server: usize, cx: &mut ViewContext<Self>) {
1042        self.update_settings_file(cx, move |setting| {
1043            if let Some(connections) = setting.ssh_connections.as_mut() {
1044                connections.remove(server);
1045            }
1046        });
1047    }
1048
1049    fn delete_ssh_project(&mut self, server: usize, project: usize, cx: &mut ViewContext<Self>) {
1050        self.update_settings_file(cx, move |setting| {
1051            if let Some(server) = setting
1052                .ssh_connections
1053                .as_mut()
1054                .and_then(|connections| connections.get_mut(server))
1055            {
1056                server.projects.remove(project);
1057            }
1058        });
1059    }
1060
1061    fn add_ssh_server(
1062        &mut self,
1063        connection_options: remote::SshConnectionOptions,
1064        cx: &mut ViewContext<Self>,
1065    ) {
1066        self.update_settings_file(cx, move |setting| {
1067            setting
1068                .ssh_connections
1069                .get_or_insert(Default::default())
1070                .push(SshConnection {
1071                    host: connection_options.host,
1072                    username: connection_options.username,
1073                    port: connection_options.port,
1074                    projects: vec![],
1075                })
1076        });
1077    }
1078
1079    fn render_create_new_project(
1080        &mut self,
1081        creating: bool,
1082        _: &mut ViewContext<Self>,
1083    ) -> impl IntoElement {
1084        ListItem::new("create-remote-project")
1085            .disabled(true)
1086            .start_slot(Icon::new(IconName::FileTree).color(Color::Muted))
1087            .child(self.project_path_input.clone())
1088            .child(div().w(IconSize::Medium.rems()).when(creating, |el| {
1089                el.child(
1090                    Icon::new(IconName::ArrowCircle)
1091                        .size(IconSize::Medium)
1092                        .with_animation(
1093                            "arrow-circle",
1094                            Animation::new(Duration::from_secs(2)).repeat(),
1095                            |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1096                        ),
1097                )
1098            }))
1099    }
1100
1101    fn render_dev_server_project(
1102        &mut self,
1103        project: &DevServerProject,
1104        cx: &mut ViewContext<Self>,
1105    ) -> impl IntoElement {
1106        let dev_server_project_id = project.id;
1107        let project_id = project.project_id;
1108        let is_online = project_id.is_some();
1109
1110        ListItem::new(("remote-project", dev_server_project_id.0))
1111            .start_slot(Icon::new(IconName::FileTree).when(!is_online, |icon| icon.color(Color::Muted)))
1112            .child(
1113                    Label::new(project.paths.join(", "))
1114            )
1115            .on_click(cx.listener(move |_, _, cx| {
1116                if let Some(project_id) = project_id {
1117                    if let Some(app_state) = AppState::global(cx).upgrade() {
1118                        workspace::join_dev_server_project(dev_server_project_id, project_id, app_state, None, cx)
1119                            .detach_and_prompt_err("Could not join project", cx, |_, _| None)
1120                    }
1121                } else {
1122                    cx.spawn(|_, mut cx| async move {
1123                        cx.prompt(gpui::PromptLevel::Critical, "This project is offline", Some("The `zed` instance running on this dev server is not connected. You will have to restart it."), &["Ok"]).await.log_err();
1124                    }).detach();
1125                }
1126            }))
1127            .end_hover_slot::<AnyElement>(Some(IconButton::new("remove-remote-project", IconName::Trash)
1128                .on_click(cx.listener(move |this, _, cx| {
1129                    this.delete_dev_server_project(dev_server_project_id, cx)
1130                }))
1131                .tooltip(|cx| Tooltip::text("Delete remote project", cx)).into_any_element()))
1132    }
1133
1134    fn render_create_dev_server(
1135        &self,
1136        state: &CreateDevServer,
1137        cx: &mut ViewContext<Self>,
1138    ) -> impl IntoElement {
1139        let creating = state.creating.is_some();
1140        let dev_server_id = state.dev_server_id;
1141        let access_token = state.access_token.clone();
1142        let ssh_prompt = state.ssh_prompt.clone();
1143        let use_direct_ssh = SshSettings::get_global(cx).use_direct_ssh()
1144            || Client::global(cx).status().borrow().is_signed_out();
1145
1146        let mut kind = state.kind;
1147        if use_direct_ssh && kind == NewServerKind::LegacySSH {
1148            kind = NewServerKind::DirectSSH;
1149        }
1150
1151        let status = dev_server_id
1152            .map(|id| self.dev_server_store.read(cx).dev_server_status(id))
1153            .unwrap_or_default();
1154
1155        let name = self.dev_server_name_input.update(cx, |input, cx| {
1156            input.editor().update(cx, |editor, cx| {
1157                if editor.text(cx).is_empty() {
1158                    match kind {
1159                        NewServerKind::DirectSSH => editor.set_placeholder_text("ssh host", cx),
1160                        NewServerKind::LegacySSH => editor.set_placeholder_text("ssh host", cx),
1161                        NewServerKind::Manual => editor.set_placeholder_text("example-host", cx),
1162                    }
1163                }
1164                editor.text(cx)
1165            })
1166        });
1167
1168        const MANUAL_SETUP_MESSAGE: &str =
1169            "Generate a token for this server and follow the steps to set Zed up on that machine.";
1170        const SSH_SETUP_MESSAGE: &str =
1171            "Enter the command you use to SSH into this server.\nFor example: `ssh me@my.server` or `ssh me@secret-box:2222`.";
1172
1173        Modal::new("create-dev-server", Some(self.scroll_handle.clone()))
1174            .header(
1175                ModalHeader::new()
1176                    .headline("Create Dev Server")
1177                    .show_back_button(true),
1178            )
1179            .section(
1180                Section::new()
1181                    .header(if kind == NewServerKind::Manual {
1182                        "Server Name".into()
1183                    } else {
1184                        "SSH arguments".into()
1185                    })
1186                    .child(
1187                        div()
1188                            .max_w(rems(16.))
1189                            .child(self.dev_server_name_input.clone()),
1190                    ),
1191            )
1192            .section(
1193                Section::new_contained()
1194                    .header("Connection Method".into())
1195                    .child(
1196                        v_flex()
1197                            .w_full()
1198                            .px_2()
1199                            .gap_y(Spacing::Large.rems(cx))
1200                            .when(ssh_prompt.is_none(), |el| {
1201                                el.child(
1202                                    v_flex()
1203                                        .when(use_direct_ssh, |el| {
1204                                            el.child(RadioWithLabel::new(
1205                                                "use-server-name-in-ssh",
1206                                                Label::new("Connect via SSH (default)"),
1207                                                NewServerKind::DirectSSH == kind,
1208                                                cx.listener({
1209                                                    move |this, _, cx| {
1210                                                        if let Mode::CreateDevServer(
1211                                                            CreateDevServer { kind, .. },
1212                                                        ) = &mut this.mode
1213                                                        {
1214                                                            *kind = NewServerKind::DirectSSH;
1215                                                        }
1216                                                        cx.notify()
1217                                                    }
1218                                                }),
1219                                            ))
1220                                        })
1221                                        .when(!use_direct_ssh, |el| {
1222                                            el.child(RadioWithLabel::new(
1223                                                "use-server-name-in-ssh",
1224                                                Label::new("Configure over SSH (default)"),
1225                                                kind == NewServerKind::LegacySSH,
1226                                                cx.listener({
1227                                                    move |this, _, cx| {
1228                                                        if let Mode::CreateDevServer(
1229                                                            CreateDevServer { kind, .. },
1230                                                        ) = &mut this.mode
1231                                                        {
1232                                                            *kind = NewServerKind::LegacySSH;
1233                                                        }
1234                                                        cx.notify()
1235                                                    }
1236                                                }),
1237                                            ))
1238                                        })
1239                                        .child(RadioWithLabel::new(
1240                                            "use-server-name-in-ssh",
1241                                            Label::new("Configure manually"),
1242                                            kind == NewServerKind::Manual,
1243                                            cx.listener({
1244                                                move |this, _, cx| {
1245                                                    if let Mode::CreateDevServer(
1246                                                        CreateDevServer { kind, .. },
1247                                                    ) = &mut this.mode
1248                                                    {
1249                                                        *kind = NewServerKind::Manual;
1250                                                    }
1251                                                    cx.notify()
1252                                                }
1253                                            }),
1254                                        )),
1255                                )
1256                            })
1257                            .when(dev_server_id.is_none() && ssh_prompt.is_none(), |el| {
1258                                el.child(
1259                                    if kind == NewServerKind::Manual {
1260                                        Label::new(MANUAL_SETUP_MESSAGE)
1261                                    } else {
1262                                        Label::new(SSH_SETUP_MESSAGE)
1263                                    }
1264                                    .size(LabelSize::Small)
1265                                    .color(Color::Muted),
1266                                )
1267                            })
1268                            .when_some(ssh_prompt, |el, ssh_prompt| el.child(ssh_prompt))
1269                            .when(dev_server_id.is_some() && access_token.is_none(), |el| {
1270                                el.child(
1271                                    if kind == NewServerKind::Manual {
1272                                        Label::new(
1273                                            "Note: updating the dev server generate a new token",
1274                                        )
1275                                    } else {
1276                                        Label::new(SSH_SETUP_MESSAGE)
1277                                    }
1278                                    .size(LabelSize::Small)
1279                                    .color(Color::Muted),
1280                                )
1281                            })
1282                            .when_some(access_token.clone(), {
1283                                |el, access_token| {
1284                                    el.child(self.render_dev_server_token_creating(
1285                                        access_token,
1286                                        name,
1287                                        kind,
1288                                        status,
1289                                        creating,
1290                                        cx,
1291                                    ))
1292                                }
1293                            }),
1294                    ),
1295            )
1296            .footer(
1297                ModalFooter::new().end_slot(if status == DevServerStatus::Online {
1298                    Button::new("create-dev-server", "Done")
1299                        .style(ButtonStyle::Filled)
1300                        .layer(ElevationIndex::ModalSurface)
1301                        .on_click(cx.listener(move |this, _, cx| {
1302                            cx.focus(&this.focus_handle);
1303                            this.mode = Mode::Default(None);
1304                            cx.notify();
1305                        }))
1306                } else {
1307                    Button::new(
1308                        "create-dev-server",
1309                        if kind == NewServerKind::Manual {
1310                            if dev_server_id.is_some() {
1311                                "Update"
1312                            } else {
1313                                "Create"
1314                            }
1315                        } else if dev_server_id.is_some() {
1316                            "Reconnect"
1317                        } else {
1318                            "Connect"
1319                        },
1320                    )
1321                    .style(ButtonStyle::Filled)
1322                    .layer(ElevationIndex::ModalSurface)
1323                    .disabled(creating && dev_server_id.is_none())
1324                    .on_click(cx.listener({
1325                        let access_token = access_token.clone();
1326                        move |this, _, cx| {
1327                            if kind == NewServerKind::DirectSSH {
1328                                this.create_ssh_server(cx);
1329                                return;
1330                            }
1331                            this.create_or_update_dev_server(
1332                                kind,
1333                                dev_server_id,
1334                                access_token.clone(),
1335                                cx,
1336                            );
1337                        }
1338                    }))
1339                }),
1340            )
1341    }
1342
1343    fn render_dev_server_token_creating(
1344        &self,
1345        access_token: String,
1346        dev_server_name: String,
1347        kind: NewServerKind,
1348        status: DevServerStatus,
1349        creating: bool,
1350        cx: &mut ViewContext<Self>,
1351    ) -> Div {
1352        self.markdown.update(cx, |markdown, cx| {
1353            if kind == NewServerKind::Manual {
1354                markdown.reset(format!("Please log into '{}'. If you don't yet have Zed installed, run:\n```\ncurl https://zed.dev/install.sh | bash\n```\nThen, to start Zed in headless mode:\n```\nzed --dev-server-token {}\n```", dev_server_name, access_token), cx);
1355            } else {
1356                markdown.reset("Please wait while we connect over SSH.\n\nIf you run into problems, please [file a bug](https://github.com/zed-industries/zed), and in the meantime try using the manual setup.".to_string(), cx);
1357            }
1358        });
1359
1360        v_flex()
1361            .pl_2()
1362            .pt_2()
1363            .gap_2()
1364            .child(v_flex().w_full().text_sm().child(self.markdown.clone()))
1365            .map(|el| {
1366                if status == DevServerStatus::Offline && kind != NewServerKind::Manual && !creating
1367                {
1368                    el.child(
1369                        h_flex()
1370                            .gap_2()
1371                            .child(Icon::new(IconName::Disconnected).size(IconSize::Medium))
1372                            .child(Label::new("Not connected")),
1373                    )
1374                } else if status == DevServerStatus::Offline {
1375                    el.child(Self::render_loading_spinner("Waiting for connection…"))
1376                } else {
1377                    el.child(Label::new("🎊 Connection established!"))
1378                }
1379            })
1380    }
1381
1382    fn render_loading_spinner(label: impl Into<SharedString>) -> Div {
1383        h_flex()
1384            .gap_2()
1385            .child(
1386                Icon::new(IconName::ArrowCircle)
1387                    .size(IconSize::Medium)
1388                    .with_animation(
1389                        "arrow-circle",
1390                        Animation::new(Duration::from_secs(2)).repeat(),
1391                        |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
1392                    ),
1393            )
1394            .child(Label::new(label))
1395    }
1396
1397    fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1398        let dev_servers = self.dev_server_store.read(cx).dev_servers();
1399        let ssh_connections = SshSettings::get_global(cx)
1400            .ssh_connections()
1401            .collect::<Vec<_>>();
1402
1403        let Mode::Default(create_dev_server_project) = &self.mode else {
1404            unreachable!()
1405        };
1406
1407        let mut is_creating = None;
1408        let mut creating_dev_server = None;
1409        if let Some(CreateDevServerProject {
1410            creating,
1411            dev_server_id,
1412            ..
1413        }) = create_dev_server_project
1414        {
1415            is_creating = Some(*creating);
1416            creating_dev_server = Some(*dev_server_id);
1417        };
1418
1419        Modal::new("remote-projects", Some(self.scroll_handle.clone()))
1420            .header(
1421                ModalHeader::new()
1422                    .show_dismiss_button(true)
1423                    .child(Headline::new("Remote Projects (alpha)").size(HeadlineSize::Small)),
1424            )
1425            .section(
1426                Section::new().child(
1427                    div().child(
1428                        List::new()
1429                            .empty_message("No dev servers registered yet.")
1430                            .header(Some(
1431                                ListHeader::new("Connections").end_slot(
1432                                    Button::new("register-dev-server-button", "Connect New Server")
1433                                        .icon(IconName::Plus)
1434                                        .icon_position(IconPosition::Start)
1435                                        .icon_color(Color::Muted)
1436                                        .on_click(cx.listener(|this, _, cx| {
1437                                            this.mode = Mode::CreateDevServer(CreateDevServer {
1438                                                kind: if SshSettings::get_global(cx)
1439                                                    .use_direct_ssh()
1440                                                {
1441                                                    NewServerKind::DirectSSH
1442                                                } else {
1443                                                    NewServerKind::LegacySSH
1444                                                },
1445                                                ..Default::default()
1446                                            });
1447                                            this.dev_server_name_input.update(
1448                                                cx,
1449                                                |text_field, cx| {
1450                                                    text_field.editor().update(cx, |editor, cx| {
1451                                                        editor.set_text("", cx);
1452                                                    });
1453                                                },
1454                                            );
1455                                            cx.notify();
1456                                        })),
1457                                ),
1458                            ))
1459                            .children(ssh_connections.iter().cloned().enumerate().map(
1460                                |(ix, connection)| {
1461                                    self.render_ssh_connection(ix, connection, cx)
1462                                        .into_any_element()
1463                                },
1464                            ))
1465                            .children(dev_servers.iter().map(|dev_server| {
1466                                let creating = if creating_dev_server == Some(dev_server.id) {
1467                                    is_creating
1468                                } else {
1469                                    None
1470                                };
1471                                self.render_dev_server(dev_server, creating, cx)
1472                                    .into_any_element()
1473                            })),
1474                    ),
1475                ),
1476            )
1477    }
1478}
1479
1480fn get_text(element: &View<TextField>, cx: &mut WindowContext) -> String {
1481    element
1482        .read(cx)
1483        .editor()
1484        .read(cx)
1485        .text(cx)
1486        .trim()
1487        .to_string()
1488}
1489
1490impl ModalView for DevServerProjects {}
1491
1492impl FocusableView for DevServerProjects {
1493    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1494        self.focus_handle.clone()
1495    }
1496}
1497
1498impl EventEmitter<DismissEvent> for DevServerProjects {}
1499
1500impl Render for DevServerProjects {
1501    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1502        div()
1503            .track_focus(&self.focus_handle)
1504            .p_2()
1505            .elevation_3(cx)
1506            .key_context("DevServerModal")
1507            .on_action(cx.listener(Self::cancel))
1508            .on_action(cx.listener(Self::confirm))
1509            .capture_any_mouse_down(cx.listener(|this, _, cx| {
1510                this.focus_handle(cx).focus(cx);
1511            }))
1512            .on_mouse_down_out(cx.listener(|this, _, cx| {
1513                if matches!(this.mode, Mode::Default(None)) {
1514                    cx.emit(DismissEvent)
1515                }
1516            }))
1517            .w(rems(34.))
1518            .max_h(rems(40.))
1519            .child(match &self.mode {
1520                Mode::Default(_) => self.render_default(cx).into_any_element(),
1521                Mode::CreateDevServer(state) => {
1522                    self.render_create_dev_server(state, cx).into_any_element()
1523                }
1524            })
1525    }
1526}
1527
1528pub fn reconnect_to_dev_server_project(
1529    workspace: View<Workspace>,
1530    dev_server: DevServer,
1531    dev_server_project_id: DevServerProjectId,
1532    replace_current_window: bool,
1533    cx: &mut WindowContext,
1534) -> Task<Result<()>> {
1535    let store = dev_server_projects::Store::global(cx);
1536    let reconnect = reconnect_to_dev_server(workspace.clone(), dev_server, cx);
1537    cx.spawn(|mut cx| async move {
1538        reconnect.await?;
1539
1540        cx.background_executor()
1541            .timer(Duration::from_millis(1000))
1542            .await;
1543
1544        if let Some(project_id) = store.update(&mut cx, |store, _| {
1545            store
1546                .dev_server_project(dev_server_project_id)
1547                .and_then(|p| p.project_id)
1548        })? {
1549            workspace
1550                .update(&mut cx, move |_, cx| {
1551                    open_dev_server_project(
1552                        replace_current_window,
1553                        dev_server_project_id,
1554                        project_id,
1555                        cx,
1556                    )
1557                })?
1558                .await?;
1559        }
1560
1561        Ok(())
1562    })
1563}
1564
1565pub fn reconnect_to_dev_server(
1566    workspace: View<Workspace>,
1567    dev_server: DevServer,
1568    cx: &mut WindowContext,
1569) -> Task<Result<()>> {
1570    let Some(ssh_connection_string) = dev_server.ssh_connection_string else {
1571        return Task::ready(Err(anyhow!("Can't reconnect, no ssh_connection_string")));
1572    };
1573    let dev_server_store = dev_server_projects::Store::global(cx);
1574    let get_access_token = dev_server_store.update(cx, |store, cx| {
1575        store.regenerate_dev_server_token(dev_server.id, cx)
1576    });
1577
1578    cx.spawn(|mut cx| async move {
1579        let access_token = get_access_token.await?.access_token;
1580
1581        spawn_ssh_task(
1582            workspace,
1583            dev_server_store,
1584            dev_server.id,
1585            ssh_connection_string.to_string(),
1586            access_token,
1587            &mut cx,
1588        )
1589        .await
1590    })
1591}
1592
1593pub async fn spawn_ssh_task(
1594    workspace: View<Workspace>,
1595    dev_server_store: Model<dev_server_projects::Store>,
1596    dev_server_id: DevServerId,
1597    ssh_connection_string: String,
1598    access_token: String,
1599    cx: &mut AsyncWindowContext,
1600) -> Result<()> {
1601    let terminal_panel = workspace
1602        .update(cx, |workspace, cx| workspace.panel::<TerminalPanel>(cx))
1603        .ok()
1604        .flatten()
1605        .with_context(|| anyhow!("No terminal panel"))?;
1606
1607    let command = "sh".to_string();
1608    let args = vec![
1609        "-x".to_string(),
1610        "-c".to_string(),
1611        format!(
1612            r#"~/.local/bin/zed -v >/dev/stderr || (curl -f https://zed.dev/install.sh || wget -qO- https://zed.dev/install.sh) | sh && ZED_HEADLESS=1 ~/.local/bin/zed --dev-server-token {}"#,
1613            access_token
1614        ),
1615    ];
1616
1617    let ssh_connection_string = ssh_connection_string.to_string();
1618    let (command, args) = wrap_for_ssh(
1619        &SshCommand::DevServer(ssh_connection_string.clone()),
1620        Some((&command, &args)),
1621        None,
1622        HashMap::default(),
1623        None,
1624    );
1625
1626    let terminal = terminal_panel
1627        .update(cx, |terminal_panel, cx| {
1628            terminal_panel.spawn_in_new_terminal(
1629                SpawnInTerminal {
1630                    id: task::TaskId("ssh-remote".into()),
1631                    full_label: "Install zed over ssh".into(),
1632                    label: "Install zed over ssh".into(),
1633                    command,
1634                    args,
1635                    command_label: ssh_connection_string.clone(),
1636                    cwd: None,
1637                    use_new_terminal: true,
1638                    allow_concurrent_runs: false,
1639                    reveal: RevealStrategy::Always,
1640                    hide: HideStrategy::Never,
1641                    env: Default::default(),
1642                    shell: Default::default(),
1643                },
1644                cx,
1645            )
1646        })?
1647        .await?;
1648
1649    terminal
1650        .update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
1651        .await;
1652
1653    // There's a race-condition between the task completing successfully, and the server sending us the online status. Make it less likely we'll show the error state.
1654    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
1655        == DevServerStatus::Offline
1656    {
1657        cx.background_executor()
1658            .timer(Duration::from_millis(200))
1659            .await
1660    }
1661
1662    if dev_server_store.update(cx, |this, _| this.dev_server_status(dev_server_id))?
1663        == DevServerStatus::Offline
1664    {
1665        return Err(anyhow!("couldn't reconnect"))?;
1666    }
1667
1668    Ok(())
1669}