1500 lines · 56.7 KB · Rust
RawDownload
use std::{cmp, ops::ControlFlow, path::PathBuf, sync::Arc, time::Duration}; use crate::{    default_working_directory,    persistence::{        deserialize_terminal_panel, serialize_pane_group, SerializedItems, SerializedTerminalPanel,    },    TerminalView,};use breadcrumbs::Breadcrumbs;use collections::HashMap;use db::kvp::KEY_VALUE_STORE;use futures::future::join_all;use gpui::{    actions, Action, AnyView, App, AsyncApp, AsyncWindowContext, Context, Corner, Entity,    EventEmitter, ExternalPaths, FocusHandle, Focusable, IntoElement, ParentElement, Pixels,    Render, Styled, Task, WeakEntity, Window,};use itertools::Itertools;use project::{terminals::TerminalKind, Fs, Project, ProjectEntryId};use search::{buffer_search::DivRegistrar, BufferSearchBar};use settings::Settings;use task::{RevealStrategy, RevealTarget, ShellBuilder, SpawnInTerminal, TaskId};use terminal::{    terminal_settings::{TerminalDockPosition, TerminalSettings},    Terminal,};use ui::{    prelude::*, ButtonCommon, Clickable, ContextMenu, FluentBuilder, PopoverMenu, Toggleable,    Tooltip,};use util::{ResultExt, TryFutureExt};use workspace::{    dock::{DockPosition, Panel, PanelEvent, PanelHandle},    item::SerializableItem,    move_active_item, move_item, pane,    ui::IconName,    ActivateNextPane, ActivatePane, ActivatePaneDown, ActivatePaneLeft, ActivatePaneRight,    ActivatePaneUp, ActivatePreviousPane, DraggedSelection, DraggedTab, ItemId, MoveItemToPane,    MoveItemToPaneInDirection, NewTerminal, Pane, PaneGroup, SplitDirection, SplitDown, SplitLeft,    SplitRight, SplitUp, SwapPaneDown, SwapPaneLeft, SwapPaneRight, SwapPaneUp, ToggleZoom,    Workspace,}; use anyhow::{anyhow, Context as _, Result};use zed_actions::assistant::InlineAssist; const TERMINAL_PANEL_KEY: &str = "TerminalPanel"; actions!(terminal_panel, [ToggleFocus]); pub fn init(cx: &mut App) {    cx.observe_new(        |workspace: &mut Workspace, _window, _: &mut Context<Workspace>| {            workspace.register_action(TerminalPanel::new_terminal);            workspace.register_action(TerminalPanel::open_terminal);            workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {                if is_enabled_in_workspace(workspace, cx) {                    workspace.toggle_panel_focus::<TerminalPanel>(window, cx);                }            });        },    )    .detach();} pub struct TerminalPanel {    pub(crate) active_pane: Entity<Pane>,    pub(crate) center: PaneGroup,    fs: Arc<dyn Fs>,    workspace: WeakEntity<Workspace>,    pub(crate) width: Option<Pixels>,    pub(crate) height: Option<Pixels>,    pending_serialization: Task<Option<()>>,    pending_terminals_to_add: usize,    deferred_tasks: HashMap<TaskId, Task<()>>,    assistant_enabled: bool,    assistant_tab_bar_button: Option<AnyView>,    active: bool,} impl TerminalPanel {    pub fn new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {        let project = workspace.project();        let pane = new_terminal_pane(workspace.weak_handle(), project.clone(), false, window, cx);        let center = PaneGroup::new(pane.clone());        let terminal_panel = Self {            center,            active_pane: pane,            fs: workspace.app_state().fs.clone(),            workspace: workspace.weak_handle(),            pending_serialization: Task::ready(None),            width: None,            height: None,            pending_terminals_to_add: 0,            deferred_tasks: HashMap::default(),            assistant_enabled: false,            assistant_tab_bar_button: None,            active: false,        };        terminal_panel.apply_tab_bar_buttons(&terminal_panel.active_pane, cx);        terminal_panel    }     pub fn set_assistant_enabled(&mut self, enabled: bool, cx: &mut Context<Self>) {        self.assistant_enabled = enabled;        if enabled {            let focus_handle = self                .active_pane                .read(cx)                .active_item()                .map(|item| item.item_focus_handle(cx))                .unwrap_or(self.focus_handle(cx));            self.assistant_tab_bar_button = Some(                cx.new(move |_| InlineAssistTabBarButton { focus_handle })                    .into(),            );        } else {            self.assistant_tab_bar_button = None;        }        for pane in self.center.panes() {            self.apply_tab_bar_buttons(pane, cx);        }    }     fn apply_tab_bar_buttons(&self, terminal_pane: &Entity<Pane>, cx: &mut Context<Self>) {        let assistant_tab_bar_button = self.assistant_tab_bar_button.clone();        terminal_pane.update(cx, |pane, cx| {            pane.set_render_tab_bar_buttons(cx, move |pane, window, cx| {                let split_context = pane                    .active_item()                    .and_then(|item| item.downcast::<TerminalView>())                    .map(|terminal_view| terminal_view.read(cx).focus_handle.clone());                if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {                    return (None, None);                }                let focus_handle = pane.focus_handle(cx);                let right_children = h_flex()                    .gap(DynamicSpacing::Base02.rems(cx))                    .child(                        PopoverMenu::new("terminal-tab-bar-popover-menu")                            .trigger_with_tooltip(                                IconButton::new("plus", IconName::Plus).icon_size(IconSize::Small),                                Tooltip::text("New…"),                            )                            .anchor(Corner::TopRight)                            .with_handle(pane.new_item_context_menu_handle.clone())                            .menu(move |window, cx| {                                let focus_handle = focus_handle.clone();                                let menu = ContextMenu::build(window, cx, |menu, _, _| {                                    menu.context(focus_handle.clone())                                        .action(                                            "New Terminal",                                            workspace::NewTerminal.boxed_clone(),                                        )                                        // We want the focus to go back to terminal panel once task modal is dismissed,                                        // hence we focus that first. Otherwise, we'd end up without a focused element, as                                        // context menu will be gone the moment we spawn the modal.                                        .action(                                            "Spawn task",                                            zed_actions::Spawn::modal().boxed_clone(),                                        )                                });                                 Some(menu)                            }),                    )                    .children(assistant_tab_bar_button.clone())                    .child(                        PopoverMenu::new("terminal-pane-tab-bar-split")                            .trigger_with_tooltip(                                IconButton::new("terminal-pane-split", IconName::Split)                                    .icon_size(IconSize::Small),                                Tooltip::text("Split Pane"),                            )                            .anchor(Corner::TopRight)                            .with_handle(pane.split_item_context_menu_handle.clone())                            .menu({                                let split_context = split_context.clone();                                move |window, cx| {                                    ContextMenu::build(window, cx, |menu, _, _| {                                        menu.when_some(                                            split_context.clone(),                                            |menu, split_context| menu.context(split_context),                                        )                                        .action("Split Right", SplitRight.boxed_clone())                                        .action("Split Left", SplitLeft.boxed_clone())                                        .action("Split Up", SplitUp.boxed_clone())                                        .action("Split Down", SplitDown.boxed_clone())                                    })                                    .into()                                }                            }),                    )                    .child({                        let zoomed = pane.is_zoomed();                        IconButton::new("toggle_zoom", IconName::Maximize)                            .icon_size(IconSize::Small)                            .toggle_state(zoomed)                            .selected_icon(IconName::Minimize)                            .on_click(cx.listener(|pane, _, window, cx| {                                pane.toggle_zoom(&workspace::ToggleZoom, window, cx);                            }))                            .tooltip(move |window, cx| {                                Tooltip::for_action(                                    if zoomed { "Zoom Out" } else { "Zoom In" },                                    &ToggleZoom,                                    window,                                    cx,                                )                            })                    })                    .into_any_element()                    .into();                (None, right_children)            });        });    }     fn serialization_key(workspace: &Workspace) -> Option<String> {        workspace            .database_id()            .map(|id| i64::from(id).to_string())            .or(workspace.session_id())            .map(|id| format!("{:?}-{:?}", TERMINAL_PANEL_KEY, id))    }     pub async fn load(        workspace: WeakEntity<Workspace>,        mut cx: AsyncWindowContext,    ) -> Result<Entity<Self>> {        let mut terminal_panel = None;         match workspace            .read_with(&mut cx, |workspace, _| {                workspace                    .database_id()                    .zip(TerminalPanel::serialization_key(workspace))            })            .ok()            .flatten()        {            Some((database_id, serialization_key)) => {                if let Some(serialized_panel) = cx                    .background_spawn(async move { KEY_VALUE_STORE.read_kvp(&serialization_key) })                    .await                    .log_err()                    .flatten()                    .map(|panel| serde_json::from_str::<SerializedTerminalPanel>(&panel))                    .transpose()                    .log_err()                    .flatten()                {                    if let Ok(serialized) = workspace                        .update_in(&mut cx, |workspace, window, cx| {                            deserialize_terminal_panel(                                workspace.weak_handle(),                                workspace.project().clone(),                                database_id,                                serialized_panel,                                window,                                cx,                            )                        })?                        .await                    {                        terminal_panel = Some(serialized);                    }                }            }            _ => {}        }         let terminal_panel = if let Some(panel) = terminal_panel {            panel        } else {            workspace.update_in(&mut cx, |workspace, window, cx| {                cx.new(|cx| TerminalPanel::new(workspace, window, cx))            })?        };         if let Some(workspace) = workspace.upgrade() {            terminal_panel                .update_in(&mut cx, |_, window, cx| {                    cx.subscribe_in(&workspace, window, |terminal_panel, _, e, window, cx| {                        if let workspace::Event::SpawnTask {                            action: spawn_in_terminal,                        } = e                        {                            terminal_panel.spawn_task(spawn_in_terminal, window, cx);                        };                    })                    .detach();                })                .ok();        }         // Since panels/docks are loaded outside from the workspace, we cleanup here, instead of through the workspace.        if let Some(workspace) = workspace.upgrade() {            let cleanup_task = workspace.update_in(&mut cx, |workspace, window, cx| {                let alive_item_ids = terminal_panel                    .read(cx)                    .center                    .panes()                    .into_iter()                    .flat_map(|pane| pane.read(cx).items())                    .map(|item| item.item_id().as_u64() as ItemId)                    .collect();                workspace.database_id().map(|workspace_id| {                    TerminalView::cleanup(workspace_id, alive_item_ids, window, cx)                })            })?;            if let Some(task) = cleanup_task {                task.await.log_err();            }        }         if let Some(workspace) = workspace.upgrade() {            let should_focus = workspace                .update_in(&mut cx, |workspace, window, cx| {                    workspace.active_item(cx).is_none()                        && workspace                            .is_dock_at_position_open(terminal_panel.position(window, cx), cx)                })                .unwrap_or(false);             if should_focus {                terminal_panel                    .update_in(&mut cx, |panel, window, cx| {                        panel.active_pane.update(cx, |pane, cx| {                            pane.focus_active_item(window, cx);                        });                    })                    .ok();            }        }         Ok(terminal_panel)    }     fn handle_pane_event(        &mut self,        pane: &Entity<Pane>,        event: &pane::Event,        window: &mut Window,        cx: &mut Context<Self>,    ) {        match event {            pane::Event::ActivateItem { .. } => self.serialize(cx),            pane::Event::RemovedItem { .. } => self.serialize(cx),            pane::Event::Remove { focus_on_pane } => {                let pane_count_before_removal = self.center.panes().len();                let _removal_result = self.center.remove(&pane);                if pane_count_before_removal == 1 {                    self.center.first_pane().update(cx, |pane, cx| {                        pane.set_zoomed(false, cx);                    });                    cx.emit(PanelEvent::Close);                } else {                    if let Some(focus_on_pane) =                        focus_on_pane.as_ref().or_else(|| self.center.panes().pop())                    {                        focus_on_pane.focus_handle(cx).focus(window);                    }                }            }            pane::Event::ZoomIn => {                for pane in self.center.panes() {                    pane.update(cx, |pane, cx| {                        pane.set_zoomed(true, cx);                    })                }                cx.emit(PanelEvent::ZoomIn);                cx.notify();            }            pane::Event::ZoomOut => {                for pane in self.center.panes() {                    pane.update(cx, |pane, cx| {                        pane.set_zoomed(false, cx);                    })                }                cx.emit(PanelEvent::ZoomOut);                cx.notify();            }            pane::Event::AddItem { item } => {                if let Some(workspace) = self.workspace.upgrade() {                    workspace.update(cx, |workspace, cx| {                        item.added_to_pane(workspace, pane.clone(), window, cx)                    })                }                self.serialize(cx);            }            pane::Event::Split(direction) => {                let Some(new_pane) = self.new_pane_with_cloned_active_terminal(window, cx) else {                    return;                };                let pane = pane.clone();                let direction = *direction;                self.center.split(&pane, &new_pane, direction).log_err();                window.focus(&new_pane.focus_handle(cx));            }            pane::Event::Focus => {                self.active_pane = pane.clone();            }             _ => {}        }    }     fn new_pane_with_cloned_active_terminal(        &mut self,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Option<Entity<Pane>> {        let workspace = self.workspace.upgrade()?;        let workspace = workspace.read(cx);        let database_id = workspace.database_id();        let weak_workspace = self.workspace.clone();        let project = workspace.project().clone();        let (working_directory, python_venv_directory) = self            .active_pane            .read(cx)            .active_item()            .and_then(|item| item.downcast::<TerminalView>())            .map(|terminal_view| {                let terminal = terminal_view.read(cx).terminal().read(cx);                (                    terminal                        .working_directory()                        .or_else(|| default_working_directory(workspace, cx)),                    terminal.python_venv_directory.clone(),                )            })            .unwrap_or((None, None));        let kind = TerminalKind::Shell(working_directory);        let window_handle = window.window_handle();        let terminal = project            .update(cx, |project, cx| {                project.create_terminal_with_venv(kind, python_venv_directory, window_handle, cx)            })            .ok()?;         let terminal_view = Box::new(cx.new(|cx| {            TerminalView::new(                terminal.clone(),                weak_workspace.clone(),                database_id,                project.downgrade(),                window,                cx,            )        }));        let pane = new_terminal_pane(            weak_workspace,            project,            self.active_pane.read(cx).is_zoomed(),            window,            cx,        );        self.apply_tab_bar_buttons(&pane, cx);        pane.update(cx, |pane, cx| {            pane.add_item(terminal_view, true, true, None, window, cx);        });         Some(pane)    }     pub fn open_terminal(        workspace: &mut Workspace,        action: &workspace::OpenTerminal,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        let Some(terminal_panel) = workspace.panel::<Self>(cx) else {            return;        };         terminal_panel            .update(cx, |panel, cx| {                panel.add_terminal(                    TerminalKind::Shell(Some(action.working_directory.clone())),                    RevealStrategy::Always,                    window,                    cx,                )            })            .detach_and_log_err(cx);    }     fn spawn_task(&mut self, task: &SpawnInTerminal, window: &mut Window, cx: &mut Context<Self>) {        let Ok(is_local) = self            .workspace            .update(cx, |workspace, cx| workspace.project().read(cx).is_local())        else {            return;        };         let builder = ShellBuilder::new(is_local, &task.shell);        let command_label = builder.command_label(&task.command_label);        let (command, args) = builder.build(task.command.clone(), &task.args);

Lines 1–500 of 1500.