terminal_view.rs

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