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};
  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
  65pub struct ImeState {
  66    pub marked_text: String,
  67    pub marked_range_utf16: Option<Range<usize>>,
  68}
  69
  70const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
  71const TERMINAL_SCROLLBAR_WIDTH: Pixels = px(12.);
  72
  73/// Event to transmit the scroll from the element to the view
  74#[derive(Clone, Debug, PartialEq)]
  75pub struct ScrollTerminal(pub i32);
  76
  77/// Sends the specified text directly to the terminal.
  78#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Action)]
  79#[action(namespace = terminal)]
  80pub struct SendText(String);
  81
  82/// Sends a keystroke sequence to the terminal.
  83#[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq, Action)]
  84#[action(namespace = terminal)]
  85pub struct SendKeystroke(String);
  86
  87actions!(
  88    terminal,
  89    [
  90        /// Reruns the last executed task in the terminal.
  91        RerunTask
  92    ]
  93);
  94
  95pub fn init(cx: &mut App) {
  96    assistant_slash_command::init(cx);
  97    terminal_panel::init(cx);
  98    terminal::init(cx);
  99
 100    register_serializable_item::<TerminalView>(cx);
 101
 102    cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
 103        workspace.register_action(TerminalView::deploy);
 104    })
 105    .detach();
 106    SlashCommandRegistry::global(cx).register_command(TerminalSlashCommand, true);
 107}
 108
 109pub struct BlockProperties {
 110    pub height: u8,
 111    pub render: Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>,
 112}
 113
 114pub struct BlockContext<'a, 'b> {
 115    pub window: &'a mut Window,
 116    pub context: &'b mut App,
 117    pub dimensions: TerminalBounds,
 118}
 119
 120///A terminal view, maintains the PTY's file handles and communicates with the terminal
 121pub struct TerminalView {
 122    terminal: Entity<Terminal>,
 123    workspace: WeakEntity<Workspace>,
 124    project: WeakEntity<Project>,
 125    focus_handle: FocusHandle,
 126    //Currently using iTerm bell, show bell emoji in tab until input is received
 127    has_bell: bool,
 128    context_menu: Option<(Entity<ContextMenu>, gpui::Point<Pixels>, Subscription)>,
 129    cursor_shape: CursorShape,
 130    blink_state: bool,
 131    mode: TerminalMode,
 132    blinking_terminal_enabled: bool,
 133    cwd_serialized: bool,
 134    blinking_paused: bool,
 135    blink_epoch: usize,
 136    hover: Option<HoverTarget>,
 137    hover_tooltip_update: Task<()>,
 138    workspace_id: Option<WorkspaceId>,
 139    show_breadcrumbs: bool,
 140    block_below_cursor: Option<Rc<BlockProperties>>,
 141    scroll_top: Pixels,
 142    scrollbar_state: ScrollbarState,
 143    scroll_handle: TerminalScrollHandle,
 144    show_scrollbar: bool,
 145    hide_scrollbar_task: Option<Task<()>>,
 146    ime_state: Option<ImeState>,
 147    _subscriptions: Vec<Subscription>,
 148    _terminal_subscriptions: Vec<Subscription>,
 149}
 150
 151#[derive(Default, Clone)]
 152pub enum TerminalMode {
 153    #[default]
 154    Standalone,
 155    Embedded {
 156        max_lines_when_unfocused: Option<usize>,
 157    },
 158}
 159
 160#[derive(Clone)]
 161pub enum ContentMode {
 162    Scrollable,
 163    Inline {
 164        displayed_lines: usize,
 165        total_lines: usize,
 166    },
 167}
 168
 169impl ContentMode {
 170    pub fn is_limited(&self) -> bool {
 171        match self {
 172            ContentMode::Scrollable => false,
 173            ContentMode::Inline {
 174                displayed_lines,
 175                total_lines,
 176            } => displayed_lines < total_lines,
 177        }
 178    }
 179
 180    pub fn is_scrollable(&self) -> bool {
 181        matches!(self, ContentMode::Scrollable)
 182    }
 183}
 184
 185#[derive(Debug)]
 186#[cfg_attr(test, derive(Clone, Eq, PartialEq))]
 187struct HoverTarget {
 188    tooltip: String,
 189    hovered_word: HoveredWord,
 190}
 191
 192impl EventEmitter<Event> for TerminalView {}
 193impl EventEmitter<ItemEvent> for TerminalView {}
 194impl EventEmitter<SearchEvent> for TerminalView {}
 195
 196impl Focusable for TerminalView {
 197    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 198        self.focus_handle.clone()
 199    }
 200}
 201
 202impl TerminalView {
 203    ///Create a new Terminal in the current working directory or the user's home directory
 204    pub fn deploy(
 205        workspace: &mut Workspace,
 206        _: &NewCenterTerminal,
 207        window: &mut Window,
 208        cx: &mut Context<Workspace>,
 209    ) {
 210        let working_directory = default_working_directory(workspace, cx);
 211        TerminalPanel::add_center_terminal(workspace, window, cx, |project, cx| {
 212            project.create_terminal_shell(working_directory, 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            ime_state: 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: Option<Range<usize>>,
 330        cx: &mut Context<Self>,
 331    ) {
 332        self.ime_state = Some(ImeState {
 333            marked_text: text,
 334            marked_range_utf16: range,
 335        });
 336        cx.notify();
 337    }
 338
 339    /// Gets the current marked range (UTF-16).
 340    pub(crate) fn marked_text_range(&self) -> Option<Range<usize>> {
 341        self.ime_state.as_ref()?.marked_range_utf16.clone()
 342    }
 343
 344    /// Clears the marked (pre-edit) text state.
 345    pub(crate) fn clear_marked_text(&mut self, cx: &mut Context<Self>) {
 346        if self.ime_state.is_some() {
 347            self.ime_state = 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 cwd = project
1338                    .active_project_directory(cx)
1339                    .map(|it| it.to_path_buf());
1340                project.clone_terminal(self.terminal(), cx, || cwd)
1341            })
1342            .ok()?
1343            .log_err()?;
1344
1345        Some(cx.new(|cx| {
1346            TerminalView::new(
1347                terminal,
1348                self.workspace.clone(),
1349                workspace_id,
1350                self.project.clone(),
1351                window,
1352                cx,
1353            )
1354        }))
1355    }
1356
1357    fn is_dirty(&self, cx: &gpui::App) -> bool {
1358        match self.terminal.read(cx).task() {
1359            Some(task) => task.status == TaskStatus::Running,
1360            None => self.has_bell(),
1361        }
1362    }
1363
1364    fn has_conflict(&self, _cx: &App) -> bool {
1365        false
1366    }
1367
1368    fn can_save_as(&self, _cx: &App) -> bool {
1369        false
1370    }
1371
1372    fn is_singleton(&self, _cx: &App) -> bool {
1373        true
1374    }
1375
1376    fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1377        Some(Box::new(handle.clone()))
1378    }
1379
1380    fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1381        if self.show_breadcrumbs && !self.terminal().read(cx).breadcrumb_text.trim().is_empty() {
1382            ToolbarItemLocation::PrimaryLeft
1383        } else {
1384            ToolbarItemLocation::Hidden
1385        }
1386    }
1387
1388    fn breadcrumbs(&self, _: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
1389        Some(vec![BreadcrumbText {
1390            text: self.terminal().read(cx).breadcrumb_text.clone(),
1391            highlights: None,
1392            font: None,
1393        }])
1394    }
1395
1396    fn added_to_workspace(
1397        &mut self,
1398        workspace: &mut Workspace,
1399        _: &mut Window,
1400        cx: &mut Context<Self>,
1401    ) {
1402        if self.terminal().read(cx).task().is_none() {
1403            if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) {
1404                log::debug!(
1405                    "Updating workspace id for the terminal, old: {old_id:?}, new: {new_id:?}",
1406                );
1407                cx.background_spawn(TERMINAL_DB.update_workspace_id(
1408                    new_id,
1409                    old_id,
1410                    cx.entity_id().as_u64(),
1411                ))
1412                .detach();
1413            }
1414            self.workspace_id = workspace.database_id();
1415        }
1416    }
1417
1418    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1419        f(*event)
1420    }
1421}
1422
1423impl SerializableItem for TerminalView {
1424    fn serialized_item_kind() -> &'static str {
1425        "Terminal"
1426    }
1427
1428    fn cleanup(
1429        workspace_id: WorkspaceId,
1430        alive_items: Vec<workspace::ItemId>,
1431        _window: &mut Window,
1432        cx: &mut App,
1433    ) -> Task<anyhow::Result<()>> {
1434        delete_unloaded_items(alive_items, workspace_id, "terminals", &TERMINAL_DB, cx)
1435    }
1436
1437    fn serialize(
1438        &mut self,
1439        _workspace: &mut Workspace,
1440        item_id: workspace::ItemId,
1441        _closing: bool,
1442        _: &mut Window,
1443        cx: &mut Context<Self>,
1444    ) -> Option<Task<anyhow::Result<()>>> {
1445        let terminal = self.terminal().read(cx);
1446        if terminal.task().is_some() {
1447            return None;
1448        }
1449
1450        if let Some((cwd, workspace_id)) = terminal.working_directory().zip(self.workspace_id) {
1451            self.cwd_serialized = true;
1452            Some(cx.background_spawn(async move {
1453                TERMINAL_DB
1454                    .save_working_directory(item_id, workspace_id, cwd)
1455                    .await
1456            }))
1457        } else {
1458            None
1459        }
1460    }
1461
1462    fn should_serialize(&self, _: &Self::Event) -> bool {
1463        !self.cwd_serialized
1464    }
1465
1466    fn deserialize(
1467        project: Entity<Project>,
1468        workspace: WeakEntity<Workspace>,
1469        workspace_id: workspace::WorkspaceId,
1470        item_id: workspace::ItemId,
1471        window: &mut Window,
1472        cx: &mut App,
1473    ) -> Task<anyhow::Result<Entity<Self>>> {
1474        window.spawn(cx, async move |cx| {
1475            let cwd = cx
1476                .update(|_window, cx| {
1477                    let from_db = TERMINAL_DB
1478                        .get_working_directory(item_id, workspace_id)
1479                        .log_err()
1480                        .flatten();
1481                    if from_db
1482                        .as_ref()
1483                        .is_some_and(|from_db| !from_db.as_os_str().is_empty())
1484                    {
1485                        from_db
1486                    } else {
1487                        workspace
1488                            .upgrade()
1489                            .and_then(|workspace| default_working_directory(workspace.read(cx), cx))
1490                    }
1491                })
1492                .ok()
1493                .flatten();
1494
1495            let terminal = project
1496                .update(cx, |project, cx| project.create_terminal_shell(cwd, cx))?
1497                .await?;
1498            cx.update(|window, cx| {
1499                cx.new(|cx| {
1500                    TerminalView::new(
1501                        terminal,
1502                        workspace,
1503                        Some(workspace_id),
1504                        project.downgrade(),
1505                        window,
1506                        cx,
1507                    )
1508                })
1509            })
1510        })
1511    }
1512}
1513
1514impl SearchableItem for TerminalView {
1515    type Match = RangeInclusive<Point>;
1516
1517    fn supported_options(&self) -> SearchOptions {
1518        SearchOptions {
1519            case: false,
1520            word: false,
1521            regex: true,
1522            replacement: false,
1523            selection: false,
1524            find_in_results: false,
1525        }
1526    }
1527
1528    /// Clear stored matches
1529    fn clear_matches(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1530        self.terminal().update(cx, |term, _| term.matches.clear())
1531    }
1532
1533    /// Store matches returned from find_matches somewhere for rendering
1534    fn update_matches(
1535        &mut self,
1536        matches: &[Self::Match],
1537        _window: &mut Window,
1538        cx: &mut Context<Self>,
1539    ) {
1540        self.terminal()
1541            .update(cx, |term, _| term.matches = matches.to_vec())
1542    }
1543
1544    /// Returns the selection content to pre-load into this search
1545    fn query_suggestion(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> String {
1546        self.terminal()
1547            .read(cx)
1548            .last_content
1549            .selection_text
1550            .clone()
1551            .unwrap_or_default()
1552    }
1553
1554    /// Focus match at given index into the Vec of matches
1555    fn activate_match(
1556        &mut self,
1557        index: usize,
1558        _: &[Self::Match],
1559        _window: &mut Window,
1560        cx: &mut Context<Self>,
1561    ) {
1562        self.terminal()
1563            .update(cx, |term, _| term.activate_match(index));
1564        cx.notify();
1565    }
1566
1567    /// Add selections for all matches given.
1568    fn select_matches(&mut self, matches: &[Self::Match], _: &mut Window, cx: &mut Context<Self>) {
1569        self.terminal()
1570            .update(cx, |term, _| term.select_matches(matches));
1571        cx.notify();
1572    }
1573
1574    /// Get all of the matches for this query, should be done on the background
1575    fn find_matches(
1576        &mut self,
1577        query: Arc<SearchQuery>,
1578        _: &mut Window,
1579        cx: &mut Context<Self>,
1580    ) -> Task<Vec<Self::Match>> {
1581        if let Some(s) = regex_search_for_query(&query) {
1582            self.terminal()
1583                .update(cx, |term, cx| term.find_matches(s, cx))
1584        } else {
1585            Task::ready(vec![])
1586        }
1587    }
1588
1589    /// Reports back to the search toolbar what the active match should be (the selection)
1590    fn active_match_index(
1591        &mut self,
1592        direction: Direction,
1593        matches: &[Self::Match],
1594        _: &mut Window,
1595        cx: &mut Context<Self>,
1596    ) -> Option<usize> {
1597        // Selection head might have a value if there's a selection that isn't
1598        // associated with a match. Therefore, if there are no matches, we should
1599        // report None, no matter the state of the terminal
1600
1601        if !matches.is_empty() {
1602            if let Some(selection_head) = self.terminal().read(cx).selection_head {
1603                // If selection head is contained in a match. Return that match
1604                match direction {
1605                    Direction::Prev => {
1606                        // If no selection before selection head, return the first match
1607                        Some(
1608                            matches
1609                                .iter()
1610                                .enumerate()
1611                                .rev()
1612                                .find(|(_, search_match)| {
1613                                    search_match.contains(&selection_head)
1614                                        || search_match.start() < &selection_head
1615                                })
1616                                .map(|(ix, _)| ix)
1617                                .unwrap_or(0),
1618                        )
1619                    }
1620                    Direction::Next => {
1621                        // If no selection after selection head, return the last match
1622                        Some(
1623                            matches
1624                                .iter()
1625                                .enumerate()
1626                                .find(|(_, search_match)| {
1627                                    search_match.contains(&selection_head)
1628                                        || search_match.start() > &selection_head
1629                                })
1630                                .map(|(ix, _)| ix)
1631                                .unwrap_or(matches.len().saturating_sub(1)),
1632                        )
1633                    }
1634                }
1635            } else {
1636                // Matches found but no active selection, return the first last one (closest to cursor)
1637                Some(matches.len().saturating_sub(1))
1638            }
1639        } else {
1640            None
1641        }
1642    }
1643    fn replace(
1644        &mut self,
1645        _: &Self::Match,
1646        _: &SearchQuery,
1647        _window: &mut Window,
1648        _: &mut Context<Self>,
1649    ) {
1650        // Replacement is not supported in terminal view, so this is a no-op.
1651    }
1652}
1653
1654///Gets the working directory for the given workspace, respecting the user's settings.
1655/// None implies "~" on whichever machine we end up on.
1656pub(crate) fn default_working_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
1657    match &TerminalSettings::get_global(cx).working_directory {
1658        WorkingDirectory::CurrentProjectDirectory => workspace
1659            .project()
1660            .read(cx)
1661            .active_project_directory(cx)
1662            .as_deref()
1663            .map(Path::to_path_buf),
1664        WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
1665        WorkingDirectory::AlwaysHome => None,
1666        WorkingDirectory::Always { directory } => {
1667            shellexpand::full(&directory) //TODO handle this better
1668                .ok()
1669                .map(|dir| Path::new(&dir.to_string()).to_path_buf())
1670                .filter(|dir| dir.is_dir())
1671        }
1672    }
1673}
1674///Gets the first project's home directory, or the home directory
1675fn first_project_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
1676    let worktree = workspace.worktrees(cx).next()?.read(cx);
1677    if !worktree.root_entry()?.is_dir() {
1678        return None;
1679    }
1680    Some(worktree.abs_path().to_path_buf())
1681}
1682
1683#[cfg(test)]
1684mod tests {
1685    use super::*;
1686    use gpui::TestAppContext;
1687    use project::{Entry, Project, ProjectPath, Worktree};
1688    use std::path::Path;
1689    use workspace::AppState;
1690
1691    // Working directory calculation tests
1692
1693    // No Worktrees in project -> home_dir()
1694    #[gpui::test]
1695    async fn no_worktree(cx: &mut TestAppContext) {
1696        let (project, workspace) = init_test(cx).await;
1697        cx.read(|cx| {
1698            let workspace = workspace.read(cx);
1699            let active_entry = project.read(cx).active_entry();
1700
1701            //Make sure environment is as expected
1702            assert!(active_entry.is_none());
1703            assert!(workspace.worktrees(cx).next().is_none());
1704
1705            let res = default_working_directory(workspace, cx);
1706            assert_eq!(res, None);
1707            let res = first_project_directory(workspace, cx);
1708            assert_eq!(res, None);
1709        });
1710    }
1711
1712    // No active entry, but a worktree, worktree is a file -> home_dir()
1713    #[gpui::test]
1714    async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
1715        let (project, workspace) = init_test(cx).await;
1716
1717        create_file_wt(project.clone(), "/root.txt", cx).await;
1718        cx.read(|cx| {
1719            let workspace = workspace.read(cx);
1720            let active_entry = project.read(cx).active_entry();
1721
1722            //Make sure environment is as expected
1723            assert!(active_entry.is_none());
1724            assert!(workspace.worktrees(cx).next().is_some());
1725
1726            let res = default_working_directory(workspace, cx);
1727            assert_eq!(res, None);
1728            let res = first_project_directory(workspace, cx);
1729            assert_eq!(res, None);
1730        });
1731    }
1732
1733    // No active entry, but a worktree, worktree is a folder -> worktree_folder
1734    #[gpui::test]
1735    async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
1736        let (project, workspace) = init_test(cx).await;
1737
1738        let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
1739        cx.update(|cx| {
1740            let workspace = workspace.read(cx);
1741            let active_entry = project.read(cx).active_entry();
1742
1743            assert!(active_entry.is_none());
1744            assert!(workspace.worktrees(cx).next().is_some());
1745
1746            let res = default_working_directory(workspace, cx);
1747            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
1748            let res = first_project_directory(workspace, cx);
1749            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
1750        });
1751    }
1752
1753    // Active entry with a work tree, worktree is a file -> worktree_folder()
1754    #[gpui::test]
1755    async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
1756        let (project, workspace) = init_test(cx).await;
1757
1758        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
1759        let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
1760        insert_active_entry_for(wt2, entry2, project.clone(), cx);
1761
1762        cx.update(|cx| {
1763            let workspace = workspace.read(cx);
1764            let active_entry = project.read(cx).active_entry();
1765
1766            assert!(active_entry.is_some());
1767
1768            let res = default_working_directory(workspace, cx);
1769            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
1770            let res = first_project_directory(workspace, cx);
1771            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
1772        });
1773    }
1774
1775    // Active entry, with a worktree, worktree is a folder -> worktree_folder
1776    #[gpui::test]
1777    async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
1778        let (project, workspace) = init_test(cx).await;
1779
1780        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
1781        let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
1782        insert_active_entry_for(wt2, entry2, project.clone(), cx);
1783
1784        cx.update(|cx| {
1785            let workspace = workspace.read(cx);
1786            let active_entry = project.read(cx).active_entry();
1787
1788            assert!(active_entry.is_some());
1789
1790            let res = default_working_directory(workspace, cx);
1791            assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
1792            let res = first_project_directory(workspace, cx);
1793            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
1794        });
1795    }
1796
1797    /// Creates a worktree with 1 file: /root.txt
1798    pub async fn init_test(cx: &mut TestAppContext) -> (Entity<Project>, Entity<Workspace>) {
1799        let params = cx.update(AppState::test);
1800        cx.update(|cx| {
1801            terminal::init(cx);
1802            theme::init(theme::LoadThemes::JustBase, cx);
1803            Project::init_settings(cx);
1804            language::init(cx);
1805        });
1806
1807        let project = Project::test(params.fs.clone(), [], cx).await;
1808        let workspace = cx
1809            .add_window(|window, cx| Workspace::test_new(project.clone(), window, cx))
1810            .root(cx)
1811            .unwrap();
1812
1813        (project, workspace)
1814    }
1815
1816    /// Creates a worktree with 1 folder: /root{suffix}/
1817    async fn create_folder_wt(
1818        project: Entity<Project>,
1819        path: impl AsRef<Path>,
1820        cx: &mut TestAppContext,
1821    ) -> (Entity<Worktree>, Entry) {
1822        create_wt(project, true, path, cx).await
1823    }
1824
1825    /// Creates a worktree with 1 file: /root{suffix}.txt
1826    async fn create_file_wt(
1827        project: Entity<Project>,
1828        path: impl AsRef<Path>,
1829        cx: &mut TestAppContext,
1830    ) -> (Entity<Worktree>, Entry) {
1831        create_wt(project, false, path, cx).await
1832    }
1833
1834    async fn create_wt(
1835        project: Entity<Project>,
1836        is_dir: bool,
1837        path: impl AsRef<Path>,
1838        cx: &mut TestAppContext,
1839    ) -> (Entity<Worktree>, Entry) {
1840        let (wt, _) = project
1841            .update(cx, |project, cx| {
1842                project.find_or_create_worktree(path, true, cx)
1843            })
1844            .await
1845            .unwrap();
1846
1847        let entry = cx
1848            .update(|cx| {
1849                wt.update(cx, |wt, cx| {
1850                    wt.create_entry(Path::new(""), is_dir, None, cx)
1851                })
1852            })
1853            .await
1854            .unwrap()
1855            .into_included()
1856            .unwrap();
1857
1858        (wt, entry)
1859    }
1860
1861    pub fn insert_active_entry_for(
1862        wt: Entity<Worktree>,
1863        entry: Entry,
1864        project: Entity<Project>,
1865        cx: &mut TestAppContext,
1866    ) {
1867        cx.update(|cx| {
1868            let p = ProjectPath {
1869                worktree_id: wt.read(cx).id(),
1870                path: entry.path,
1871            };
1872            project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
1873        });
1874    }
1875}