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