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, search::RegexSearch},
  29    },
  30    terminal_settings::{self, CursorShape, TerminalBlink, TerminalSettings, WorkingDirectory},
  31};
  32use terminal_element::{TerminalElement, is_blank};
  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);
  67
  68const GIT_DIFF_PATH_PREFIXES: &[&str] = &["a", "b"];
  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                        if let Some(max_lines) = max_lines_when_unfocused {
 313                            displayed_lines = displayed_lines.min(*max_lines)
 314                        }
 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            .map_or(false, |terminal_panel| {
 389                terminal_panel.read(cx).assistant_enabled()
 390            });
 391        let context_menu = ContextMenu::build(window, cx, |menu, _, _| {
 392            menu.context(self.focus_handle.clone())
 393                .action("New Terminal", Box::new(NewTerminal))
 394                .separator()
 395                .action("Copy", Box::new(Copy))
 396                .action("Paste", Box::new(Paste))
 397                .action("Select All", Box::new(SelectAll))
 398                .action("Clear", Box::new(Clear))
 399                .when(assistant_enabled, |menu| {
 400                    menu.separator()
 401                        .action("Inline Assist", Box::new(InlineAssist::default()))
 402                })
 403                .separator()
 404                .action(
 405                    "Close Terminal Tab",
 406                    Box::new(CloseActiveItem {
 407                        save_intent: None,
 408                        close_pinned: true,
 409                    }),
 410                )
 411        });
 412
 413        window.focus(&context_menu.focus_handle(cx));
 414        let subscription = cx.subscribe_in(
 415            &context_menu,
 416            window,
 417            |this, _, _: &DismissEvent, window, cx| {
 418                if this.context_menu.as_ref().is_some_and(|context_menu| {
 419                    context_menu.0.focus_handle(cx).contains_focused(window, cx)
 420                }) {
 421                    cx.focus_self(window);
 422                }
 423                this.context_menu.take();
 424                cx.notify();
 425            },
 426        );
 427
 428        self.context_menu = Some((context_menu, position, subscription));
 429    }
 430
 431    fn settings_changed(&mut self, cx: &mut Context<Self>) {
 432        let settings = TerminalSettings::get_global(cx);
 433        self.show_breadcrumbs = settings.toolbar.breadcrumbs;
 434
 435        let new_cursor_shape = settings.cursor_shape.unwrap_or_default();
 436        let old_cursor_shape = self.cursor_shape;
 437        if old_cursor_shape != new_cursor_shape {
 438            self.cursor_shape = new_cursor_shape;
 439            self.terminal.update(cx, |term, _| {
 440                term.set_cursor_shape(self.cursor_shape);
 441            });
 442        }
 443
 444        cx.notify();
 445    }
 446
 447    fn show_character_palette(
 448        &mut self,
 449        _: &ShowCharacterPalette,
 450        window: &mut Window,
 451        cx: &mut Context<Self>,
 452    ) {
 453        if self
 454            .terminal
 455            .read(cx)
 456            .last_content
 457            .mode
 458            .contains(TermMode::ALT_SCREEN)
 459        {
 460            self.terminal.update(cx, |term, cx| {
 461                term.try_keystroke(
 462                    &Keystroke::parse("ctrl-cmd-space").unwrap(),
 463                    TerminalSettings::get_global(cx).option_as_meta,
 464                )
 465            });
 466        } else {
 467            window.show_character_palette();
 468        }
 469    }
 470
 471    fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {
 472        self.terminal.update(cx, |term, _| term.select_all());
 473        cx.notify();
 474    }
 475
 476    fn rerun_task(&mut self, _: &RerunTask, window: &mut Window, cx: &mut Context<Self>) {
 477        let task = self
 478            .terminal
 479            .read(cx)
 480            .task()
 481            .map(|task| terminal_rerun_override(&task.id))
 482            .unwrap_or_default();
 483        window.dispatch_action(Box::new(task), cx);
 484    }
 485
 486    fn clear(&mut self, _: &Clear, _: &mut Window, cx: &mut Context<Self>) {
 487        self.scroll_top = px(0.);
 488        self.terminal.update(cx, |term, _| term.clear());
 489        cx.notify();
 490    }
 491
 492    fn max_scroll_top(&self, cx: &App) -> Pixels {
 493        let terminal = self.terminal.read(cx);
 494
 495        let Some(block) = self.block_below_cursor.as_ref() else {
 496            return Pixels::ZERO;
 497        };
 498
 499        let line_height = terminal.last_content().terminal_bounds.line_height;
 500        let mut terminal_lines = terminal.total_lines();
 501        let viewport_lines = terminal.viewport_lines();
 502        if terminal.total_lines() == terminal.viewport_lines() {
 503            let mut last_line = None;
 504            for cell in terminal.last_content.cells.iter().rev() {
 505                if !is_blank(cell) {
 506                    break;
 507                }
 508
 509                let last_line = last_line.get_or_insert(cell.point.line);
 510                if *last_line != cell.point.line {
 511                    terminal_lines -= 1;
 512                }
 513                *last_line = cell.point.line;
 514            }
 515        }
 516
 517        let max_scroll_top_in_lines =
 518            (block.height as usize).saturating_sub(viewport_lines.saturating_sub(terminal_lines));
 519
 520        max_scroll_top_in_lines as f32 * line_height
 521    }
 522
 523    fn scroll_wheel(&mut self, event: &ScrollWheelEvent, cx: &mut Context<Self>) {
 524        let terminal_content = self.terminal.read(cx).last_content();
 525
 526        if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
 527            let line_height = terminal_content.terminal_bounds.line_height;
 528            let y_delta = event.delta.pixel_delta(line_height).y;
 529            if y_delta < Pixels::ZERO || self.scroll_top > Pixels::ZERO {
 530                self.scroll_top = cmp::max(
 531                    Pixels::ZERO,
 532                    cmp::min(self.scroll_top - y_delta, self.max_scroll_top(cx)),
 533                );
 534                cx.notify();
 535                return;
 536            }
 537        }
 538        self.terminal.update(cx, |term, _| term.scroll_wheel(event));
 539    }
 540
 541    fn scroll_line_up(&mut self, _: &ScrollLineUp, _: &mut Window, cx: &mut Context<Self>) {
 542        let terminal_content = self.terminal.read(cx).last_content();
 543        if self.block_below_cursor.is_some()
 544            && terminal_content.display_offset == 0
 545            && self.scroll_top > Pixels::ZERO
 546        {
 547            let line_height = terminal_content.terminal_bounds.line_height;
 548            self.scroll_top = cmp::max(self.scroll_top - line_height, Pixels::ZERO);
 549            return;
 550        }
 551
 552        self.terminal.update(cx, |term, _| term.scroll_line_up());
 553        cx.notify();
 554    }
 555
 556    fn scroll_line_down(&mut self, _: &ScrollLineDown, _: &mut Window, cx: &mut Context<Self>) {
 557        let terminal_content = self.terminal.read(cx).last_content();
 558        if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
 559            let max_scroll_top = self.max_scroll_top(cx);
 560            if self.scroll_top < max_scroll_top {
 561                let line_height = terminal_content.terminal_bounds.line_height;
 562                self.scroll_top = cmp::min(self.scroll_top + line_height, max_scroll_top);
 563            }
 564            return;
 565        }
 566
 567        self.terminal.update(cx, |term, _| term.scroll_line_down());
 568        cx.notify();
 569    }
 570
 571    fn scroll_page_up(&mut self, _: &ScrollPageUp, _: &mut Window, cx: &mut Context<Self>) {
 572        if self.scroll_top == Pixels::ZERO {
 573            self.terminal.update(cx, |term, _| term.scroll_page_up());
 574        } else {
 575            let line_height = self
 576                .terminal
 577                .read(cx)
 578                .last_content
 579                .terminal_bounds
 580                .line_height();
 581            let visible_block_lines = (self.scroll_top / line_height) as usize;
 582            let viewport_lines = self.terminal.read(cx).viewport_lines();
 583            let visible_content_lines = viewport_lines - visible_block_lines;
 584
 585            if visible_block_lines >= viewport_lines {
 586                self.scroll_top = ((visible_block_lines - viewport_lines) as f32) * line_height;
 587            } else {
 588                self.scroll_top = px(0.);
 589                self.terminal
 590                    .update(cx, |term, _| term.scroll_up_by(visible_content_lines));
 591            }
 592        }
 593        cx.notify();
 594    }
 595
 596    fn scroll_page_down(&mut self, _: &ScrollPageDown, _: &mut Window, cx: &mut Context<Self>) {
 597        self.terminal.update(cx, |term, _| term.scroll_page_down());
 598        let terminal = self.terminal.read(cx);
 599        if terminal.last_content().display_offset < terminal.viewport_lines() {
 600            self.scroll_top = self.max_scroll_top(cx);
 601        }
 602        cx.notify();
 603    }
 604
 605    fn scroll_to_top(&mut self, _: &ScrollToTop, _: &mut Window, cx: &mut Context<Self>) {
 606        self.terminal.update(cx, |term, _| term.scroll_to_top());
 607        cx.notify();
 608    }
 609
 610    fn scroll_to_bottom(&mut self, _: &ScrollToBottom, _: &mut Window, cx: &mut Context<Self>) {
 611        self.terminal.update(cx, |term, _| term.scroll_to_bottom());
 612        if self.block_below_cursor.is_some() {
 613            self.scroll_top = self.max_scroll_top(cx);
 614        }
 615        cx.notify();
 616    }
 617
 618    fn toggle_vi_mode(&mut self, _: &ToggleViMode, _: &mut Window, cx: &mut Context<Self>) {
 619        self.terminal.update(cx, |term, _| term.toggle_vi_mode());
 620        cx.notify();
 621    }
 622
 623    pub fn should_show_cursor(&self, focused: bool, cx: &mut Context<Self>) -> bool {
 624        //Don't blink the cursor when not focused, blinking is disabled, or paused
 625        if !focused
 626            || self.blinking_paused
 627            || self
 628                .terminal
 629                .read(cx)
 630                .last_content
 631                .mode
 632                .contains(TermMode::ALT_SCREEN)
 633        {
 634            return true;
 635        }
 636
 637        match TerminalSettings::get_global(cx).blinking {
 638            //If the user requested to never blink, don't blink it.
 639            TerminalBlink::Off => true,
 640            //If the terminal is controlling it, check terminal mode
 641            TerminalBlink::TerminalControlled => {
 642                !self.blinking_terminal_enabled || self.blink_state
 643            }
 644            TerminalBlink::On => self.blink_state,
 645        }
 646    }
 647
 648    fn blink_cursors(&mut self, epoch: usize, window: &mut Window, cx: &mut Context<Self>) {
 649        if epoch == self.blink_epoch && !self.blinking_paused {
 650            self.blink_state = !self.blink_state;
 651            cx.notify();
 652
 653            let epoch = self.next_blink_epoch();
 654            cx.spawn_in(window, async move |this, cx| {
 655                Timer::after(CURSOR_BLINK_INTERVAL).await;
 656                this.update_in(cx, |this, window, cx| this.blink_cursors(epoch, window, cx))
 657                    .ok();
 658            })
 659            .detach();
 660        }
 661    }
 662
 663    pub fn pause_cursor_blinking(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 664        self.blink_state = true;
 665        cx.notify();
 666
 667        let epoch = self.next_blink_epoch();
 668        cx.spawn_in(window, async move |this, cx| {
 669            Timer::after(CURSOR_BLINK_INTERVAL).await;
 670            this.update_in(cx, |this, window, cx| {
 671                this.resume_cursor_blinking(epoch, window, cx)
 672            })
 673            .ok();
 674        })
 675        .detach();
 676    }
 677
 678    pub fn terminal(&self) -> &Entity<Terminal> {
 679        &self.terminal
 680    }
 681
 682    pub fn set_block_below_cursor(
 683        &mut self,
 684        block: BlockProperties,
 685        window: &mut Window,
 686        cx: &mut Context<Self>,
 687    ) {
 688        self.block_below_cursor = Some(Rc::new(block));
 689        self.scroll_to_bottom(&ScrollToBottom, window, cx);
 690        cx.notify();
 691    }
 692
 693    pub fn clear_block_below_cursor(&mut self, cx: &mut Context<Self>) {
 694        self.block_below_cursor = None;
 695        self.scroll_top = Pixels::ZERO;
 696        cx.notify();
 697    }
 698
 699    fn next_blink_epoch(&mut self) -> usize {
 700        self.blink_epoch += 1;
 701        self.blink_epoch
 702    }
 703
 704    fn resume_cursor_blinking(
 705        &mut self,
 706        epoch: usize,
 707        window: &mut Window,
 708        cx: &mut Context<Self>,
 709    ) {
 710        if epoch == self.blink_epoch {
 711            self.blinking_paused = false;
 712            self.blink_cursors(epoch, window, cx);
 713        }
 714    }
 715
 716    ///Attempt to paste the clipboard into the terminal
 717    fn copy(&mut self, _: &Copy, _: &mut Window, cx: &mut Context<Self>) {
 718        self.terminal.update(cx, |term, _| term.copy());
 719        cx.notify();
 720    }
 721
 722    ///Attempt to paste the clipboard into the terminal
 723    fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
 724        if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) {
 725            self.terminal
 726                .update(cx, |terminal, _cx| terminal.paste(&clipboard_string));
 727        }
 728    }
 729
 730    fn send_text(&mut self, text: &SendText, _: &mut Window, cx: &mut Context<Self>) {
 731        self.clear_bell(cx);
 732        self.terminal.update(cx, |term, _| {
 733            term.input(text.0.to_string().into_bytes());
 734        });
 735    }
 736
 737    fn send_keystroke(&mut self, text: &SendKeystroke, _: &mut Window, cx: &mut Context<Self>) {
 738        if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
 739            self.clear_bell(cx);
 740            self.terminal.update(cx, |term, cx| {
 741                let processed =
 742                    term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta);
 743                if processed && term.vi_mode_enabled() {
 744                    cx.notify();
 745                }
 746                processed
 747            });
 748        }
 749    }
 750
 751    fn dispatch_context(&self, cx: &App) -> KeyContext {
 752        let mut dispatch_context = KeyContext::new_with_defaults();
 753        dispatch_context.add("Terminal");
 754
 755        if self.terminal.read(cx).vi_mode_enabled() {
 756            dispatch_context.add("vi_mode");
 757        }
 758
 759        let mode = self.terminal.read(cx).last_content.mode;
 760        dispatch_context.set(
 761            "screen",
 762            if mode.contains(TermMode::ALT_SCREEN) {
 763                "alt"
 764            } else {
 765                "normal"
 766            },
 767        );
 768
 769        if mode.contains(TermMode::APP_CURSOR) {
 770            dispatch_context.add("DECCKM");
 771        }
 772        if mode.contains(TermMode::APP_KEYPAD) {
 773            dispatch_context.add("DECPAM");
 774        } else {
 775            dispatch_context.add("DECPNM");
 776        }
 777        if mode.contains(TermMode::SHOW_CURSOR) {
 778            dispatch_context.add("DECTCEM");
 779        }
 780        if mode.contains(TermMode::LINE_WRAP) {
 781            dispatch_context.add("DECAWM");
 782        }
 783        if mode.contains(TermMode::ORIGIN) {
 784            dispatch_context.add("DECOM");
 785        }
 786        if mode.contains(TermMode::INSERT) {
 787            dispatch_context.add("IRM");
 788        }
 789        //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
 790        if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
 791            dispatch_context.add("LNM");
 792        }
 793        if mode.contains(TermMode::FOCUS_IN_OUT) {
 794            dispatch_context.add("report_focus");
 795        }
 796        if mode.contains(TermMode::ALTERNATE_SCROLL) {
 797            dispatch_context.add("alternate_scroll");
 798        }
 799        if mode.contains(TermMode::BRACKETED_PASTE) {
 800            dispatch_context.add("bracketed_paste");
 801        }
 802        if mode.intersects(TermMode::MOUSE_MODE) {
 803            dispatch_context.add("any_mouse_reporting");
 804        }
 805        {
 806            let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
 807                "click"
 808            } else if mode.contains(TermMode::MOUSE_DRAG) {
 809                "drag"
 810            } else if mode.contains(TermMode::MOUSE_MOTION) {
 811                "motion"
 812            } else {
 813                "off"
 814            };
 815            dispatch_context.set("mouse_reporting", mouse_reporting);
 816        }
 817        {
 818            let format = if mode.contains(TermMode::SGR_MOUSE) {
 819                "sgr"
 820            } else if mode.contains(TermMode::UTF8_MOUSE) {
 821                "utf8"
 822            } else {
 823                "normal"
 824            };
 825            dispatch_context.set("mouse_format", format);
 826        };
 827
 828        if self.terminal.read(cx).last_content.selection.is_some() {
 829            dispatch_context.add("selection");
 830        }
 831
 832        dispatch_context
 833    }
 834
 835    fn set_terminal(
 836        &mut self,
 837        terminal: Entity<Terminal>,
 838        window: &mut Window,
 839        cx: &mut Context<TerminalView>,
 840    ) {
 841        self._terminal_subscriptions =
 842            subscribe_for_terminal_events(&terminal, self.workspace.clone(), window, cx);
 843        self.terminal = terminal;
 844    }
 845
 846    // Hack: Using editor in terminal causes cyclic dependency i.e. editor -> terminal -> project -> editor.
 847    fn map_show_scrollbar_from_editor_to_terminal(
 848        show_scrollbar: editor::ShowScrollbar,
 849    ) -> terminal_settings::ShowScrollbar {
 850        match show_scrollbar {
 851            editor::ShowScrollbar::Auto => terminal_settings::ShowScrollbar::Auto,
 852            editor::ShowScrollbar::System => terminal_settings::ShowScrollbar::System,
 853            editor::ShowScrollbar::Always => terminal_settings::ShowScrollbar::Always,
 854            editor::ShowScrollbar::Never => terminal_settings::ShowScrollbar::Never,
 855        }
 856    }
 857
 858    fn should_show_scrollbar(cx: &App) -> bool {
 859        let show = TerminalSettings::get_global(cx)
 860            .scrollbar
 861            .show
 862            .unwrap_or_else(|| {
 863                Self::map_show_scrollbar_from_editor_to_terminal(
 864                    EditorSettings::get_global(cx).scrollbar.show,
 865                )
 866            });
 867        match show {
 868            terminal_settings::ShowScrollbar::Auto => true,
 869            terminal_settings::ShowScrollbar::System => true,
 870            terminal_settings::ShowScrollbar::Always => true,
 871            terminal_settings::ShowScrollbar::Never => false,
 872        }
 873    }
 874
 875    fn should_autohide_scrollbar(cx: &App) -> bool {
 876        let show = TerminalSettings::get_global(cx)
 877            .scrollbar
 878            .show
 879            .unwrap_or_else(|| {
 880                Self::map_show_scrollbar_from_editor_to_terminal(
 881                    EditorSettings::get_global(cx).scrollbar.show,
 882                )
 883            });
 884        match show {
 885            terminal_settings::ShowScrollbar::Auto => true,
 886            terminal_settings::ShowScrollbar::System => cx
 887                .try_global::<ScrollbarAutoHide>()
 888                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
 889            terminal_settings::ShowScrollbar::Always => false,
 890            terminal_settings::ShowScrollbar::Never => true,
 891        }
 892    }
 893
 894    fn hide_scrollbar(&mut self, cx: &mut Context<Self>) {
 895        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 896        if !Self::should_autohide_scrollbar(cx) {
 897            return;
 898        }
 899        self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| {
 900            cx.background_executor()
 901                .timer(SCROLLBAR_SHOW_INTERVAL)
 902                .await;
 903            panel
 904                .update(cx, |panel, cx| {
 905                    panel.show_scrollbar = false;
 906                    cx.notify();
 907                })
 908                .log_err();
 909        }))
 910    }
 911
 912    fn render_scrollbar(&self, window: &Window, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
 913        if !Self::should_show_scrollbar(cx)
 914            || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
 915            || !self.content_mode(window, cx).is_scrollable()
 916        {
 917            return None;
 918        }
 919
 920        if self.terminal.read(cx).total_lines() == self.terminal.read(cx).viewport_lines() {
 921            return None;
 922        }
 923
 924        self.scroll_handle.update(self.terminal.read(cx));
 925
 926        if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() {
 927            self.terminal.update(cx, |term, _| {
 928                let delta = new_display_offset as i32 - term.last_content.display_offset as i32;
 929                match delta.cmp(&0) {
 930                    std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize),
 931                    std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize),
 932                    std::cmp::Ordering::Equal => {}
 933                }
 934            });
 935        }
 936
 937        Some(
 938            div()
 939                .occlude()
 940                .id("terminal-view-scroll")
 941                .on_mouse_move(cx.listener(|_, _, _window, cx| {
 942                    cx.notify();
 943                    cx.stop_propagation()
 944                }))
 945                .on_hover(|_, _window, cx| {
 946                    cx.stop_propagation();
 947                })
 948                .on_any_mouse_down(|_, _window, cx| {
 949                    cx.stop_propagation();
 950                })
 951                .on_mouse_up(
 952                    MouseButton::Left,
 953                    cx.listener(|terminal_view, _, window, cx| {
 954                        if !terminal_view.scrollbar_state.is_dragging()
 955                            && !terminal_view.focus_handle.contains_focused(window, cx)
 956                        {
 957                            terminal_view.hide_scrollbar(cx);
 958                            cx.notify();
 959                        }
 960                        cx.stop_propagation();
 961                    }),
 962                )
 963                .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
 964                    cx.notify();
 965                }))
 966                .h_full()
 967                .absolute()
 968                .right_1()
 969                .top_1()
 970                .bottom_0()
 971                .w(px(12.))
 972                .cursor_default()
 973                .children(Scrollbar::vertical(self.scrollbar_state.clone())),
 974        )
 975    }
 976
 977    fn rerun_button(task: &TaskState) -> Option<IconButton> {
 978        if !task.show_rerun {
 979            return None;
 980        }
 981
 982        let task_id = task.id.clone();
 983        Some(
 984            IconButton::new("rerun-icon", IconName::Rerun)
 985                .icon_size(IconSize::Small)
 986                .size(ButtonSize::Compact)
 987                .icon_color(Color::Default)
 988                .shape(ui::IconButtonShape::Square)
 989                .tooltip(move |window, cx| {
 990                    Tooltip::for_action("Rerun task", &RerunTask, window, cx)
 991                })
 992                .on_click(move |_, window, cx| {
 993                    window.dispatch_action(Box::new(terminal_rerun_override(&task_id)), cx);
 994                }),
 995        )
 996    }
 997}
 998
 999fn terminal_rerun_override(task: &TaskId) -> zed_actions::Rerun {
1000    zed_actions::Rerun {
1001        task_id: Some(task.0.clone()),
1002        allow_concurrent_runs: Some(true),
1003        use_new_terminal: Some(false),
1004        reevaluate_context: false,
1005    }
1006}
1007
1008fn subscribe_for_terminal_events(
1009    terminal: &Entity<Terminal>,
1010    workspace: WeakEntity<Workspace>,
1011    window: &mut Window,
1012    cx: &mut Context<TerminalView>,
1013) -> Vec<Subscription> {
1014    let terminal_subscription = cx.observe(terminal, |_, _, cx| cx.notify());
1015    let mut previous_cwd = None;
1016    let terminal_events_subscription = cx.subscribe_in(
1017        terminal,
1018        window,
1019        move |terminal_view, terminal, event, window, cx| {
1020            let current_cwd = terminal.read(cx).working_directory();
1021            if current_cwd != previous_cwd {
1022                previous_cwd = current_cwd;
1023                terminal_view.cwd_serialized = false;
1024            }
1025
1026            match event {
1027                Event::Wakeup => {
1028                    cx.notify();
1029                    cx.emit(Event::Wakeup);
1030                    cx.emit(ItemEvent::UpdateTab);
1031                    cx.emit(SearchEvent::MatchesInvalidated);
1032                }
1033
1034                Event::Bell => {
1035                    terminal_view.has_bell = true;
1036                    cx.emit(Event::Wakeup);
1037                }
1038
1039                Event::BlinkChanged(blinking) => {
1040                    if matches!(
1041                        TerminalSettings::get_global(cx).blinking,
1042                        TerminalBlink::TerminalControlled
1043                    ) {
1044                        terminal_view.blinking_terminal_enabled = *blinking;
1045                    }
1046                }
1047
1048                Event::TitleChanged => {
1049                    cx.emit(ItemEvent::UpdateTab);
1050                }
1051
1052                Event::NewNavigationTarget(maybe_navigation_target) => {
1053                    match maybe_navigation_target
1054                        .as_ref()
1055                        .zip(terminal.read(cx).last_content.last_hovered_word.as_ref())
1056                    {
1057                        Some((MaybeNavigationTarget::Url(url), hovered_word)) => {
1058                            if Some(hovered_word)
1059                                != terminal_view
1060                                    .hover
1061                                    .as_ref()
1062                                    .map(|hover| &hover.hovered_word)
1063                            {
1064                                terminal_view.hover = Some(HoverTarget {
1065                                    tooltip: url.clone(),
1066                                    hovered_word: hovered_word.clone(),
1067                                });
1068                                terminal_view.hover_tooltip_update = Task::ready(());
1069                                cx.notify();
1070                            }
1071                        }
1072                        Some((MaybeNavigationTarget::PathLike(path_like_target), hovered_word)) => {
1073                            if Some(hovered_word)
1074                                != terminal_view
1075                                    .hover
1076                                    .as_ref()
1077                                    .map(|hover| &hover.hovered_word)
1078                            {
1079                                let valid_files_to_open_task = possible_open_target(
1080                                    &workspace,
1081                                    &path_like_target.terminal_dir,
1082                                    &path_like_target.maybe_path,
1083                                    cx,
1084                                );
1085                                let hovered_word = hovered_word.clone();
1086
1087                                terminal_view.hover = None;
1088                                terminal_view.hover_tooltip_update =
1089                                    cx.spawn(async move |terminal_view, cx| {
1090                                        let file_to_open = valid_files_to_open_task.await;
1091                                        terminal_view
1092                                            .update(cx, |terminal_view, _| match file_to_open {
1093                                                Some(
1094                                                    OpenTarget::File(path, _)
1095                                                    | OpenTarget::Worktree(path, _),
1096                                                ) => {
1097                                                    terminal_view.hover = Some(HoverTarget {
1098                                                        tooltip: path.to_string(|path| {
1099                                                            path.to_string_lossy().to_string()
1100                                                        }),
1101                                                        hovered_word,
1102                                                    });
1103                                                }
1104                                                None => {
1105                                                    terminal_view.hover = None;
1106                                                }
1107                                            })
1108                                            .ok();
1109                                    });
1110                                cx.notify();
1111                            }
1112                        }
1113                        None => {
1114                            terminal_view.hover = None;
1115                            terminal_view.hover_tooltip_update = Task::ready(());
1116                            cx.notify();
1117                        }
1118                    }
1119                }
1120
1121                Event::Open(maybe_navigation_target) => match maybe_navigation_target {
1122                    MaybeNavigationTarget::Url(url) => cx.open_url(url),
1123
1124                    MaybeNavigationTarget::PathLike(path_like_target) => {
1125                        if terminal_view.hover.is_none() {
1126                            return;
1127                        }
1128                        let task_workspace = workspace.clone();
1129                        let path_like_target = path_like_target.clone();
1130                        cx.spawn_in(window, async move |terminal_view, cx| {
1131                            let open_target = terminal_view
1132                                .update(cx, |_, cx| {
1133                                    possible_open_target(
1134                                        &task_workspace,
1135                                        &path_like_target.terminal_dir,
1136                                        &path_like_target.maybe_path,
1137                                        cx,
1138                                    )
1139                                })?
1140                                .await;
1141                            if let Some(open_target) = open_target {
1142                                let path_to_open = open_target.path();
1143                                let opened_items = task_workspace
1144                                    .update_in(cx, |workspace, window, cx| {
1145                                        workspace.open_paths(
1146                                            vec![path_to_open.path.clone()],
1147                                            OpenOptions {
1148                                                visible: Some(OpenVisible::OnlyDirectories),
1149                                                ..Default::default()
1150                                            },
1151                                            None,
1152                                            window,
1153                                            cx,
1154                                        )
1155                                    })
1156                                    .context("workspace update")?
1157                                    .await;
1158                                if opened_items.len() != 1 {
1159                                    debug_panic!(
1160                                        "Received {} items for one path {path_to_open:?}",
1161                                        opened_items.len(),
1162                                    );
1163                                }
1164
1165                                if let Some(opened_item) = opened_items.first() {
1166                                    if open_target.is_file() {
1167                                        if let Some(Ok(opened_item)) = opened_item {
1168                                            if let Some(row) = path_to_open.row {
1169                                                let col = path_to_open.column.unwrap_or(0);
1170                                                if let Some(active_editor) =
1171                                                    opened_item.downcast::<Editor>()
1172                                                {
1173                                                    active_editor
1174                                                        .downgrade()
1175                                                        .update_in(cx, |editor, window, cx| {
1176                                                            editor.go_to_singleton_buffer_point(
1177                                                                language::Point::new(
1178                                                                    row.saturating_sub(1),
1179                                                                    col.saturating_sub(1),
1180                                                                ),
1181                                                                window,
1182                                                                cx,
1183                                                            )
1184                                                        })
1185                                                        .log_err();
1186                                                }
1187                                            }
1188                                        }
1189                                    } else if open_target.is_dir() {
1190                                        task_workspace.update(cx, |workspace, cx| {
1191                                            workspace.project().update(cx, |_, cx| {
1192                                                cx.emit(project::Event::ActivateProjectPanel);
1193                                            })
1194                                        })?;
1195                                    }
1196                                }
1197                            }
1198
1199                            anyhow::Ok(())
1200                        })
1201                        .detach_and_log_err(cx)
1202                    }
1203                },
1204                Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
1205                Event::CloseTerminal => cx.emit(ItemEvent::CloseItem),
1206                Event::SelectionsChanged => {
1207                    window.invalidate_character_coordinates();
1208                    cx.emit(SearchEvent::ActiveMatchChanged)
1209                }
1210            }
1211        },
1212    );
1213    vec![terminal_subscription, terminal_events_subscription]
1214}
1215
1216#[derive(Debug, Clone)]
1217enum OpenTarget {
1218    Worktree(PathWithPosition, Entry),
1219    File(PathWithPosition, Metadata),
1220}
1221
1222impl OpenTarget {
1223    fn is_file(&self) -> bool {
1224        match self {
1225            OpenTarget::Worktree(_, entry) => entry.is_file(),
1226            OpenTarget::File(_, metadata) => !metadata.is_dir,
1227        }
1228    }
1229
1230    fn is_dir(&self) -> bool {
1231        match self {
1232            OpenTarget::Worktree(_, entry) => entry.is_dir(),
1233            OpenTarget::File(_, metadata) => metadata.is_dir,
1234        }
1235    }
1236
1237    fn path(&self) -> &PathWithPosition {
1238        match self {
1239            OpenTarget::Worktree(path, _) => path,
1240            OpenTarget::File(path, _) => path,
1241        }
1242    }
1243}
1244
1245fn possible_open_target(
1246    workspace: &WeakEntity<Workspace>,
1247    cwd: &Option<PathBuf>,
1248    maybe_path: &str,
1249    cx: &App,
1250) -> Task<Option<OpenTarget>> {
1251    let Some(workspace) = workspace.upgrade() else {
1252        return Task::ready(None);
1253    };
1254    // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too.
1255    // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away.
1256    let mut potential_paths = Vec::new();
1257    let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path));
1258    let path_with_position = PathWithPosition::parse_str(maybe_path);
1259    let worktree_candidates = workspace
1260        .read(cx)
1261        .worktrees(cx)
1262        .sorted_by_key(|worktree| {
1263            let worktree_root = worktree.read(cx).abs_path();
1264            match cwd
1265                .as_ref()
1266                .and_then(|cwd| worktree_root.strip_prefix(cwd).ok())
1267            {
1268                Some(cwd_child) => cwd_child.components().count(),
1269                None => usize::MAX,
1270            }
1271        })
1272        .collect::<Vec<_>>();
1273    // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it.
1274    for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) {
1275        if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() {
1276            potential_paths.push(PathWithPosition {
1277                path: stripped.to_owned(),
1278                row: original_path.row,
1279                column: original_path.column,
1280            });
1281        }
1282        if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() {
1283            potential_paths.push(PathWithPosition {
1284                path: stripped.to_owned(),
1285                row: path_with_position.row,
1286                column: path_with_position.column,
1287            });
1288        }
1289    }
1290
1291    let insert_both_paths = original_path != path_with_position;
1292    potential_paths.insert(0, original_path);
1293    if insert_both_paths {
1294        potential_paths.insert(1, path_with_position);
1295    }
1296
1297    // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix.
1298    // That will be slow, though, so do the fast checks first.
1299    let mut worktree_paths_to_check = Vec::new();
1300    for worktree in &worktree_candidates {
1301        let worktree_root = worktree.read(cx).abs_path();
1302        let mut paths_to_check = Vec::with_capacity(potential_paths.len());
1303
1304        for path_with_position in &potential_paths {
1305            let path_to_check = if worktree_root.ends_with(&path_with_position.path) {
1306                let root_path_with_position = PathWithPosition {
1307                    path: worktree_root.to_path_buf(),
1308                    row: path_with_position.row,
1309                    column: path_with_position.column,
1310                };
1311                match worktree.read(cx).root_entry() {
1312                    Some(root_entry) => {
1313                        return Task::ready(Some(OpenTarget::Worktree(
1314                            root_path_with_position,
1315                            root_entry.clone(),
1316                        )));
1317                    }
1318                    None => root_path_with_position,
1319                }
1320            } else {
1321                PathWithPosition {
1322                    path: path_with_position
1323                        .path
1324                        .strip_prefix(&worktree_root)
1325                        .unwrap_or(&path_with_position.path)
1326                        .to_owned(),
1327                    row: path_with_position.row,
1328                    column: path_with_position.column,
1329                }
1330            };
1331
1332            if path_to_check.path.is_relative() {
1333                if let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) {
1334                    return Task::ready(Some(OpenTarget::Worktree(
1335                        PathWithPosition {
1336                            path: worktree_root.join(&entry.path),
1337                            row: path_to_check.row,
1338                            column: path_to_check.column,
1339                        },
1340                        entry.clone(),
1341                    )));
1342                }
1343            }
1344
1345            paths_to_check.push(path_to_check);
1346        }
1347
1348        if !paths_to_check.is_empty() {
1349            worktree_paths_to_check.push((worktree.clone(), paths_to_check));
1350        }
1351    }
1352
1353    // Before entire worktree traversal(s), make an attempt to do FS checks if available.
1354    let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() {
1355        potential_paths
1356            .into_iter()
1357            .flat_map(|path_to_check| {
1358                let mut paths_to_check = Vec::new();
1359                let maybe_path = &path_to_check.path;
1360                if maybe_path.starts_with("~") {
1361                    if let Some(home_path) =
1362                        maybe_path
1363                            .strip_prefix("~")
1364                            .ok()
1365                            .and_then(|stripped_maybe_path| {
1366                                Some(dirs::home_dir()?.join(stripped_maybe_path))
1367                            })
1368                    {
1369                        paths_to_check.push(PathWithPosition {
1370                            path: home_path,
1371                            row: path_to_check.row,
1372                            column: path_to_check.column,
1373                        });
1374                    }
1375                } else {
1376                    paths_to_check.push(PathWithPosition {
1377                        path: maybe_path.clone(),
1378                        row: path_to_check.row,
1379                        column: path_to_check.column,
1380                    });
1381                    if maybe_path.is_relative() {
1382                        if let Some(cwd) = &cwd {
1383                            paths_to_check.push(PathWithPosition {
1384                                path: cwd.join(maybe_path),
1385                                row: path_to_check.row,
1386                                column: path_to_check.column,
1387                            });
1388                        }
1389                        for worktree in &worktree_candidates {
1390                            paths_to_check.push(PathWithPosition {
1391                                path: worktree.read(cx).abs_path().join(maybe_path),
1392                                row: path_to_check.row,
1393                                column: path_to_check.column,
1394                            });
1395                        }
1396                    }
1397                }
1398                paths_to_check
1399            })
1400            .collect()
1401    } else {
1402        Vec::new()
1403    };
1404
1405    let worktree_check_task = cx.spawn(async move |cx| {
1406        for (worktree, worktree_paths_to_check) in worktree_paths_to_check {
1407            let found_entry = worktree
1408                .update(cx, |worktree, _| {
1409                    let worktree_root = worktree.abs_path();
1410                    let mut traversal = worktree.traverse_from_path(true, true, false, "".as_ref());
1411                    while let Some(entry) = traversal.next() {
1412                        if let Some(path_in_worktree) = worktree_paths_to_check
1413                            .iter()
1414                            .find(|path_to_check| entry.path.ends_with(&path_to_check.path))
1415                        {
1416                            return Some(OpenTarget::Worktree(
1417                                PathWithPosition {
1418                                    path: worktree_root.join(&entry.path),
1419                                    row: path_in_worktree.row,
1420                                    column: path_in_worktree.column,
1421                                },
1422                                entry.clone(),
1423                            ));
1424                        }
1425                    }
1426                    None
1427                })
1428                .ok()?;
1429            if let Some(found_entry) = found_entry {
1430                return Some(found_entry);
1431            }
1432        }
1433        None
1434    });
1435
1436    let fs = workspace.read(cx).project().read(cx).fs().clone();
1437    cx.background_spawn(async move {
1438        for mut path_to_check in fs_paths_to_check {
1439            if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() {
1440                if let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() {
1441                    path_to_check.path = fs_path_to_check;
1442                    return Some(OpenTarget::File(path_to_check, metadata));
1443                }
1444            }
1445        }
1446
1447        worktree_check_task.await
1448    })
1449}
1450
1451fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
1452    let str = query.as_str();
1453    if query.is_regex() {
1454        if str == "." {
1455            return None;
1456        }
1457        RegexSearch::new(str).ok()
1458    } else {
1459        RegexSearch::new(&regex::escape(str)).ok()
1460    }
1461}
1462
1463impl TerminalView {
1464    fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
1465        self.clear_bell(cx);
1466        self.pause_cursor_blinking(window, cx);
1467
1468        self.terminal.update(cx, |term, cx| {
1469            let handled = term.try_keystroke(
1470                &event.keystroke,
1471                TerminalSettings::get_global(cx).option_as_meta,
1472            );
1473            if handled {
1474                cx.stop_propagation();
1475            }
1476        });
1477    }
1478
1479    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1480        self.terminal.update(cx, |terminal, _| {
1481            terminal.set_cursor_shape(self.cursor_shape);
1482            terminal.focus_in();
1483        });
1484        self.blink_cursors(self.blink_epoch, window, cx);
1485        window.invalidate_character_coordinates();
1486        cx.notify();
1487    }
1488
1489    fn focus_out(&mut self, _: &mut Window, cx: &mut Context<Self>) {
1490        self.terminal.update(cx, |terminal, _| {
1491            terminal.focus_out();
1492            terminal.set_cursor_shape(CursorShape::Hollow);
1493        });
1494        self.hide_scrollbar(cx);
1495        cx.notify();
1496    }
1497}
1498
1499impl Render for TerminalView {
1500    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1501        let terminal_handle = self.terminal.clone();
1502        let terminal_view_handle = cx.entity().clone();
1503
1504        let focused = self.focus_handle.is_focused(window);
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                    .child(TerminalElement::new(
1556                        terminal_handle,
1557                        terminal_view_handle,
1558                        self.workspace.clone(),
1559                        self.focus_handle.clone(),
1560                        focused,
1561                        self.should_show_cursor(focused, cx),
1562                        self.block_below_cursor.clone(),
1563                        self.mode.clone(),
1564                    ))
1565                    .when_some(self.render_scrollbar(window, cx), |div, scrollbar| {
1566                        div.child(scrollbar)
1567                    }),
1568            )
1569            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1570                deferred(
1571                    anchored()
1572                        .position(*position)
1573                        .anchor(gpui::Corner::TopLeft)
1574                        .child(menu.clone()),
1575                )
1576                .with_priority(1)
1577            }))
1578    }
1579}
1580
1581impl Item for TerminalView {
1582    type Event = ItemEvent;
1583
1584    fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent> {
1585        let terminal = self.terminal().read(cx);
1586        let title = terminal.title(false);
1587        let pid = terminal.pty_info.pid_getter().fallback_pid();
1588
1589        Some(TabTooltipContent::Custom(Box::new(move |_window, cx| {
1590            cx.new(|_| TerminalTooltip::new(title.clone(), pid)).into()
1591        })))
1592    }
1593
1594    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
1595        let terminal = self.terminal().read(cx);
1596        let title = terminal.title(true);
1597
1598        let (icon, icon_color, rerun_button) = match terminal.task() {
1599            Some(terminal_task) => match &terminal_task.status {
1600                TaskStatus::Running => (
1601                    IconName::Play,
1602                    Color::Disabled,
1603                    TerminalView::rerun_button(&terminal_task),
1604                ),
1605                TaskStatus::Unknown => (
1606                    IconName::Warning,
1607                    Color::Warning,
1608                    TerminalView::rerun_button(&terminal_task),
1609                ),
1610                TaskStatus::Completed { success } => {
1611                    let rerun_button = TerminalView::rerun_button(&terminal_task);
1612
1613                    if *success {
1614                        (IconName::Check, Color::Success, rerun_button)
1615                    } else {
1616                        (IconName::XCircle, Color::Error, rerun_button)
1617                    }
1618                }
1619            },
1620            None => (IconName::Terminal, Color::Muted, None),
1621        };
1622
1623        h_flex()
1624            .gap_1()
1625            .group("term-tab-icon")
1626            .child(
1627                h_flex()
1628                    .group("term-tab-icon")
1629                    .child(
1630                        div()
1631                            .when(rerun_button.is_some(), |this| {
1632                                this.hover(|style| style.invisible().w_0())
1633                            })
1634                            .child(Icon::new(icon).color(icon_color)),
1635                    )
1636                    .when_some(rerun_button, |this, rerun_button| {
1637                        this.child(
1638                            div()
1639                                .absolute()
1640                                .visible_on_hover("term-tab-icon")
1641                                .child(rerun_button),
1642                        )
1643                    }),
1644            )
1645            .child(Label::new(title).color(params.text_color()))
1646            .into_any()
1647    }
1648
1649    fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
1650        let terminal = self.terminal().read(cx);
1651        terminal.title(detail == 0).into()
1652    }
1653
1654    fn telemetry_event_text(&self) -> Option<&'static str> {
1655        None
1656    }
1657
1658    fn clone_on_split(
1659        &self,
1660        workspace_id: Option<WorkspaceId>,
1661        window: &mut Window,
1662        cx: &mut Context<Self>,
1663    ) -> Option<Entity<Self>> {
1664        let window_handle = window.window_handle();
1665        let terminal = self
1666            .project
1667            .update(cx, |project, cx| {
1668                let terminal = self.terminal().read(cx);
1669                let working_directory = terminal
1670                    .working_directory()
1671                    .or_else(|| Some(project.active_project_directory(cx)?.to_path_buf()));
1672                let python_venv_directory = terminal.python_venv_directory.clone();
1673                project.create_terminal_with_venv(
1674                    TerminalKind::Shell(working_directory),
1675                    python_venv_directory,
1676                    window_handle,
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        let window_handle = window.window_handle();
1813        window.spawn(cx, async move |cx| {
1814            let cwd = cx
1815                .update(|_window, cx| {
1816                    let from_db = TERMINAL_DB
1817                        .get_working_directory(item_id, workspace_id)
1818                        .log_err()
1819                        .flatten();
1820                    if from_db
1821                        .as_ref()
1822                        .is_some_and(|from_db| !from_db.as_os_str().is_empty())
1823                    {
1824                        from_db
1825                    } else {
1826                        workspace
1827                            .upgrade()
1828                            .and_then(|workspace| default_working_directory(workspace.read(cx), cx))
1829                    }
1830                })
1831                .ok()
1832                .flatten();
1833
1834            let terminal = project
1835                .update(cx, |project, cx| {
1836                    project.create_terminal(TerminalKind::Shell(cwd), window_handle, cx)
1837                })?
1838                .await?;
1839            cx.update(|window, cx| {
1840                cx.new(|cx| {
1841                    TerminalView::new(
1842                        terminal,
1843                        workspace,
1844                        Some(workspace_id),
1845                        project.downgrade(),
1846                        window,
1847                        cx,
1848                    )
1849                })
1850            })
1851        })
1852    }
1853}
1854
1855impl SearchableItem for TerminalView {
1856    type Match = RangeInclusive<Point>;
1857
1858    fn supported_options(&self) -> SearchOptions {
1859        SearchOptions {
1860            case: false,
1861            word: false,
1862            regex: true,
1863            replacement: false,
1864            selection: false,
1865            find_in_results: false,
1866        }
1867    }
1868
1869    /// Clear stored matches
1870    fn clear_matches(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1871        self.terminal().update(cx, |term, _| term.matches.clear())
1872    }
1873
1874    /// Store matches returned from find_matches somewhere for rendering
1875    fn update_matches(
1876        &mut self,
1877        matches: &[Self::Match],
1878        _window: &mut Window,
1879        cx: &mut Context<Self>,
1880    ) {
1881        self.terminal()
1882            .update(cx, |term, _| term.matches = matches.to_vec())
1883    }
1884
1885    /// Returns the selection content to pre-load into this search
1886    fn query_suggestion(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> String {
1887        self.terminal()
1888            .read(cx)
1889            .last_content
1890            .selection_text
1891            .clone()
1892            .unwrap_or_default()
1893    }
1894
1895    /// Focus match at given index into the Vec of matches
1896    fn activate_match(
1897        &mut self,
1898        index: usize,
1899        _: &[Self::Match],
1900        _window: &mut Window,
1901        cx: &mut Context<Self>,
1902    ) {
1903        self.terminal()
1904            .update(cx, |term, _| term.activate_match(index));
1905        cx.notify();
1906    }
1907
1908    /// Add selections for all matches given.
1909    fn select_matches(&mut self, matches: &[Self::Match], _: &mut Window, cx: &mut Context<Self>) {
1910        self.terminal()
1911            .update(cx, |term, _| term.select_matches(matches));
1912        cx.notify();
1913    }
1914
1915    /// Get all of the matches for this query, should be done on the background
1916    fn find_matches(
1917        &mut self,
1918        query: Arc<SearchQuery>,
1919        _: &mut Window,
1920        cx: &mut Context<Self>,
1921    ) -> Task<Vec<Self::Match>> {
1922        if let Some(s) = regex_search_for_query(&query) {
1923            self.terminal()
1924                .update(cx, |term, cx| term.find_matches(s, cx))
1925        } else {
1926            Task::ready(vec![])
1927        }
1928    }
1929
1930    /// Reports back to the search toolbar what the active match should be (the selection)
1931    fn active_match_index(
1932        &mut self,
1933        direction: Direction,
1934        matches: &[Self::Match],
1935        _: &mut Window,
1936        cx: &mut Context<Self>,
1937    ) -> Option<usize> {
1938        // Selection head might have a value if there's a selection that isn't
1939        // associated with a match. Therefore, if there are no matches, we should
1940        // report None, no matter the state of the terminal
1941        let res = 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        res
1984    }
1985    fn replace(
1986        &mut self,
1987        _: &Self::Match,
1988        _: &SearchQuery,
1989        _window: &mut Window,
1990        _: &mut Context<Self>,
1991    ) {
1992        // Replacement is not supported in terminal view, so this is a no-op.
1993    }
1994}
1995
1996///Gets the working directory for the given workspace, respecting the user's settings.
1997/// None implies "~" on whichever machine we end up on.
1998pub(crate) fn default_working_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
1999    match &TerminalSettings::get_global(cx).working_directory {
2000        WorkingDirectory::CurrentProjectDirectory => workspace
2001            .project()
2002            .read(cx)
2003            .active_project_directory(cx)
2004            .as_deref()
2005            .map(Path::to_path_buf),
2006        WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
2007        WorkingDirectory::AlwaysHome => None,
2008        WorkingDirectory::Always { directory } => {
2009            shellexpand::full(&directory) //TODO handle this better
2010                .ok()
2011                .map(|dir| Path::new(&dir.to_string()).to_path_buf())
2012                .filter(|dir| dir.is_dir())
2013        }
2014    }
2015}
2016///Gets the first project's home directory, or the home directory
2017fn first_project_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
2018    let worktree = workspace.worktrees(cx).next()?.read(cx);
2019    if !worktree.root_entry()?.is_dir() {
2020        return None;
2021    }
2022    Some(worktree.abs_path().to_path_buf())
2023}
2024
2025#[cfg(test)]
2026mod tests {
2027    use super::*;
2028    use gpui::TestAppContext;
2029    use project::{Entry, Project, ProjectPath, Worktree};
2030    use std::path::Path;
2031    use workspace::AppState;
2032
2033    // Working directory calculation tests
2034
2035    // No Worktrees in project -> home_dir()
2036    #[gpui::test]
2037    async fn no_worktree(cx: &mut TestAppContext) {
2038        let (project, workspace) = init_test(cx).await;
2039        cx.read(|cx| {
2040            let workspace = workspace.read(cx);
2041            let active_entry = project.read(cx).active_entry();
2042
2043            //Make sure environment is as expected
2044            assert!(active_entry.is_none());
2045            assert!(workspace.worktrees(cx).next().is_none());
2046
2047            let res = default_working_directory(workspace, cx);
2048            assert_eq!(res, None);
2049            let res = first_project_directory(workspace, cx);
2050            assert_eq!(res, None);
2051        });
2052    }
2053
2054    // No active entry, but a worktree, worktree is a file -> home_dir()
2055    #[gpui::test]
2056    async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
2057        let (project, workspace) = init_test(cx).await;
2058
2059        create_file_wt(project.clone(), "/root.txt", cx).await;
2060        cx.read(|cx| {
2061            let workspace = workspace.read(cx);
2062            let active_entry = project.read(cx).active_entry();
2063
2064            //Make sure environment is as expected
2065            assert!(active_entry.is_none());
2066            assert!(workspace.worktrees(cx).next().is_some());
2067
2068            let res = default_working_directory(workspace, cx);
2069            assert_eq!(res, None);
2070            let res = first_project_directory(workspace, cx);
2071            assert_eq!(res, None);
2072        });
2073    }
2074
2075    // No active entry, but a worktree, worktree is a folder -> worktree_folder
2076    #[gpui::test]
2077    async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
2078        let (project, workspace) = init_test(cx).await;
2079
2080        let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
2081        cx.update(|cx| {
2082            let workspace = workspace.read(cx);
2083            let active_entry = project.read(cx).active_entry();
2084
2085            assert!(active_entry.is_none());
2086            assert!(workspace.worktrees(cx).next().is_some());
2087
2088            let res = default_working_directory(workspace, cx);
2089            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
2090            let res = first_project_directory(workspace, cx);
2091            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
2092        });
2093    }
2094
2095    // Active entry with a work tree, worktree is a file -> worktree_folder()
2096    #[gpui::test]
2097    async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
2098        let (project, workspace) = init_test(cx).await;
2099
2100        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
2101        let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
2102        insert_active_entry_for(wt2, entry2, project.clone(), cx);
2103
2104        cx.update(|cx| {
2105            let workspace = workspace.read(cx);
2106            let active_entry = project.read(cx).active_entry();
2107
2108            assert!(active_entry.is_some());
2109
2110            let res = default_working_directory(workspace, cx);
2111            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
2112            let res = first_project_directory(workspace, cx);
2113            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
2114        });
2115    }
2116
2117    // Active entry, with a worktree, worktree is a folder -> worktree_folder
2118    #[gpui::test]
2119    async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
2120        let (project, workspace) = init_test(cx).await;
2121
2122        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
2123        let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
2124        insert_active_entry_for(wt2, entry2, project.clone(), cx);
2125
2126        cx.update(|cx| {
2127            let workspace = workspace.read(cx);
2128            let active_entry = project.read(cx).active_entry();
2129
2130            assert!(active_entry.is_some());
2131
2132            let res = default_working_directory(workspace, cx);
2133            assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
2134            let res = first_project_directory(workspace, cx);
2135            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
2136        });
2137    }
2138
2139    /// Creates a worktree with 1 file: /root.txt
2140    pub async fn init_test(cx: &mut TestAppContext) -> (Entity<Project>, Entity<Workspace>) {
2141        let params = cx.update(AppState::test);
2142        cx.update(|cx| {
2143            terminal::init(cx);
2144            theme::init(theme::LoadThemes::JustBase, cx);
2145            Project::init_settings(cx);
2146            language::init(cx);
2147        });
2148
2149        let project = Project::test(params.fs.clone(), [], cx).await;
2150        let workspace = cx
2151            .add_window(|window, cx| Workspace::test_new(project.clone(), window, cx))
2152            .root(cx)
2153            .unwrap();
2154
2155        (project, workspace)
2156    }
2157
2158    /// Creates a worktree with 1 folder: /root{suffix}/
2159    async fn create_folder_wt(
2160        project: Entity<Project>,
2161        path: impl AsRef<Path>,
2162        cx: &mut TestAppContext,
2163    ) -> (Entity<Worktree>, Entry) {
2164        create_wt(project, true, path, cx).await
2165    }
2166
2167    /// Creates a worktree with 1 file: /root{suffix}.txt
2168    async fn create_file_wt(
2169        project: Entity<Project>,
2170        path: impl AsRef<Path>,
2171        cx: &mut TestAppContext,
2172    ) -> (Entity<Worktree>, Entry) {
2173        create_wt(project, false, path, cx).await
2174    }
2175
2176    async fn create_wt(
2177        project: Entity<Project>,
2178        is_dir: bool,
2179        path: impl AsRef<Path>,
2180        cx: &mut TestAppContext,
2181    ) -> (Entity<Worktree>, Entry) {
2182        let (wt, _) = project
2183            .update(cx, |project, cx| {
2184                project.find_or_create_worktree(path, true, cx)
2185            })
2186            .await
2187            .unwrap();
2188
2189        let entry = cx
2190            .update(|cx| {
2191                wt.update(cx, |wt, cx| {
2192                    wt.create_entry(Path::new(""), is_dir, None, cx)
2193                })
2194            })
2195            .await
2196            .unwrap()
2197            .to_included()
2198            .unwrap();
2199
2200        (wt, entry)
2201    }
2202
2203    pub fn insert_active_entry_for(
2204        wt: Entity<Worktree>,
2205        entry: Entry,
2206        project: Entity<Project>,
2207        cx: &mut TestAppContext,
2208    ) {
2209        cx.update(|cx| {
2210            let p = ProjectPath {
2211                worktree_id: wt.read(cx).id(),
2212                path: entry.path,
2213            };
2214            project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
2215        });
2216    }
2217}