terminal_view.rs

   1mod color_contrast;
   2mod persistence;
   3pub mod terminal_element;
   4pub mod terminal_panel;
   5mod terminal_path_like_target;
   6pub mod terminal_scrollbar;
   7mod terminal_slash_command;
   8pub mod terminal_tab_tooltip;
   9
  10use assistant_slash_command::SlashCommandRegistry;
  11use editor::{EditorSettings, actions::SelectAll, scroll::ScrollbarAutoHide};
  12use gpui::{
  13    Action, AnyElement, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
  14    KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent, Pixels, Render,
  15    ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity, actions, anchored,
  16    deferred, div,
  17};
  18use persistence::TERMINAL_DB;
  19use project::{Project, search::SearchQuery, terminals::TerminalKind};
  20use schemars::JsonSchema;
  21use task::TaskId;
  22use terminal::{
  23    Clear, Copy, Event, HoveredWord, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp,
  24    ScrollPageDown, ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskState,
  25    TaskStatus, Terminal, TerminalBounds, ToggleViMode,
  26    alacritty_terminal::{
  27        index::Point,
  28        term::{TermMode, point_to_viewport, search::RegexSearch},
  29    },
  30    terminal_settings::{self, CursorShape, TerminalBlink, TerminalSettings, WorkingDirectory},
  31};
  32use terminal_element::TerminalElement;
  33use terminal_panel::TerminalPanel;
  34use terminal_path_like_target::{hover_path_like_target, open_path_like_target};
  35use terminal_scrollbar::TerminalScrollHandle;
  36use terminal_slash_command::TerminalSlashCommand;
  37use terminal_tab_tooltip::TerminalTooltip;
  38use ui::{
  39    ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip, h_flex, prelude::*,
  40};
  41use util::ResultExt;
  42use workspace::{
  43    CloseActiveItem, NewCenterTerminal, NewTerminal, ToolbarItemLocation, Workspace, WorkspaceId,
  44    delete_unloaded_items,
  45    item::{
  46        BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent,
  47    },
  48    register_serializable_item,
  49    searchable::{Direction, SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
  50};
  51
  52use serde::Deserialize;
  53use settings::{Settings, SettingsStore};
  54use smol::Timer;
  55use zed_actions::assistant::InlineAssist;
  56
  57use std::{
  58    cmp,
  59    ops::{Range, RangeInclusive},
  60    path::{Path, PathBuf},
  61    rc::Rc,
  62    sync::Arc,
  63    time::Duration,
  64};
  65
  66const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
  67const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.);
  68
  69/// Event to transmit the scroll from the element to the view
  70#[derive(Clone, Debug, PartialEq)]
  71pub struct ScrollTerminal(pub i32);
  72
  73/// Sends the specified text directly to the terminal.
  74#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Action)]
  75#[action(namespace = terminal)]
  76pub struct SendText(String);
  77
  78/// Sends a keystroke sequence to the terminal.
  79#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Action)]
  80#[action(namespace = terminal)]
  81pub struct SendKeystroke(String);
  82
  83actions!(
  84    terminal,
  85    [
  86        /// Reruns the last executed task in the terminal.
  87        RerunTask
  88    ]
  89);
  90
  91pub fn init(cx: &mut App) {
  92    assistant_slash_command::init(cx);
  93    terminal_panel::init(cx);
  94    terminal::init(cx);
  95
  96    register_serializable_item::<TerminalView>(cx);
  97
  98    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
  99        workspace.register_action(TerminalView::deploy);
 100    })
 101    .detach();
 102    SlashCommandRegistry::global(cx).register_command(TerminalSlashCommand, true);
 103}
 104
 105pub struct BlockProperties {
 106    pub height: u8,
 107    pub render: Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>,
 108}
 109
 110pub struct BlockContext<'a, 'b> {
 111    pub window: &'a mut Window,
 112    pub context: &'b mut App,
 113    pub dimensions: TerminalBounds,
 114}
 115
 116///A terminal view, maintains the PTY's file handles and communicates with the terminal
 117pub struct TerminalView {
 118    terminal: Entity<Terminal>,
 119    workspace: WeakEntity<Workspace>,
 120    project: WeakEntity<Project>,
 121    focus_handle: FocusHandle,
 122    //Currently using iTerm bell, show bell emoji in tab until input is received
 123    has_bell: bool,
 124    context_menu: Option<(Entity<ContextMenu>, gpui::Point<Pixels>, Subscription)>,
 125    cursor_shape: CursorShape,
 126    blink_state: bool,
 127    mode: TerminalMode,
 128    blinking_terminal_enabled: bool,
 129    cwd_serialized: bool,
 130    blinking_paused: bool,
 131    blink_epoch: usize,
 132    hover: Option<HoverTarget>,
 133    hover_tooltip_update: Task<()>,
 134    workspace_id: Option<WorkspaceId>,
 135    show_breadcrumbs: bool,
 136    block_below_cursor: Option<Rc<BlockProperties>>,
 137    scroll_top: Pixels,
 138    scrollbar_state: ScrollbarState,
 139    scroll_handle: TerminalScrollHandle,
 140    show_scrollbar: bool,
 141    hide_scrollbar_task: Option<Task<()>>,
 142    marked_text: Option<String>,
 143    marked_range_utf16: Option<Range<usize>>,
 144    _subscriptions: Vec<Subscription>,
 145    _terminal_subscriptions: Vec<Subscription>,
 146}
 147
 148#[derive(Default, Clone)]
 149pub enum TerminalMode {
 150    #[default]
 151    Standalone,
 152    Embedded {
 153        max_lines_when_unfocused: Option<usize>,
 154    },
 155}
 156
 157#[derive(Clone)]
 158pub enum ContentMode {
 159    Scrollable,
 160    Inline {
 161        displayed_lines: usize,
 162        total_lines: usize,
 163    },
 164}
 165
 166impl ContentMode {
 167    pub fn is_limited(&self) -> bool {
 168        match self {
 169            ContentMode::Scrollable => false,
 170            ContentMode::Inline {
 171                displayed_lines,
 172                total_lines,
 173            } => displayed_lines < total_lines,
 174        }
 175    }
 176
 177    pub fn is_scrollable(&self) -> bool {
 178        matches!(self, ContentMode::Scrollable)
 179    }
 180}
 181
 182#[derive(Debug)]
 183#[cfg_attr(test, derive(Clone, Eq, PartialEq))]
 184struct HoverTarget {
 185    tooltip: String,
 186    hovered_word: HoveredWord,
 187}
 188
 189impl EventEmitter<Event> for TerminalView {}
 190impl EventEmitter<ItemEvent> for TerminalView {}
 191impl EventEmitter<SearchEvent> for TerminalView {}
 192
 193impl Focusable for TerminalView {
 194    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 195        self.focus_handle.clone()
 196    }
 197}
 198
 199impl TerminalView {
 200    ///Create a new Terminal in the current working directory or the user's home directory
 201    pub fn deploy(
 202        workspace: &mut Workspace,
 203        _: &NewCenterTerminal,
 204        window: &mut Window,
 205        cx: &mut Context<Workspace>,
 206    ) {
 207        let working_directory = default_working_directory(workspace, cx);
 208        TerminalPanel::add_center_terminal(
 209            workspace,
 210            TerminalKind::Shell(working_directory),
 211            window,
 212            cx,
 213        )
 214        .detach_and_log_err(cx);
 215    }
 216
 217    pub fn new(
 218        terminal: Entity<Terminal>,
 219        workspace: WeakEntity<Workspace>,
 220        workspace_id: Option<WorkspaceId>,
 221        project: WeakEntity<Project>,
 222        window: &mut Window,
 223        cx: &mut Context<Self>,
 224    ) -> Self {
 225        let workspace_handle = workspace.clone();
 226        let terminal_subscriptions =
 227            subscribe_for_terminal_events(&terminal, workspace, window, cx);
 228
 229        let focus_handle = cx.focus_handle();
 230        let focus_in = cx.on_focus_in(&focus_handle, window, |terminal_view, window, cx| {
 231            terminal_view.focus_in(window, cx);
 232        });
 233        let focus_out = cx.on_focus_out(
 234            &focus_handle,
 235            window,
 236            |terminal_view, _event, window, cx| {
 237                terminal_view.focus_out(window, cx);
 238            },
 239        );
 240        let cursor_shape = TerminalSettings::get_global(cx)
 241            .cursor_shape
 242            .unwrap_or_default();
 243
 244        let scroll_handle = TerminalScrollHandle::new(terminal.read(cx));
 245
 246        Self {
 247            terminal,
 248            workspace: workspace_handle,
 249            project,
 250            has_bell: false,
 251            focus_handle,
 252            context_menu: None,
 253            cursor_shape,
 254            blink_state: true,
 255            blinking_terminal_enabled: false,
 256            blinking_paused: false,
 257            blink_epoch: 0,
 258            hover: None,
 259            hover_tooltip_update: Task::ready(()),
 260            mode: TerminalMode::Standalone,
 261            workspace_id,
 262            show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
 263            block_below_cursor: None,
 264            scroll_top: Pixels::ZERO,
 265            scrollbar_state: ScrollbarState::new(scroll_handle.clone()),
 266            scroll_handle,
 267            show_scrollbar: !Self::should_autohide_scrollbar(cx),
 268            hide_scrollbar_task: None,
 269            cwd_serialized: false,
 270            marked_text: None,
 271            marked_range_utf16: None,
 272            _subscriptions: vec![
 273                focus_in,
 274                focus_out,
 275                cx.observe_global::<SettingsStore>(Self::settings_changed),
 276            ],
 277            _terminal_subscriptions: terminal_subscriptions,
 278        }
 279    }
 280
 281    /// Enable 'embedded' mode where the terminal displays the full content with an optional limit of lines.
 282    pub fn set_embedded_mode(
 283        &mut self,
 284        max_lines_when_unfocused: Option<usize>,
 285        cx: &mut Context<Self>,
 286    ) {
 287        self.mode = TerminalMode::Embedded {
 288            max_lines_when_unfocused,
 289        };
 290        cx.notify();
 291    }
 292
 293    const MAX_EMBEDDED_LINES: usize = 1_000;
 294
 295    /// Returns the current `ContentMode` depending on the set `TerminalMode` and the current number of lines
 296    ///
 297    /// Note: Even in embedded mode, the terminal will fallback to scrollable when its content exceeds `MAX_EMBEDDED_LINES`
 298    pub fn content_mode(&self, window: &Window, cx: &App) -> ContentMode {
 299        match &self.mode {
 300            TerminalMode::Standalone => ContentMode::Scrollable,
 301            TerminalMode::Embedded {
 302                max_lines_when_unfocused,
 303            } => {
 304                let total_lines = self.terminal.read(cx).total_lines();
 305
 306                if total_lines > Self::MAX_EMBEDDED_LINES {
 307                    ContentMode::Scrollable
 308                } else {
 309                    let mut displayed_lines = total_lines;
 310
 311                    if !self.focus_handle.is_focused(window)
 312                        && let Some(max_lines) = max_lines_when_unfocused
 313                    {
 314                        displayed_lines = displayed_lines.min(*max_lines)
 315                    }
 316
 317                    ContentMode::Inline {
 318                        displayed_lines,
 319                        total_lines,
 320                    }
 321                }
 322            }
 323        }
 324    }
 325
 326    /// Sets the marked (pre-edit) text from the IME.
 327    pub(crate) fn set_marked_text(
 328        &mut self,
 329        text: String,
 330        range: Range<usize>,
 331        cx: &mut Context<Self>,
 332    ) {
 333        self.marked_text = Some(text);
 334        self.marked_range_utf16 = Some(range);
 335        cx.notify();
 336    }
 337
 338    /// Gets the current marked range (UTF-16).
 339    pub(crate) fn marked_text_range(&self) -> Option<Range<usize>> {
 340        self.marked_range_utf16.clone()
 341    }
 342
 343    /// Clears the marked (pre-edit) text state.
 344    pub(crate) fn clear_marked_text(&mut self, cx: &mut Context<Self>) {
 345        if self.marked_text.is_some() {
 346            self.marked_text = None;
 347            self.marked_range_utf16 = None;
 348            cx.notify();
 349        }
 350    }
 351
 352    /// Commits (sends) the given text to the PTY. Called by InputHandler::replace_text_in_range.
 353    pub(crate) fn commit_text(&mut self, text: &str, cx: &mut Context<Self>) {
 354        if !text.is_empty() {
 355            self.terminal.update(cx, |term, _| {
 356                term.input(text.to_string().into_bytes());
 357            });
 358        }
 359    }
 360
 361    pub(crate) fn terminal_bounds(&self, cx: &App) -> TerminalBounds {
 362        self.terminal.read(cx).last_content().terminal_bounds
 363    }
 364
 365    pub fn entity(&self) -> &Entity<Terminal> {
 366        &self.terminal
 367    }
 368
 369    pub fn has_bell(&self) -> bool {
 370        self.has_bell
 371    }
 372
 373    pub fn clear_bell(&mut self, cx: &mut Context<TerminalView>) {
 374        self.has_bell = false;
 375        cx.emit(Event::Wakeup);
 376    }
 377
 378    pub fn deploy_context_menu(
 379        &mut self,
 380        position: gpui::Point<Pixels>,
 381        window: &mut Window,
 382        cx: &mut Context<Self>,
 383    ) {
 384        let assistant_enabled = self
 385            .workspace
 386            .upgrade()
 387            .and_then(|workspace| workspace.read(cx).panel::<TerminalPanel>(cx))
 388            .is_some_and(|terminal_panel| terminal_panel.read(cx).assistant_enabled());
 389        let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
 390            menu.context(self.focus_handle.clone())
 391                .action("New Terminal", Box::new(NewTerminal))
 392                .separator()
 393                .action("Copy", Box::new(Copy))
 394                .action("Paste", Box::new(Paste))
 395                .action("Select All", Box::new(SelectAll))
 396                .action("Clear", Box::new(Clear))
 397                .when(assistant_enabled, |menu| {
 398                    menu.separator()
 399                        .action("Inline Assist", Box::new(InlineAssist::default()))
 400                })
 401                .separator()
 402                .action(
 403                    "Close Terminal Tab",
 404                    Box::new(CloseActiveItem {
 405                        save_intent: None,
 406                        close_pinned: true,
 407                    }),
 408                )
 409        });
 410
 411        window.focus(&context_menu.focus_handle(cx));
 412        let subscription = cx.subscribe_in(
 413            &context_menu,
 414            window,
 415            |this, _, _: &DismissEvent, window, cx| {
 416                if this.context_menu.as_ref().is_some_and(|context_menu| {
 417                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
 418                }) {
 419                    cx.focus_self(window);
 420                }
 421                this.context_menu.take();
 422                cx.notify();
 423            },
 424        );
 425
 426        self.context_menu = Some((context_menu, position, subscription));
 427    }
 428
 429    fn settings_changed(&mut self, cx: &mut Context<Self>) {
 430        let settings = TerminalSettings::get_global(cx);
 431        let breadcrumb_visibility_changed = self.show_breadcrumbs != settings.toolbar.breadcrumbs;
 432        self.show_breadcrumbs = settings.toolbar.breadcrumbs;
 433
 434        let new_cursor_shape = settings.cursor_shape.unwrap_or_default();
 435        let old_cursor_shape = self.cursor_shape;
 436        if old_cursor_shape != new_cursor_shape {
 437            self.cursor_shape = new_cursor_shape;
 438            self.terminal.update(cx, |term, _| {
 439                term.set_cursor_shape(self.cursor_shape);
 440            });
 441        }
 442
 443        if breadcrumb_visibility_changed {
 444            cx.emit(ItemEvent::UpdateBreadcrumbs);
 445        }
 446        cx.notify();
 447    }
 448
 449    fn show_character_palette(
 450        &mut self,
 451        _: &ShowCharacterPalette,
 452        window: &mut Window,
 453        cx: &mut Context<Self>,
 454    ) {
 455        if self
 456            .terminal
 457            .read(cx)
 458            .last_content
 459            .mode
 460            .contains(TermMode::ALT_SCREEN)
 461        {
 462            self.terminal.update(cx, |term, cx| {
 463                term.try_keystroke(
 464                    &Keystroke::parse("ctrl-cmd-space").unwrap(),
 465                    TerminalSettings::get_global(cx).option_as_meta,
 466                )
 467            });
 468        } else {
 469            window.show_character_palette();
 470        }
 471    }
 472
 473    fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
 474        self.terminal.update(cx, |term, _| term.select_all());
 475        cx.notify();
 476    }
 477
 478    fn rerun_task(&mut self, _: &RerunTask, window: &mut Window, cx: &mut Context<Self>) {
 479        let task = self
 480            .terminal
 481            .read(cx)
 482            .task()
 483            .map(|task| terminal_rerun_override(&task.id))
 484            .unwrap_or_default();
 485        window.dispatch_action(Box::new(task), cx);
 486    }
 487
 488    fn clear(&mut self, _: &Clear, _: &mut Window, cx: &mut Context<Self>) {
 489        self.scroll_top = px(0.);
 490        self.terminal.update(cx, |term, _| term.clear());
 491        cx.notify();
 492    }
 493
 494    fn max_scroll_top(&self, cx: &App) -> Pixels {
 495        let terminal = self.terminal.read(cx);
 496
 497        let Some(block) = self.block_below_cursor.as_ref() else {
 498            return Pixels::ZERO;
 499        };
 500
 501        let line_height = terminal.last_content().terminal_bounds.line_height;
 502        let viewport_lines = terminal.viewport_lines();
 503        let cursor = point_to_viewport(
 504            terminal.last_content.display_offset,
 505            terminal.last_content.cursor.point,
 506        )
 507        .unwrap_or_default();
 508        let max_scroll_top_in_lines =
 509            (block.height as usize).saturating_sub(viewport_lines.saturating_sub(cursor.line + 1));
 510
 511        max_scroll_top_in_lines as f32 * line_height
 512    }
 513
 514    fn scroll_wheel(&mut self, event: &ScrollWheelEvent, cx: &mut Context<Self>) {
 515        let terminal_content = self.terminal.read(cx).last_content();
 516
 517        if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
 518            let line_height = terminal_content.terminal_bounds.line_height;
 519            let y_delta = event.delta.pixel_delta(line_height).y;
 520            if y_delta < Pixels::ZERO || self.scroll_top > Pixels::ZERO {
 521                self.scroll_top = cmp::max(
 522                    Pixels::ZERO,
 523                    cmp::min(self.scroll_top - y_delta, self.max_scroll_top(cx)),
 524                );
 525                cx.notify();
 526                return;
 527            }
 528        }
 529        self.terminal.update(cx, |term, _| term.scroll_wheel(event));
 530    }
 531
 532    fn scroll_line_up(&mut self, _: &ScrollLineUp, _: &mut Window, cx: &mut Context<Self>) {
 533        let terminal_content = self.terminal.read(cx).last_content();
 534        if self.block_below_cursor.is_some()
 535            && terminal_content.display_offset == 0
 536            && self.scroll_top > Pixels::ZERO
 537        {
 538            let line_height = terminal_content.terminal_bounds.line_height;
 539            self.scroll_top = cmp::max(self.scroll_top - line_height, Pixels::ZERO);
 540            return;
 541        }
 542
 543        self.terminal.update(cx, |term, _| term.scroll_line_up());
 544        cx.notify();
 545    }
 546
 547    fn scroll_line_down(&mut self, _: &ScrollLineDown, _: &mut Window, cx: &mut Context<Self>) {
 548        let terminal_content = self.terminal.read(cx).last_content();
 549        if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
 550            let max_scroll_top = self.max_scroll_top(cx);
 551            if self.scroll_top < max_scroll_top {
 552                let line_height = terminal_content.terminal_bounds.line_height;
 553                self.scroll_top = cmp::min(self.scroll_top + line_height, max_scroll_top);
 554            }
 555            return;
 556        }
 557
 558        self.terminal.update(cx, |term, _| term.scroll_line_down());
 559        cx.notify();
 560    }
 561
 562    fn scroll_page_up(&mut self, _: &ScrollPageUp, _: &mut Window, cx: &mut Context<Self>) {
 563        if self.scroll_top == Pixels::ZERO {
 564            self.terminal.update(cx, |term, _| term.scroll_page_up());
 565        } else {
 566            let line_height = self
 567                .terminal
 568                .read(cx)
 569                .last_content
 570                .terminal_bounds
 571                .line_height();
 572            let visible_block_lines = (self.scroll_top / line_height) as usize;
 573            let viewport_lines = self.terminal.read(cx).viewport_lines();
 574            let visible_content_lines = viewport_lines - visible_block_lines;
 575
 576            if visible_block_lines >= viewport_lines {
 577                self.scroll_top = ((visible_block_lines - viewport_lines) as f32) * line_height;
 578            } else {
 579                self.scroll_top = px(0.);
 580                self.terminal
 581                    .update(cx, |term, _| term.scroll_up_by(visible_content_lines));
 582            }
 583        }
 584        cx.notify();
 585    }
 586
 587    fn scroll_page_down(&mut self, _: &ScrollPageDown, _: &mut Window, cx: &mut Context<Self>) {
 588        self.terminal.update(cx, |term, _| term.scroll_page_down());
 589        let terminal = self.terminal.read(cx);
 590        if terminal.last_content().display_offset < terminal.viewport_lines() {
 591            self.scroll_top = self.max_scroll_top(cx);
 592        }
 593        cx.notify();
 594    }
 595
 596    fn scroll_to_top(&mut self, _: &ScrollToTop, _: &mut Window, cx: &mut Context<Self>) {
 597        self.terminal.update(cx, |term, _| term.scroll_to_top());
 598        cx.notify();
 599    }
 600
 601    fn scroll_to_bottom(&mut self, _: &ScrollToBottom, _: &mut Window, cx: &mut Context<Self>) {
 602        self.terminal.update(cx, |term, _| term.scroll_to_bottom());
 603        if self.block_below_cursor.is_some() {
 604            self.scroll_top = self.max_scroll_top(cx);
 605        }
 606        cx.notify();
 607    }
 608
 609    fn toggle_vi_mode(&mut self, _: &ToggleViMode, _: &mut Window, cx: &mut Context<Self>) {
 610        self.terminal.update(cx, |term, _| term.toggle_vi_mode());
 611        cx.notify();
 612    }
 613
 614    pub fn should_show_cursor(&self, focused: bool, cx: &mut Context<Self>) -> bool {
 615        //Don't blink the cursor when not focused, blinking is disabled, or paused
 616        if !focused
 617            || self.blinking_paused
 618            || self
 619                .terminal
 620                .read(cx)
 621                .last_content
 622                .mode
 623                .contains(TermMode::ALT_SCREEN)
 624        {
 625            return true;
 626        }
 627
 628        match TerminalSettings::get_global(cx).blinking {
 629            //If the user requested to never blink, don't blink it.
 630            TerminalBlink::Off => true,
 631            //If the terminal is controlling it, check terminal mode
 632            TerminalBlink::TerminalControlled => {
 633                !self.blinking_terminal_enabled || self.blink_state
 634            }
 635            TerminalBlink::On => self.blink_state,
 636        }
 637    }
 638
 639    fn blink_cursors(&mut self, epoch: usize, window: &mut Window, cx: &mut Context<Self>) {
 640        if epoch == self.blink_epoch && !self.blinking_paused {
 641            self.blink_state = !self.blink_state;
 642            cx.notify();
 643
 644            let epoch = self.next_blink_epoch();
 645            cx.spawn_in(window, async move |this, cx| {
 646                Timer::after(CURSOR_BLINK_INTERVAL).await;
 647                this.update_in(cx, |this, window, cx| this.blink_cursors(epoch, window, cx))
 648                    .ok();
 649            })
 650            .detach();
 651        }
 652    }
 653
 654    pub fn pause_cursor_blinking(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 655        self.blink_state = true;
 656        cx.notify();
 657
 658        let epoch = self.next_blink_epoch();
 659        cx.spawn_in(window, async move |this, cx| {
 660            Timer::after(CURSOR_BLINK_INTERVAL).await;
 661            this.update_in(cx, |this, window, cx| {
 662                this.resume_cursor_blinking(epoch, window, cx)
 663            })
 664            .ok();
 665        })
 666        .detach();
 667    }
 668
 669    pub fn terminal(&self) -> &Entity<Terminal> {
 670        &self.terminal
 671    }
 672
 673    pub fn set_block_below_cursor(
 674        &mut self,
 675        block: BlockProperties,
 676        window: &mut Window,
 677        cx: &mut Context<Self>,
 678    ) {
 679        self.block_below_cursor = Some(Rc::new(block));
 680        self.scroll_to_bottom(&ScrollToBottom, window, cx);
 681        cx.notify();
 682    }
 683
 684    pub fn clear_block_below_cursor(&mut self, cx: &mut Context<Self>) {
 685        self.block_below_cursor = None;
 686        self.scroll_top = Pixels::ZERO;
 687        cx.notify();
 688    }
 689
 690    fn next_blink_epoch(&mut self) -> usize {
 691        self.blink_epoch += 1;
 692        self.blink_epoch
 693    }
 694
 695    fn resume_cursor_blinking(
 696        &mut self,
 697        epoch: usize,
 698        window: &mut Window,
 699        cx: &mut Context<Self>,
 700    ) {
 701        if epoch == self.blink_epoch {
 702            self.blinking_paused = false;
 703            self.blink_cursors(epoch, window, cx);
 704        }
 705    }
 706
 707    ///Attempt to paste the clipboard into the terminal
 708    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
 709        self.terminal.update(cx, |term, _| term.copy(None));
 710        cx.notify();
 711    }
 712
 713    ///Attempt to paste the clipboard into the terminal
 714    fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
 715        if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) {
 716            self.terminal
 717                .update(cx, |terminal, _cx| terminal.paste(&clipboard_string));
 718        }
 719    }
 720
 721    fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context<Self>) {
 722        self.clear_bell(cx);
 723        self.terminal.update(cx, |term, _| {
 724            term.input(text.0.to_string().into_bytes());
 725        });
 726    }
 727
 728    fn send_keystroke(&mut self, text: &SendKeystroke, _: &mut Window, cx: &mut Context<Self>) {
 729        if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
 730            self.clear_bell(cx);
 731            self.terminal.update(cx, |term, cx| {
 732                let processed =
 733                    term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta);
 734                if processed && term.vi_mode_enabled() {
 735                    cx.notify();
 736                }
 737                processed
 738            });
 739        }
 740    }
 741
 742    fn dispatch_context(&self, cx: &App) -> KeyContext {
 743        let mut dispatch_context = KeyContext::new_with_defaults();
 744        dispatch_context.add("Terminal");
 745
 746        if self.terminal.read(cx).vi_mode_enabled() {
 747            dispatch_context.add("vi_mode");
 748        }
 749
 750        let mode = self.terminal.read(cx).last_content.mode;
 751        dispatch_context.set(
 752            "screen",
 753            if mode.contains(TermMode::ALT_SCREEN) {
 754                "alt"
 755            } else {
 756                "normal"
 757            },
 758        );
 759
 760        if mode.contains(TermMode::APP_CURSOR) {
 761            dispatch_context.add("DECCKM");
 762        }
 763        if mode.contains(TermMode::APP_KEYPAD) {
 764            dispatch_context.add("DECPAM");
 765        } else {
 766            dispatch_context.add("DECPNM");
 767        }
 768        if mode.contains(TermMode::SHOW_CURSOR) {
 769            dispatch_context.add("DECTCEM");
 770        }
 771        if mode.contains(TermMode::LINE_WRAP) {
 772            dispatch_context.add("DECAWM");
 773        }
 774        if mode.contains(TermMode::ORIGIN) {
 775            dispatch_context.add("DECOM");
 776        }
 777        if mode.contains(TermMode::INSERT) {
 778            dispatch_context.add("IRM");
 779        }
 780        //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
 781        if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
 782            dispatch_context.add("LNM");
 783        }
 784        if mode.contains(TermMode::FOCUS_IN_OUT) {
 785            dispatch_context.add("report_focus");
 786        }
 787        if mode.contains(TermMode::ALTERNATE_SCROLL) {
 788            dispatch_context.add("alternate_scroll");
 789        }
 790        if mode.contains(TermMode::BRACKETED_PASTE) {
 791            dispatch_context.add("bracketed_paste");
 792        }
 793        if mode.intersects(TermMode::MOUSE_MODE) {
 794            dispatch_context.add("any_mouse_reporting");
 795        }
 796        {
 797            let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
 798                "click"
 799            } else if mode.contains(TermMode::MOUSE_DRAG) {
 800                "drag"
 801            } else if mode.contains(TermMode::MOUSE_MOTION) {
 802                "motion"
 803            } else {
 804                "off"
 805            };
 806            dispatch_context.set("mouse_reporting", mouse_reporting);
 807        }
 808        {
 809            let format = if mode.contains(TermMode::SGR_MOUSE) {
 810                "sgr"
 811            } else if mode.contains(TermMode::UTF8_MOUSE) {
 812                "utf8"
 813            } else {
 814                "normal"
 815            };
 816            dispatch_context.set("mouse_format", format);
 817        };
 818
 819        if self.terminal.read(cx).last_content.selection.is_some() {
 820            dispatch_context.add("selection");
 821        }
 822
 823        dispatch_context
 824    }
 825
 826    fn set_terminal(
 827        &mut self,
 828        terminal: Entity<Terminal>,
 829        window: &mut Window,
 830        cx: &mut Context<TerminalView>,
 831    ) {
 832        self._terminal_subscriptions =
 833            subscribe_for_terminal_events(&terminal, self.workspace.clone(), window, cx);
 834        self.terminal = terminal;
 835    }
 836
 837    // Hack: Using editor in terminal causes cyclic dependency i.e. editor -> terminal -> project -> editor.
 838    fn map_show_scrollbar_from_editor_to_terminal(
 839        show_scrollbar: editor::ShowScrollbar,
 840    ) -> terminal_settings::ShowScrollbar {
 841        match show_scrollbar {
 842            editor::ShowScrollbar::Auto => terminal_settings::ShowScrollbar::Auto,
 843            editor::ShowScrollbar::System => terminal_settings::ShowScrollbar::System,
 844            editor::ShowScrollbar::Always => terminal_settings::ShowScrollbar::Always,
 845            editor::ShowScrollbar::Never => terminal_settings::ShowScrollbar::Never,
 846        }
 847    }
 848
 849    fn should_show_scrollbar(cx: &App) -> bool {
 850        let show = TerminalSettings::get_global(cx)
 851            .scrollbar
 852            .show
 853            .unwrap_or_else(|| {
 854                Self::map_show_scrollbar_from_editor_to_terminal(
 855                    EditorSettings::get_global(cx).scrollbar.show,
 856                )
 857            });
 858        match show {
 859            terminal_settings::ShowScrollbar::Auto => true,
 860            terminal_settings::ShowScrollbar::System => true,
 861            terminal_settings::ShowScrollbar::Always => true,
 862            terminal_settings::ShowScrollbar::Never => false,
 863        }
 864    }
 865
 866    fn should_autohide_scrollbar(cx: &App) -> bool {
 867        let show = TerminalSettings::get_global(cx)
 868            .scrollbar
 869            .show
 870            .unwrap_or_else(|| {
 871                Self::map_show_scrollbar_from_editor_to_terminal(
 872                    EditorSettings::get_global(cx).scrollbar.show,
 873                )
 874            });
 875        match show {
 876            terminal_settings::ShowScrollbar::Auto => true,
 877            terminal_settings::ShowScrollbar::System => cx
 878                .try_global::<ScrollbarAutoHide>()
 879                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
 880            terminal_settings::ShowScrollbar::Always => false,
 881            terminal_settings::ShowScrollbar::Never => true,
 882        }
 883    }
 884
 885    fn hide_scrollbar(&mut self, cx: &mut Context<Self>) {
 886        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 887        if !Self::should_autohide_scrollbar(cx) {
 888            return;
 889        }
 890        self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| {
 891            cx.background_executor()
 892                .timer(SCROLLBAR_SHOW_INTERVAL)
 893                .await;
 894            panel
 895                .update(cx, |panel, cx| {
 896                    panel.show_scrollbar = false;
 897                    cx.notify();
 898                })
 899                .log_err();
 900        }))
 901    }
 902
 903    fn render_scrollbar(&self, window: &Window, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
 904        if !Self::should_show_scrollbar(cx)
 905            || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
 906            || !self.content_mode(window, cx).is_scrollable()
 907        {
 908            return None;
 909        }
 910
 911        if self.terminal.read(cx).total_lines() == self.terminal.read(cx).viewport_lines() {
 912            return None;
 913        }
 914
 915        self.scroll_handle.update(self.terminal.read(cx));
 916
 917        if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() {
 918            self.terminal.update(cx, |term, _| {
 919                let delta = new_display_offset as i32 - term.last_content.display_offset as i32;
 920                match delta.cmp(&0) {
 921                    std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize),
 922                    std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize),
 923                    std::cmp::Ordering::Equal => {}
 924                }
 925            });
 926        }
 927
 928        Some(
 929            div()
 930                .occlude()
 931                .id("terminal-view-scroll")
 932                .on_mouse_move(cx.listener(|_, _, _window, cx| {
 933                    cx.notify();
 934                    cx.stop_propagation()
 935                }))
 936                .on_hover(|_, _window, cx| {
 937                    cx.stop_propagation();
 938                })
 939                .on_any_mouse_down(|_, _window, cx| {
 940                    cx.stop_propagation();
 941                })
 942                .on_mouse_up(
 943                    MouseButton::Left,
 944                    cx.listener(|terminal_view, _, window, cx| {
 945                        if !terminal_view.scrollbar_state.is_dragging()
 946                            && !terminal_view.focus_handle.contains_focused(window, cx)
 947                        {
 948                            terminal_view.hide_scrollbar(cx);
 949                            cx.notify();
 950                        }
 951                        cx.stop_propagation();
 952                    }),
 953                )
 954                .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
 955                    cx.notify();
 956                }))
 957                .absolute()
 958                .top_0()
 959                .bottom_0()
 960                .right_0()
 961                .h_full()
 962                .w(TERMINAL_SCROLLBAR_WIDTH)
 963                .children(Scrollbar::vertical(self.scrollbar_state.clone())),
 964        )
 965    }
 966
 967    fn rerun_button(task: &TaskState) -> Option<IconButton> {
 968        if !task.show_rerun {
 969            return None;
 970        }
 971
 972        let task_id = task.id.clone();
 973        Some(
 974            IconButton::new("rerun-icon", IconName::Rerun)
 975                .icon_size(IconSize::Small)
 976                .size(ButtonSize::Compact)
 977                .icon_color(Color::Default)
 978                .shape(ui::IconButtonShape::Square)
 979                .tooltip(move |window, cx| {
 980                    Tooltip::for_action("Rerun task", &RerunTask, window, cx)
 981                })
 982                .on_click(move |_, window, cx| {
 983                    window.dispatch_action(Box::new(terminal_rerun_override(&task_id)), cx);
 984                }),
 985        )
 986    }
 987}
 988
 989fn terminal_rerun_override(task: &TaskId) -> zed_actions::Rerun {
 990    zed_actions::Rerun {
 991        task_id: Some(task.0.clone()),
 992        allow_concurrent_runs: Some(true),
 993        use_new_terminal: Some(false),
 994        reevaluate_context: false,
 995    }
 996}
 997
 998fn subscribe_for_terminal_events(
 999    terminal: &Entity<Terminal>,
1000    workspace: WeakEntity<Workspace>,
1001    window: &mut Window,
1002    cx: &mut Context<TerminalView>,
1003) -> Vec<Subscription> {
1004    let terminal_subscription = cx.observe(terminal, |_, _, cx| cx.notify());
1005    let mut previous_cwd = None;
1006    let terminal_events_subscription = cx.subscribe_in(
1007        terminal,
1008        window,
1009        move |terminal_view, terminal, event, window, cx| {
1010            let current_cwd = terminal.read(cx).working_directory();
1011            if current_cwd != previous_cwd {
1012                previous_cwd = current_cwd;
1013                terminal_view.cwd_serialized = false;
1014            }
1015
1016            match event {
1017                Event::Wakeup => {
1018                    cx.notify();
1019                    cx.emit(Event::Wakeup);
1020                    cx.emit(ItemEvent::UpdateTab);
1021                    cx.emit(SearchEvent::MatchesInvalidated);
1022                }
1023
1024                Event::Bell => {
1025                    terminal_view.has_bell = true;
1026                    cx.emit(Event::Wakeup);
1027                }
1028
1029                Event::BlinkChanged(blinking) => {
1030                    if matches!(
1031                        TerminalSettings::get_global(cx).blinking,
1032                        TerminalBlink::TerminalControlled
1033                    ) {
1034                        terminal_view.blinking_terminal_enabled = *blinking;
1035                    }
1036                }
1037
1038                Event::TitleChanged => {
1039                    cx.emit(ItemEvent::UpdateTab);
1040                }
1041
1042                Event::NewNavigationTarget(maybe_navigation_target) => {
1043                    match maybe_navigation_target
1044                        .as_ref()
1045                        .zip(terminal.read(cx).last_content.last_hovered_word.as_ref())
1046                    {
1047                        Some((MaybeNavigationTarget::Url(url), hovered_word)) => {
1048                            if Some(hovered_word)
1049                                != terminal_view
1050                                    .hover
1051                                    .as_ref()
1052                                    .map(|hover| &hover.hovered_word)
1053                            {
1054                                terminal_view.hover = Some(HoverTarget {
1055                                    tooltip: url.clone(),
1056                                    hovered_word: hovered_word.clone(),
1057                                });
1058                                terminal_view.hover_tooltip_update = Task::ready(());
1059                                cx.notify();
1060                            }
1061                        }
1062                        Some((MaybeNavigationTarget::PathLike(path_like_target), hovered_word)) => {
1063                            if Some(hovered_word)
1064                                != terminal_view
1065                                    .hover
1066                                    .as_ref()
1067                                    .map(|hover| &hover.hovered_word)
1068                            {
1069                                terminal_view.hover = None;
1070                                terminal_view.hover_tooltip_update = hover_path_like_target(
1071                                    &workspace,
1072                                    hovered_word.clone(),
1073                                    path_like_target,
1074                                    cx,
1075                                );
1076                                cx.notify();
1077                            }
1078                        }
1079                        None => {
1080                            terminal_view.hover = None;
1081                            terminal_view.hover_tooltip_update = Task::ready(());
1082                            cx.notify();
1083                        }
1084                    }
1085                }
1086
1087                Event::Open(maybe_navigation_target) => match maybe_navigation_target {
1088                    MaybeNavigationTarget::Url(url) => cx.open_url(url),
1089                    MaybeNavigationTarget::PathLike(path_like_target) => open_path_like_target(
1090                        &workspace,
1091                        terminal_view,
1092                        path_like_target,
1093                        window,
1094                        cx,
1095                    ),
1096                },
1097                Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
1098                Event::CloseTerminal => cx.emit(ItemEvent::CloseItem),
1099                Event::SelectionsChanged => {
1100                    window.invalidate_character_coordinates();
1101                    cx.emit(SearchEvent::ActiveMatchChanged)
1102                }
1103            }
1104        },
1105    );
1106    vec![terminal_subscription, terminal_events_subscription]
1107}
1108
1109fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
1110    let str = query.as_str();
1111    if query.is_regex() {
1112        if str == "." {
1113            return None;
1114        }
1115        RegexSearch::new(str).ok()
1116    } else {
1117        RegexSearch::new(&regex::escape(str)).ok()
1118    }
1119}
1120
1121impl TerminalView {
1122    fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
1123        self.clear_bell(cx);
1124        self.pause_cursor_blinking(window, cx);
1125
1126        self.terminal.update(cx, |term, cx| {
1127            let handled = term.try_keystroke(
1128                &event.keystroke,
1129                TerminalSettings::get_global(cx).option_as_meta,
1130            );
1131            if handled {
1132                cx.stop_propagation();
1133            }
1134        });
1135    }
1136
1137    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1138        self.terminal.update(cx, |terminal, _| {
1139            terminal.set_cursor_shape(self.cursor_shape);
1140            terminal.focus_in();
1141        });
1142        self.blink_cursors(self.blink_epoch, window, cx);
1143        window.invalidate_character_coordinates();
1144        cx.notify();
1145    }
1146
1147    fn focus_out(&mut self, _: &mut Window, cx: &mut Context<Self>) {
1148        self.terminal.update(cx, |terminal, _| {
1149            terminal.focus_out();
1150            terminal.set_cursor_shape(CursorShape::Hollow);
1151        });
1152        self.hide_scrollbar(cx);
1153        cx.notify();
1154    }
1155}
1156
1157impl Render for TerminalView {
1158    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1159        let terminal_handle = self.terminal.clone();
1160        let terminal_view_handle = cx.entity();
1161
1162        let focused = self.focus_handle.is_focused(window);
1163
1164        // Always calculate scrollbar width to prevent layout shift
1165        let scrollbar_width = if Self::should_show_scrollbar(cx)
1166            && self.content_mode(window, cx).is_scrollable()
1167            && self.terminal.read(cx).total_lines() > self.terminal.read(cx).viewport_lines()
1168        {
1169            TERMINAL_SCROLLBAR_WIDTH
1170        } else {
1171            px(0.)
1172        };
1173
1174        div()
1175            .id("terminal-view")
1176            .size_full()
1177            .relative()
1178            .track_focus(&self.focus_handle(cx))
1179            .key_context(self.dispatch_context(cx))
1180            .on_action(cx.listener(TerminalView::send_text))
1181            .on_action(cx.listener(TerminalView::send_keystroke))
1182            .on_action(cx.listener(TerminalView::copy))
1183            .on_action(cx.listener(TerminalView::paste))
1184            .on_action(cx.listener(TerminalView::clear))
1185            .on_action(cx.listener(TerminalView::scroll_line_up))
1186            .on_action(cx.listener(TerminalView::scroll_line_down))
1187            .on_action(cx.listener(TerminalView::scroll_page_up))
1188            .on_action(cx.listener(TerminalView::scroll_page_down))
1189            .on_action(cx.listener(TerminalView::scroll_to_top))
1190            .on_action(cx.listener(TerminalView::scroll_to_bottom))
1191            .on_action(cx.listener(TerminalView::toggle_vi_mode))
1192            .on_action(cx.listener(TerminalView::show_character_palette))
1193            .on_action(cx.listener(TerminalView::select_all))
1194            .on_action(cx.listener(TerminalView::rerun_task))
1195            .on_key_down(cx.listener(Self::key_down))
1196            .on_mouse_down(
1197                MouseButton::Right,
1198                cx.listener(|this, event: &MouseDownEvent, window, cx| {
1199                    if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) {
1200                        if this.terminal.read(cx).last_content.selection.is_none() {
1201                            this.terminal.update(cx, |terminal, _| {
1202                                terminal.select_word_at_event_position(event);
1203                            });
1204                        };
1205                        this.deploy_context_menu(event.position, window, cx);
1206                        cx.notify();
1207                    }
1208                }),
1209            )
1210            .on_hover(cx.listener(|this, hovered, window, cx| {
1211                if *hovered {
1212                    this.show_scrollbar = true;
1213                    this.hide_scrollbar_task.take();
1214                    cx.notify();
1215                } else if !this.focus_handle.contains_focused(window, cx) {
1216                    this.hide_scrollbar(cx);
1217                }
1218            }))
1219            .child(
1220                // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu
1221                div()
1222                    .size_full()
1223                    .bg(cx.theme().colors().editor_background)
1224                    .when(scrollbar_width > px(0.), |div| div.pr(scrollbar_width))
1225                    .child(TerminalElement::new(
1226                        terminal_handle,
1227                        terminal_view_handle,
1228                        self.workspace.clone(),
1229                        self.focus_handle.clone(),
1230                        focused,
1231                        self.should_show_cursor(focused, cx),
1232                        self.block_below_cursor.clone(),
1233                        self.mode.clone(),
1234                    ))
1235                    .when_some(self.render_scrollbar(window, cx), |div, scrollbar| {
1236                        div.child(scrollbar)
1237                    }),
1238            )
1239            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1240                deferred(
1241                    anchored()
1242                        .position(*position)
1243                        .anchor(gpui::Corner::TopLeft)
1244                        .child(menu.clone()),
1245                )
1246                .with_priority(1)
1247            }))
1248    }
1249}
1250
1251impl Item for TerminalView {
1252    type Event = ItemEvent;
1253
1254    fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent> {
1255        let terminal = self.terminal().read(cx);
1256        let title = terminal.title(false);
1257        let pid = terminal.pty_info.pid_getter().fallback_pid();
1258
1259        Some(TabTooltipContent::Custom(Box::new(move |_window, cx| {
1260            cx.new(|_| TerminalTooltip::new(title.clone(), pid)).into()
1261        })))
1262    }
1263
1264    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
1265        let terminal = self.terminal().read(cx);
1266        let title = terminal.title(true);
1267
1268        let (icon, icon_color, rerun_button) = match terminal.task() {
1269            Some(terminal_task) => match &terminal_task.status {
1270                TaskStatus::Running => (
1271                    IconName::PlayFilled,
1272                    Color::Disabled,
1273                    TerminalView::rerun_button(terminal_task),
1274                ),
1275                TaskStatus::Unknown => (
1276                    IconName::Warning,
1277                    Color::Warning,
1278                    TerminalView::rerun_button(terminal_task),
1279                ),
1280                TaskStatus::Completed { success } => {
1281                    let rerun_button = TerminalView::rerun_button(terminal_task);
1282
1283                    if *success {
1284                        (IconName::Check, Color::Success, rerun_button)
1285                    } else {
1286                        (IconName::XCircle, Color::Error, rerun_button)
1287                    }
1288                }
1289            },
1290            None => (IconName::Terminal, Color::Muted, None),
1291        };
1292
1293        h_flex()
1294            .gap_1()
1295            .group("term-tab-icon")
1296            .child(
1297                h_flex()
1298                    .group("term-tab-icon")
1299                    .child(
1300                        div()
1301                            .when(rerun_button.is_some(), |this| {
1302                                this.hover(|style| style.invisible().w_0())
1303                            })
1304                            .child(Icon::new(icon).color(icon_color)),
1305                    )
1306                    .when_some(rerun_button, |this, rerun_button| {
1307                        this.child(
1308                            div()
1309                                .absolute()
1310                                .visible_on_hover("term-tab-icon")
1311                                .child(rerun_button),
1312                        )
1313                    }),
1314            )
1315            .child(Label::new(title).color(params.text_color()))
1316            .into_any()
1317    }
1318
1319    fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
1320        let terminal = self.terminal().read(cx);
1321        terminal.title(detail == 0).into()
1322    }
1323
1324    fn telemetry_event_text(&self) -> Option<&'static str> {
1325        None
1326    }
1327
1328    fn clone_on_split(
1329        &self,
1330        workspace_id: Option<WorkspaceId>,
1331        window: &mut Window,
1332        cx: &mut Context<Self>,
1333    ) -> Option<Entity<Self>> {
1334        let terminal = self
1335            .project
1336            .update(cx, |project, cx| {
1337                let terminal = self.terminal().read(cx);
1338                let working_directory = terminal
1339                    .working_directory()
1340                    .or_else(|| Some(project.active_project_directory(cx)?.to_path_buf()));
1341                let python_venv_directory = terminal.python_venv_directory.clone();
1342                project.create_terminal_with_venv(
1343                    TerminalKind::Shell(working_directory),
1344                    python_venv_directory,
1345                    cx,
1346                )
1347            })
1348            .ok()?
1349            .log_err()?;
1350
1351        Some(cx.new(|cx| {
1352            TerminalView::new(
1353                terminal,
1354                self.workspace.clone(),
1355                workspace_id,
1356                self.project.clone(),
1357                window,
1358                cx,
1359            )
1360        }))
1361    }
1362
1363    fn is_dirty(&self, cx: &gpui::App) -> bool {
1364        match self.terminal.read(cx).task() {
1365            Some(task) => task.status == TaskStatus::Running,
1366            None => self.has_bell(),
1367        }
1368    }
1369
1370    fn has_conflict(&self, _cx: &App) -> bool {
1371        false
1372    }
1373
1374    fn can_save_as(&self, _cx: &App) -> bool {
1375        false
1376    }
1377
1378    fn is_singleton(&self, _cx: &App) -> bool {
1379        true
1380    }
1381
1382    fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1383        Some(Box::new(handle.clone()))
1384    }
1385
1386    fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1387        if self.show_breadcrumbs && !self.terminal().read(cx).breadcrumb_text.trim().is_empty() {
1388            ToolbarItemLocation::PrimaryLeft
1389        } else {
1390            ToolbarItemLocation::Hidden
1391        }
1392    }
1393
1394    fn breadcrumbs(&self, _: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
1395        Some(vec![BreadcrumbText {
1396            text: self.terminal().read(cx).breadcrumb_text.clone(),
1397            highlights: None,
1398            font: None,
1399        }])
1400    }
1401
1402    fn added_to_workspace(
1403        &mut self,
1404        workspace: &mut Workspace,
1405        _: &mut Window,
1406        cx: &mut Context<Self>,
1407    ) {
1408        if self.terminal().read(cx).task().is_none() {
1409            if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) {
1410                log::debug!(
1411                    "Updating workspace id for the terminal, old: {old_id:?}, new: {new_id:?}",
1412                );
1413                cx.background_spawn(TERMINAL_DB.update_workspace_id(
1414                    new_id,
1415                    old_id,
1416                    cx.entity_id().as_u64(),
1417                ))
1418                .detach();
1419            }
1420            self.workspace_id = workspace.database_id();
1421        }
1422    }
1423
1424    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1425        f(*event)
1426    }
1427}
1428
1429impl SerializableItem for TerminalView {
1430    fn serialized_item_kind() -> &'static str {
1431        "Terminal"
1432    }
1433
1434    fn cleanup(
1435        workspace_id: WorkspaceId,
1436        alive_items: Vec<workspace::ItemId>,
1437        _window: &mut Window,
1438        cx: &mut App,
1439    ) -> Task<anyhow::Result<()>> {
1440        delete_unloaded_items(alive_items, workspace_id, "terminals", &TERMINAL_DB, cx)
1441    }
1442
1443    fn serialize(
1444        &mut self,
1445        _workspace: &mut Workspace,
1446        item_id: workspace::ItemId,
1447        _closing: bool,
1448        _: &mut Window,
1449        cx: &mut Context<Self>,
1450    ) -> Option<Task<anyhow::Result<()>>> {
1451        let terminal = self.terminal().read(cx);
1452        if terminal.task().is_some() {
1453            return None;
1454        }
1455
1456        if let Some((cwd, workspace_id)) = terminal.working_directory().zip(self.workspace_id) {
1457            self.cwd_serialized = true;
1458            Some(cx.background_spawn(async move {
1459                TERMINAL_DB
1460                    .save_working_directory(item_id, workspace_id, cwd)
1461                    .await
1462            }))
1463        } else {
1464            None
1465        }
1466    }
1467
1468    fn should_serialize(&self, _: &Self::Event) -> bool {
1469        !self.cwd_serialized
1470    }
1471
1472    fn deserialize(
1473        project: Entity<Project>,
1474        workspace: WeakEntity<Workspace>,
1475        workspace_id: workspace::WorkspaceId,
1476        item_id: workspace::ItemId,
1477        window: &mut Window,
1478        cx: &mut App,
1479    ) -> Task<anyhow::Result<Entity<Self>>> {
1480        window.spawn(cx, async move |cx| {
1481            let cwd = cx
1482                .update(|_window, cx| {
1483                    let from_db = TERMINAL_DB
1484                        .get_working_directory(item_id, workspace_id)
1485                        .log_err()
1486                        .flatten();
1487                    if from_db
1488                        .as_ref()
1489                        .is_some_and(|from_db| !from_db.as_os_str().is_empty())
1490                    {
1491                        from_db
1492                    } else {
1493                        workspace
1494                            .upgrade()
1495                            .and_then(|workspace| default_working_directory(workspace.read(cx), cx))
1496                    }
1497                })
1498                .ok()
1499                .flatten();
1500
1501            let terminal = project
1502                .update(cx, |project, cx| {
1503                    project.create_terminal(TerminalKind::Shell(cwd), cx)
1504                })?
1505                .await?;
1506            cx.update(|window, cx| {
1507                cx.new(|cx| {
1508                    TerminalView::new(
1509                        terminal,
1510                        workspace,
1511                        Some(workspace_id),
1512                        project.downgrade(),
1513                        window,
1514                        cx,
1515                    )
1516                })
1517            })
1518        })
1519    }
1520}
1521
1522impl SearchableItem for TerminalView {
1523    type Match = RangeInclusive<Point>;
1524
1525    fn supported_options(&self) -> SearchOptions {
1526        SearchOptions {
1527            case: false,
1528            word: false,
1529            regex: true,
1530            replacement: false,
1531            selection: false,
1532            find_in_results: false,
1533        }
1534    }
1535
1536    /// Clear stored matches
1537    fn clear_matches(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1538        self.terminal().update(cx, |term, _| term.clear_matches())
1539    }
1540
1541    /// Store matches returned from find_matches somewhere for rendering
1542    fn update_matches(
1543        &mut self,
1544        matches: &[Self::Match],
1545        _window: &mut Window,
1546        cx: &mut Context<Self>,
1547    ) {
1548        self.terminal()
1549            .update(cx, |term, _| term.matches = matches.to_vec())
1550    }
1551
1552    /// Returns the selection content to pre-load into this search
1553    fn query_suggestion(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> String {
1554        self.terminal()
1555            .read(cx)
1556            .last_content
1557            .selection_text
1558            .clone()
1559            .unwrap_or_default()
1560    }
1561
1562    /// Focus match at given index into the Vec of matches
1563    fn activate_match(
1564        &mut self,
1565        index: usize,
1566        _: &[Self::Match],
1567        _window: &mut Window,
1568        cx: &mut Context<Self>,
1569    ) {
1570        self.terminal()
1571            .update(cx, |term, _| term.activate_match(index));
1572        cx.notify();
1573    }
1574
1575    /// Add selections for all matches given.
1576    fn select_matches(&mut self, matches: &[Self::Match], _: &mut Window, cx: &mut Context<Self>) {
1577        self.terminal()
1578            .update(cx, |term, _| term.select_matches(matches));
1579        cx.notify();
1580    }
1581
1582    /// Get all of the matches for this query, should be done on the background
1583    fn find_matches(
1584        &mut self,
1585        query: Arc<SearchQuery>,
1586        _: &mut Window,
1587        cx: &mut Context<Self>,
1588    ) -> Task<Vec<Self::Match>> {
1589        if let Some(s) = regex_search_for_query(&query) {
1590            self.terminal()
1591                .update(cx, |term, cx| term.find_matches(s, cx))
1592        } else {
1593            Task::ready(vec![])
1594        }
1595    }
1596
1597    /// Reports back to the search toolbar what the active match should be (the selection)
1598    fn active_match_index(
1599        &mut self,
1600        direction: Direction,
1601        matches: &[Self::Match],
1602        _: &mut Window,
1603        cx: &mut Context<Self>,
1604    ) -> Option<usize> {
1605        // Selection head might have a value if there's a selection that isn't
1606        // associated with a match. Therefore, if there are no matches, we should
1607        // report None, no matter the state of the terminal
1608
1609        if !matches.is_empty() {
1610            if let Some(selection_head) = self.terminal().read(cx).selection_head {
1611                // If selection head is contained in a match. Return that match
1612                match direction {
1613                    Direction::Prev => {
1614                        // If no selection before selection head, return the first match
1615                        Some(
1616                            matches
1617                                .iter()
1618                                .enumerate()
1619                                .rev()
1620                                .find(|(_, search_match)| {
1621                                    search_match.contains(&selection_head)
1622                                        || search_match.start() < &selection_head
1623                                })
1624                                .map(|(ix, _)| ix)
1625                                .unwrap_or(0),
1626                        )
1627                    }
1628                    Direction::Next => {
1629                        // If no selection after selection head, return the last match
1630                        Some(
1631                            matches
1632                                .iter()
1633                                .enumerate()
1634                                .find(|(_, search_match)| {
1635                                    search_match.contains(&selection_head)
1636                                        || search_match.start() > &selection_head
1637                                })
1638                                .map(|(ix, _)| ix)
1639                                .unwrap_or(matches.len().saturating_sub(1)),
1640                        )
1641                    }
1642                }
1643            } else {
1644                // Matches found but no active selection, return the first last one (closest to cursor)
1645                Some(matches.len().saturating_sub(1))
1646            }
1647        } else {
1648            None
1649        }
1650    }
1651    fn replace(
1652        &mut self,
1653        _: &Self::Match,
1654        _: &SearchQuery,
1655        _window: &mut Window,
1656        _: &mut Context<Self>,
1657    ) {
1658        // Replacement is not supported in terminal view, so this is a no-op.
1659    }
1660}
1661
1662///Gets the working directory for the given workspace, respecting the user's settings.
1663/// None implies "~" on whichever machine we end up on.
1664pub(crate) fn default_working_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
1665    match &TerminalSettings::get_global(cx).working_directory {
1666        WorkingDirectory::CurrentProjectDirectory => workspace
1667            .project()
1668            .read(cx)
1669            .active_project_directory(cx)
1670            .as_deref()
1671            .map(Path::to_path_buf),
1672        WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
1673        WorkingDirectory::AlwaysHome => None,
1674        WorkingDirectory::Always { directory } => {
1675            shellexpand::full(&directory) //TODO handle this better
1676                .ok()
1677                .map(|dir| Path::new(&dir.to_string()).to_path_buf())
1678                .filter(|dir| dir.is_dir())
1679        }
1680    }
1681}
1682///Gets the first project's home directory, or the home directory
1683fn first_project_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
1684    let worktree = workspace.worktrees(cx).next()?.read(cx);
1685    if !worktree.root_entry()?.is_dir() {
1686        return None;
1687    }
1688    Some(worktree.abs_path().to_path_buf())
1689}
1690
1691#[cfg(test)]
1692mod tests {
1693    use super::*;
1694    use gpui::TestAppContext;
1695    use project::{Entry, Project, ProjectPath, Worktree};
1696    use std::path::Path;
1697    use workspace::AppState;
1698
1699    // Working directory calculation tests
1700
1701    // No Worktrees in project -> home_dir()
1702    #[gpui::test]
1703    async fn no_worktree(cx: &mut TestAppContext) {
1704        let (project, workspace) = init_test(cx).await;
1705        cx.read(|cx| {
1706            let workspace = workspace.read(cx);
1707            let active_entry = project.read(cx).active_entry();
1708
1709            //Make sure environment is as expected
1710            assert!(active_entry.is_none());
1711            assert!(workspace.worktrees(cx).next().is_none());
1712
1713            let res = default_working_directory(workspace, cx);
1714            assert_eq!(res, None);
1715            let res = first_project_directory(workspace, cx);
1716            assert_eq!(res, None);
1717        });
1718    }
1719
1720    // No active entry, but a worktree, worktree is a file -> home_dir()
1721    #[gpui::test]
1722    async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
1723        let (project, workspace) = init_test(cx).await;
1724
1725        create_file_wt(project.clone(), "/root.txt", cx).await;
1726        cx.read(|cx| {
1727            let workspace = workspace.read(cx);
1728            let active_entry = project.read(cx).active_entry();
1729
1730            //Make sure environment is as expected
1731            assert!(active_entry.is_none());
1732            assert!(workspace.worktrees(cx).next().is_some());
1733
1734            let res = default_working_directory(workspace, cx);
1735            assert_eq!(res, None);
1736            let res = first_project_directory(workspace, cx);
1737            assert_eq!(res, None);
1738        });
1739    }
1740
1741    // No active entry, but a worktree, worktree is a folder -> worktree_folder
1742    #[gpui::test]
1743    async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
1744        let (project, workspace) = init_test(cx).await;
1745
1746        let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
1747        cx.update(|cx| {
1748            let workspace = workspace.read(cx);
1749            let active_entry = project.read(cx).active_entry();
1750
1751            assert!(active_entry.is_none());
1752            assert!(workspace.worktrees(cx).next().is_some());
1753
1754            let res = default_working_directory(workspace, cx);
1755            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
1756            let res = first_project_directory(workspace, cx);
1757            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
1758        });
1759    }
1760
1761    // Active entry with a work tree, worktree is a file -> worktree_folder()
1762    #[gpui::test]
1763    async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
1764        let (project, workspace) = init_test(cx).await;
1765
1766        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
1767        let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
1768        insert_active_entry_for(wt2, entry2, project.clone(), cx);
1769
1770        cx.update(|cx| {
1771            let workspace = workspace.read(cx);
1772            let active_entry = project.read(cx).active_entry();
1773
1774            assert!(active_entry.is_some());
1775
1776            let res = default_working_directory(workspace, cx);
1777            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
1778            let res = first_project_directory(workspace, cx);
1779            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
1780        });
1781    }
1782
1783    // Active entry, with a worktree, worktree is a folder -> worktree_folder
1784    #[gpui::test]
1785    async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
1786        let (project, workspace) = init_test(cx).await;
1787
1788        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
1789        let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
1790        insert_active_entry_for(wt2, entry2, project.clone(), cx);
1791
1792        cx.update(|cx| {
1793            let workspace = workspace.read(cx);
1794            let active_entry = project.read(cx).active_entry();
1795
1796            assert!(active_entry.is_some());
1797
1798            let res = default_working_directory(workspace, cx);
1799            assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
1800            let res = first_project_directory(workspace, cx);
1801            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
1802        });
1803    }
1804
1805    /// Creates a worktree with 1 file: /root.txt
1806    pub async fn init_test(cx: &mut TestAppContext) -> (Entity<Project>, Entity<Workspace>) {
1807        let params = cx.update(AppState::test);
1808        cx.update(|cx| {
1809            terminal::init(cx);
1810            theme::init(theme::LoadThemes::JustBase, cx);
1811            Project::init_settings(cx);
1812            language::init(cx);
1813        });
1814
1815        let project = Project::test(params.fs.clone(), [], cx).await;
1816        let workspace = cx
1817            .add_window(|window, cx| Workspace::test_new(project.clone(), window, cx))
1818            .root(cx)
1819            .unwrap();
1820
1821        (project, workspace)
1822    }
1823
1824    /// Creates a worktree with 1 folder: /root{suffix}/
1825    async fn create_folder_wt(
1826        project: Entity<Project>,
1827        path: impl AsRef<Path>,
1828        cx: &mut TestAppContext,
1829    ) -> (Entity<Worktree>, Entry) {
1830        create_wt(project, true, path, cx).await
1831    }
1832
1833    /// Creates a worktree with 1 file: /root{suffix}.txt
1834    async fn create_file_wt(
1835        project: Entity<Project>,
1836        path: impl AsRef<Path>,
1837        cx: &mut TestAppContext,
1838    ) -> (Entity<Worktree>, Entry) {
1839        create_wt(project, false, path, cx).await
1840    }
1841
1842    async fn create_wt(
1843        project: Entity<Project>,
1844        is_dir: bool,
1845        path: impl AsRef<Path>,
1846        cx: &mut TestAppContext,
1847    ) -> (Entity<Worktree>, Entry) {
1848        let (wt, _) = project
1849            .update(cx, |project, cx| {
1850                project.find_or_create_worktree(path, true, cx)
1851            })
1852            .await
1853            .unwrap();
1854
1855        let entry = cx
1856            .update(|cx| {
1857                wt.update(cx, |wt, cx| {
1858                    wt.create_entry(Path::new(""), is_dir, None, cx)
1859                })
1860            })
1861            .await
1862            .unwrap()
1863            .into_included()
1864            .unwrap();
1865
1866        (wt, entry)
1867    }
1868
1869    pub fn insert_active_entry_for(
1870        wt: Entity<Worktree>,
1871        entry: Entry,
1872        project: Entity<Project>,
1873        cx: &mut TestAppContext,
1874    ) {
1875        cx.update(|cx| {
1876            let p = ProjectPath {
1877                worktree_id: wt.read(cx).id(),
1878                path: entry.path,
1879            };
1880            project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
1881        });
1882    }
1883}