terminal_view.rs

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