terminal_view.rs

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