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