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().into_bytes());
 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().into_bytes());
 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                let processed =
 655                    term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta);
 656                if processed && term.vi_mode_enabled() {
 657                    cx.notify();
 658                }
 659                processed
 660            });
 661        }
 662    }
 663
 664    fn dispatch_context(&self, cx: &App) -> KeyContext {
 665        let mut dispatch_context = KeyContext::new_with_defaults();
 666        dispatch_context.add("Terminal");
 667
 668        if self.terminal.read(cx).vi_mode_enabled() {
 669            dispatch_context.add("vi_mode");
 670        }
 671
 672        let mode = self.terminal.read(cx).last_content.mode;
 673        dispatch_context.set(
 674            "screen",
 675            if mode.contains(TermMode::ALT_SCREEN) {
 676                "alt"
 677            } else {
 678                "normal"
 679            },
 680        );
 681
 682        if mode.contains(TermMode::APP_CURSOR) {
 683            dispatch_context.add("DECCKM");
 684        }
 685        if mode.contains(TermMode::APP_KEYPAD) {
 686            dispatch_context.add("DECPAM");
 687        } else {
 688            dispatch_context.add("DECPNM");
 689        }
 690        if mode.contains(TermMode::SHOW_CURSOR) {
 691            dispatch_context.add("DECTCEM");
 692        }
 693        if mode.contains(TermMode::LINE_WRAP) {
 694            dispatch_context.add("DECAWM");
 695        }
 696        if mode.contains(TermMode::ORIGIN) {
 697            dispatch_context.add("DECOM");
 698        }
 699        if mode.contains(TermMode::INSERT) {
 700            dispatch_context.add("IRM");
 701        }
 702        //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
 703        if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
 704            dispatch_context.add("LNM");
 705        }
 706        if mode.contains(TermMode::FOCUS_IN_OUT) {
 707            dispatch_context.add("report_focus");
 708        }
 709        if mode.contains(TermMode::ALTERNATE_SCROLL) {
 710            dispatch_context.add("alternate_scroll");
 711        }
 712        if mode.contains(TermMode::BRACKETED_PASTE) {
 713            dispatch_context.add("bracketed_paste");
 714        }
 715        if mode.intersects(TermMode::MOUSE_MODE) {
 716            dispatch_context.add("any_mouse_reporting");
 717        }
 718        {
 719            let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
 720                "click"
 721            } else if mode.contains(TermMode::MOUSE_DRAG) {
 722                "drag"
 723            } else if mode.contains(TermMode::MOUSE_MOTION) {
 724                "motion"
 725            } else {
 726                "off"
 727            };
 728            dispatch_context.set("mouse_reporting", mouse_reporting);
 729        }
 730        {
 731            let format = if mode.contains(TermMode::SGR_MOUSE) {
 732                "sgr"
 733            } else if mode.contains(TermMode::UTF8_MOUSE) {
 734                "utf8"
 735            } else {
 736                "normal"
 737            };
 738            dispatch_context.set("mouse_format", format);
 739        };
 740        dispatch_context
 741    }
 742
 743    fn set_terminal(
 744        &mut self,
 745        terminal: Entity<Terminal>,
 746        window: &mut Window,
 747        cx: &mut Context<TerminalView>,
 748    ) {
 749        self._terminal_subscriptions =
 750            subscribe_for_terminal_events(&terminal, self.workspace.clone(), window, cx);
 751        self.terminal = terminal;
 752    }
 753
 754    // Hack: Using editor in terminal causes cyclic dependency i.e. editor -> terminal -> project -> editor.
 755    fn map_show_scrollbar_from_editor_to_terminal(
 756        show_scrollbar: editor::ShowScrollbar,
 757    ) -> terminal_settings::ShowScrollbar {
 758        match show_scrollbar {
 759            editor::ShowScrollbar::Auto => terminal_settings::ShowScrollbar::Auto,
 760            editor::ShowScrollbar::System => terminal_settings::ShowScrollbar::System,
 761            editor::ShowScrollbar::Always => terminal_settings::ShowScrollbar::Always,
 762            editor::ShowScrollbar::Never => terminal_settings::ShowScrollbar::Never,
 763        }
 764    }
 765
 766    fn should_show_scrollbar(cx: &App) -> bool {
 767        let show = TerminalSettings::get_global(cx)
 768            .scrollbar
 769            .show
 770            .unwrap_or_else(|| {
 771                Self::map_show_scrollbar_from_editor_to_terminal(
 772                    EditorSettings::get_global(cx).scrollbar.show,
 773                )
 774            });
 775        match show {
 776            terminal_settings::ShowScrollbar::Auto => true,
 777            terminal_settings::ShowScrollbar::System => true,
 778            terminal_settings::ShowScrollbar::Always => true,
 779            terminal_settings::ShowScrollbar::Never => false,
 780        }
 781    }
 782
 783    fn should_autohide_scrollbar(cx: &App) -> bool {
 784        let show = TerminalSettings::get_global(cx)
 785            .scrollbar
 786            .show
 787            .unwrap_or_else(|| {
 788                Self::map_show_scrollbar_from_editor_to_terminal(
 789                    EditorSettings::get_global(cx).scrollbar.show,
 790                )
 791            });
 792        match show {
 793            terminal_settings::ShowScrollbar::Auto => true,
 794            terminal_settings::ShowScrollbar::System => cx
 795                .try_global::<ScrollbarAutoHide>()
 796                .map_or_else(|| cx.should_auto_hide_scrollbars(), |autohide| autohide.0),
 797            terminal_settings::ShowScrollbar::Always => false,
 798            terminal_settings::ShowScrollbar::Never => true,
 799        }
 800    }
 801
 802    fn hide_scrollbar(&mut self, cx: &mut Context<Self>) {
 803        const SCROLLBAR_SHOW_INTERVAL: Duration = Duration::from_secs(1);
 804        if !Self::should_autohide_scrollbar(cx) {
 805            return;
 806        }
 807        self.hide_scrollbar_task = Some(cx.spawn(async move |panel, cx| {
 808            cx.background_executor()
 809                .timer(SCROLLBAR_SHOW_INTERVAL)
 810                .await;
 811            panel
 812                .update(cx, |panel, cx| {
 813                    panel.show_scrollbar = false;
 814                    cx.notify();
 815                })
 816                .log_err();
 817        }))
 818    }
 819
 820    fn render_scrollbar(&self, cx: &mut Context<Self>) -> Option<Stateful<Div>> {
 821        if !Self::should_show_scrollbar(cx)
 822            || !(self.show_scrollbar || self.scrollbar_state.is_dragging())
 823        {
 824            return None;
 825        }
 826
 827        if self.terminal.read(cx).total_lines() == self.terminal.read(cx).viewport_lines() {
 828            return None;
 829        }
 830
 831        self.scroll_handle.update(self.terminal.read(cx));
 832
 833        if let Some(new_display_offset) = self.scroll_handle.future_display_offset.take() {
 834            self.terminal.update(cx, |term, _| {
 835                let delta = new_display_offset as i32 - term.last_content.display_offset as i32;
 836                match delta.cmp(&0) {
 837                    std::cmp::Ordering::Greater => term.scroll_up_by(delta as usize),
 838                    std::cmp::Ordering::Less => term.scroll_down_by(-delta as usize),
 839                    std::cmp::Ordering::Equal => {}
 840                }
 841            });
 842        }
 843
 844        Some(
 845            div()
 846                .occlude()
 847                .id("terminal-view-scroll")
 848                .on_mouse_move(cx.listener(|_, _, _window, cx| {
 849                    cx.notify();
 850                    cx.stop_propagation()
 851                }))
 852                .on_hover(|_, _window, cx| {
 853                    cx.stop_propagation();
 854                })
 855                .on_any_mouse_down(|_, _window, cx| {
 856                    cx.stop_propagation();
 857                })
 858                .on_mouse_up(
 859                    MouseButton::Left,
 860                    cx.listener(|terminal_view, _, window, cx| {
 861                        if !terminal_view.scrollbar_state.is_dragging()
 862                            && !terminal_view.focus_handle.contains_focused(window, cx)
 863                        {
 864                            terminal_view.hide_scrollbar(cx);
 865                            cx.notify();
 866                        }
 867                        cx.stop_propagation();
 868                    }),
 869                )
 870                .on_scroll_wheel(cx.listener(|_, _, _window, cx| {
 871                    cx.notify();
 872                }))
 873                .h_full()
 874                .absolute()
 875                .right_1()
 876                .top_1()
 877                .bottom_0()
 878                .w(px(12.))
 879                .cursor_default()
 880                .children(Scrollbar::vertical(self.scrollbar_state.clone())),
 881        )
 882    }
 883
 884    fn rerun_button(task: &TaskState) -> Option<IconButton> {
 885        if !task.show_rerun {
 886            return None;
 887        }
 888
 889        let task_id = task.id.clone();
 890        Some(
 891            IconButton::new("rerun-icon", IconName::Rerun)
 892                .icon_size(IconSize::Small)
 893                .size(ButtonSize::Compact)
 894                .icon_color(Color::Default)
 895                .shape(ui::IconButtonShape::Square)
 896                .tooltip(move |window, cx| {
 897                    Tooltip::for_action("Rerun task", &RerunTask, window, cx)
 898                })
 899                .on_click(move |_, window, cx| {
 900                    window.dispatch_action(Box::new(terminal_rerun_override(&task_id)), cx);
 901                }),
 902        )
 903    }
 904}
 905
 906fn terminal_rerun_override(task: &TaskId) -> zed_actions::Rerun {
 907    zed_actions::Rerun {
 908        task_id: Some(task.0.clone()),
 909        allow_concurrent_runs: Some(true),
 910        use_new_terminal: Some(false),
 911        reevaluate_context: false,
 912    }
 913}
 914
 915fn subscribe_for_terminal_events(
 916    terminal: &Entity<Terminal>,
 917    workspace: WeakEntity<Workspace>,
 918    window: &mut Window,
 919    cx: &mut Context<TerminalView>,
 920) -> Vec<Subscription> {
 921    let terminal_subscription = cx.observe(terminal, |_, _, cx| cx.notify());
 922    let mut previous_cwd = None;
 923    let terminal_events_subscription = cx.subscribe_in(
 924        terminal,
 925        window,
 926        move |terminal_view, terminal, event, window, cx| {
 927            let current_cwd = terminal.read(cx).working_directory();
 928            if current_cwd != previous_cwd {
 929                previous_cwd = current_cwd;
 930                terminal_view.cwd_serialized = false;
 931            }
 932
 933            match event {
 934                Event::Wakeup => {
 935                    cx.notify();
 936                    cx.emit(Event::Wakeup);
 937                    cx.emit(ItemEvent::UpdateTab);
 938                    cx.emit(SearchEvent::MatchesInvalidated);
 939                }
 940
 941                Event::Bell => {
 942                    terminal_view.has_bell = true;
 943                    cx.emit(Event::Wakeup);
 944                }
 945
 946                Event::BlinkChanged(blinking) => {
 947                    if matches!(
 948                        TerminalSettings::get_global(cx).blinking,
 949                        TerminalBlink::TerminalControlled
 950                    ) {
 951                        terminal_view.blinking_terminal_enabled = *blinking;
 952                    }
 953                }
 954
 955                Event::TitleChanged => {
 956                    cx.emit(ItemEvent::UpdateTab);
 957                }
 958
 959                Event::NewNavigationTarget(maybe_navigation_target) => {
 960                    match maybe_navigation_target
 961                        .as_ref()
 962                        .zip(terminal.read(cx).last_content.last_hovered_word.as_ref())
 963                    {
 964                        Some((MaybeNavigationTarget::Url(url), hovered_word)) => {
 965                            if Some(hovered_word)
 966                                != terminal_view
 967                                    .hover
 968                                    .as_ref()
 969                                    .map(|hover| &hover.hovered_word)
 970                            {
 971                                terminal_view.hover = Some(HoverTarget {
 972                                    tooltip: url.clone(),
 973                                    hovered_word: hovered_word.clone(),
 974                                });
 975                                terminal_view.hover_tooltip_update = Task::ready(());
 976                                cx.notify();
 977                            }
 978                        }
 979                        Some((MaybeNavigationTarget::PathLike(path_like_target), hovered_word)) => {
 980                            if Some(hovered_word)
 981                                != terminal_view
 982                                    .hover
 983                                    .as_ref()
 984                                    .map(|hover| &hover.hovered_word)
 985                            {
 986                                let valid_files_to_open_task = possible_open_target(
 987                                    &workspace,
 988                                    &path_like_target.terminal_dir,
 989                                    &path_like_target.maybe_path,
 990                                    cx,
 991                                );
 992                                let hovered_word = hovered_word.clone();
 993
 994                                terminal_view.hover = None;
 995                                terminal_view.hover_tooltip_update =
 996                                    cx.spawn(async move |terminal_view, cx| {
 997                                        let file_to_open = valid_files_to_open_task.await;
 998                                        terminal_view
 999                                            .update(cx, |terminal_view, _| match file_to_open {
1000                                                Some(
1001                                                    OpenTarget::File(path, _)
1002                                                    | OpenTarget::Worktree(path, _),
1003                                                ) => {
1004                                                    terminal_view.hover = Some(HoverTarget {
1005                                                        tooltip: path.to_string(|path| {
1006                                                            path.to_string_lossy().to_string()
1007                                                        }),
1008                                                        hovered_word,
1009                                                    });
1010                                                }
1011                                                None => {
1012                                                    terminal_view.hover = None;
1013                                                }
1014                                            })
1015                                            .ok();
1016                                    });
1017                                cx.notify();
1018                            }
1019                        }
1020                        None => {
1021                            terminal_view.hover = None;
1022                            terminal_view.hover_tooltip_update = Task::ready(());
1023                            cx.notify();
1024                        }
1025                    }
1026                }
1027
1028                Event::Open(maybe_navigation_target) => match maybe_navigation_target {
1029                    MaybeNavigationTarget::Url(url) => cx.open_url(url),
1030
1031                    MaybeNavigationTarget::PathLike(path_like_target) => {
1032                        if terminal_view.hover.is_none() {
1033                            return;
1034                        }
1035                        let task_workspace = workspace.clone();
1036                        let path_like_target = path_like_target.clone();
1037                        cx.spawn_in(window, async move |terminal_view, cx| {
1038                            let open_target = terminal_view
1039                                .update(cx, |_, cx| {
1040                                    possible_open_target(
1041                                        &task_workspace,
1042                                        &path_like_target.terminal_dir,
1043                                        &path_like_target.maybe_path,
1044                                        cx,
1045                                    )
1046                                })?
1047                                .await;
1048                            if let Some(open_target) = open_target {
1049                                let path_to_open = open_target.path();
1050                                let opened_items = task_workspace
1051                                    .update_in(cx, |workspace, window, cx| {
1052                                        workspace.open_paths(
1053                                            vec![path_to_open.path.clone()],
1054                                            OpenOptions {
1055                                                visible: Some(OpenVisible::OnlyDirectories),
1056                                                ..Default::default()
1057                                            },
1058                                            None,
1059                                            window,
1060                                            cx,
1061                                        )
1062                                    })
1063                                    .context("workspace update")?
1064                                    .await;
1065                                if opened_items.len() != 1 {
1066                                    debug_panic!(
1067                                        "Received {} items for one path {path_to_open:?}",
1068                                        opened_items.len(),
1069                                    );
1070                                }
1071
1072                                if let Some(opened_item) = opened_items.first() {
1073                                    if open_target.is_file() {
1074                                        if let Some(Ok(opened_item)) = opened_item {
1075                                            if let Some(row) = path_to_open.row {
1076                                                let col = path_to_open.column.unwrap_or(0);
1077                                                if let Some(active_editor) =
1078                                                    opened_item.downcast::<Editor>()
1079                                                {
1080                                                    active_editor
1081                                                        .downgrade()
1082                                                        .update_in(cx, |editor, window, cx| {
1083                                                            editor.go_to_singleton_buffer_point(
1084                                                                language::Point::new(
1085                                                                    row.saturating_sub(1),
1086                                                                    col.saturating_sub(1),
1087                                                                ),
1088                                                                window,
1089                                                                cx,
1090                                                            )
1091                                                        })
1092                                                        .log_err();
1093                                                }
1094                                            }
1095                                        }
1096                                    } else if open_target.is_dir() {
1097                                        task_workspace.update(cx, |workspace, cx| {
1098                                            workspace.project().update(cx, |_, cx| {
1099                                                cx.emit(project::Event::ActivateProjectPanel);
1100                                            })
1101                                        })?;
1102                                    }
1103                                }
1104                            }
1105
1106                            anyhow::Ok(())
1107                        })
1108                        .detach_and_log_err(cx)
1109                    }
1110                },
1111                Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
1112                Event::CloseTerminal => cx.emit(ItemEvent::CloseItem),
1113                Event::SelectionsChanged => {
1114                    window.invalidate_character_coordinates();
1115                    cx.emit(SearchEvent::ActiveMatchChanged)
1116                }
1117            }
1118        },
1119    );
1120    vec![terminal_subscription, terminal_events_subscription]
1121}
1122
1123#[derive(Debug, Clone)]
1124enum OpenTarget {
1125    Worktree(PathWithPosition, Entry),
1126    File(PathWithPosition, Metadata),
1127}
1128
1129impl OpenTarget {
1130    fn is_file(&self) -> bool {
1131        match self {
1132            OpenTarget::Worktree(_, entry) => entry.is_file(),
1133            OpenTarget::File(_, metadata) => !metadata.is_dir,
1134        }
1135    }
1136
1137    fn is_dir(&self) -> bool {
1138        match self {
1139            OpenTarget::Worktree(_, entry) => entry.is_dir(),
1140            OpenTarget::File(_, metadata) => metadata.is_dir,
1141        }
1142    }
1143
1144    fn path(&self) -> &PathWithPosition {
1145        match self {
1146            OpenTarget::Worktree(path, _) => path,
1147            OpenTarget::File(path, _) => path,
1148        }
1149    }
1150}
1151
1152fn possible_open_target(
1153    workspace: &WeakEntity<Workspace>,
1154    cwd: &Option<PathBuf>,
1155    maybe_path: &str,
1156    cx: &App,
1157) -> Task<Option<OpenTarget>> {
1158    let Some(workspace) = workspace.upgrade() else {
1159        return Task::ready(None);
1160    };
1161    // We have to check for both paths, as on Unix, certain paths with positions are valid file paths too.
1162    // We can be on FS remote part, without real FS, so cannot canonicalize or check for existence the path right away.
1163    let mut potential_paths = Vec::new();
1164    let original_path = PathWithPosition::from_path(PathBuf::from(maybe_path));
1165    let path_with_position = PathWithPosition::parse_str(maybe_path);
1166    let worktree_candidates = workspace
1167        .read(cx)
1168        .worktrees(cx)
1169        .sorted_by_key(|worktree| {
1170            let worktree_root = worktree.read(cx).abs_path();
1171            match cwd
1172                .as_ref()
1173                .and_then(|cwd| worktree_root.strip_prefix(cwd).ok())
1174            {
1175                Some(cwd_child) => cwd_child.components().count(),
1176                None => usize::MAX,
1177            }
1178        })
1179        .collect::<Vec<_>>();
1180    // Since we do not check paths via FS and joining, we need to strip off potential `./`, `a/`, `b/` prefixes out of it.
1181    for prefix_str in GIT_DIFF_PATH_PREFIXES.iter().chain(std::iter::once(&".")) {
1182        if let Some(stripped) = original_path.path.strip_prefix(prefix_str).ok() {
1183            potential_paths.push(PathWithPosition {
1184                path: stripped.to_owned(),
1185                row: original_path.row,
1186                column: original_path.column,
1187            });
1188        }
1189        if let Some(stripped) = path_with_position.path.strip_prefix(prefix_str).ok() {
1190            potential_paths.push(PathWithPosition {
1191                path: stripped.to_owned(),
1192                row: path_with_position.row,
1193                column: path_with_position.column,
1194            });
1195        }
1196    }
1197
1198    let insert_both_paths = original_path != path_with_position;
1199    potential_paths.insert(0, original_path);
1200    if insert_both_paths {
1201        potential_paths.insert(1, path_with_position);
1202    }
1203
1204    // If we won't find paths "easily", we can traverse the entire worktree to look what ends with the potential path suffix.
1205    // That will be slow, though, so do the fast checks first.
1206    let mut worktree_paths_to_check = Vec::new();
1207    for worktree in &worktree_candidates {
1208        let worktree_root = worktree.read(cx).abs_path();
1209        let mut paths_to_check = Vec::with_capacity(potential_paths.len());
1210
1211        for path_with_position in &potential_paths {
1212            let path_to_check = if worktree_root.ends_with(&path_with_position.path) {
1213                let root_path_with_position = PathWithPosition {
1214                    path: worktree_root.to_path_buf(),
1215                    row: path_with_position.row,
1216                    column: path_with_position.column,
1217                };
1218                match worktree.read(cx).root_entry() {
1219                    Some(root_entry) => {
1220                        return Task::ready(Some(OpenTarget::Worktree(
1221                            root_path_with_position,
1222                            root_entry.clone(),
1223                        )));
1224                    }
1225                    None => root_path_with_position,
1226                }
1227            } else {
1228                PathWithPosition {
1229                    path: path_with_position
1230                        .path
1231                        .strip_prefix(&worktree_root)
1232                        .unwrap_or(&path_with_position.path)
1233                        .to_owned(),
1234                    row: path_with_position.row,
1235                    column: path_with_position.column,
1236                }
1237            };
1238
1239            if path_to_check.path.is_relative() {
1240                if let Some(entry) = worktree.read(cx).entry_for_path(&path_to_check.path) {
1241                    return Task::ready(Some(OpenTarget::Worktree(
1242                        PathWithPosition {
1243                            path: worktree_root.join(&entry.path),
1244                            row: path_to_check.row,
1245                            column: path_to_check.column,
1246                        },
1247                        entry.clone(),
1248                    )));
1249                }
1250            }
1251
1252            paths_to_check.push(path_to_check);
1253        }
1254
1255        if !paths_to_check.is_empty() {
1256            worktree_paths_to_check.push((worktree.clone(), paths_to_check));
1257        }
1258    }
1259
1260    // Before entire worktree traversal(s), make an attempt to do FS checks if available.
1261    let fs_paths_to_check = if workspace.read(cx).project().read(cx).is_local() {
1262        potential_paths
1263            .into_iter()
1264            .flat_map(|path_to_check| {
1265                let mut paths_to_check = Vec::new();
1266                let maybe_path = &path_to_check.path;
1267                if maybe_path.starts_with("~") {
1268                    if let Some(home_path) =
1269                        maybe_path
1270                            .strip_prefix("~")
1271                            .ok()
1272                            .and_then(|stripped_maybe_path| {
1273                                Some(dirs::home_dir()?.join(stripped_maybe_path))
1274                            })
1275                    {
1276                        paths_to_check.push(PathWithPosition {
1277                            path: home_path,
1278                            row: path_to_check.row,
1279                            column: path_to_check.column,
1280                        });
1281                    }
1282                } else {
1283                    paths_to_check.push(PathWithPosition {
1284                        path: maybe_path.clone(),
1285                        row: path_to_check.row,
1286                        column: path_to_check.column,
1287                    });
1288                    if maybe_path.is_relative() {
1289                        if let Some(cwd) = &cwd {
1290                            paths_to_check.push(PathWithPosition {
1291                                path: cwd.join(maybe_path),
1292                                row: path_to_check.row,
1293                                column: path_to_check.column,
1294                            });
1295                        }
1296                        for worktree in &worktree_candidates {
1297                            paths_to_check.push(PathWithPosition {
1298                                path: worktree.read(cx).abs_path().join(maybe_path),
1299                                row: path_to_check.row,
1300                                column: path_to_check.column,
1301                            });
1302                        }
1303                    }
1304                }
1305                paths_to_check
1306            })
1307            .collect()
1308    } else {
1309        Vec::new()
1310    };
1311
1312    let worktree_check_task = cx.spawn(async move |cx| {
1313        for (worktree, worktree_paths_to_check) in worktree_paths_to_check {
1314            let found_entry = worktree
1315                .update(cx, |worktree, _| {
1316                    let worktree_root = worktree.abs_path();
1317                    let mut traversal = worktree.traverse_from_path(true, true, false, "".as_ref());
1318                    while let Some(entry) = traversal.next() {
1319                        if let Some(path_in_worktree) = worktree_paths_to_check
1320                            .iter()
1321                            .find(|path_to_check| entry.path.ends_with(&path_to_check.path))
1322                        {
1323                            return Some(OpenTarget::Worktree(
1324                                PathWithPosition {
1325                                    path: worktree_root.join(&entry.path),
1326                                    row: path_in_worktree.row,
1327                                    column: path_in_worktree.column,
1328                                },
1329                                entry.clone(),
1330                            ));
1331                        }
1332                    }
1333                    None
1334                })
1335                .ok()?;
1336            if let Some(found_entry) = found_entry {
1337                return Some(found_entry);
1338            }
1339        }
1340        None
1341    });
1342
1343    let fs = workspace.read(cx).project().read(cx).fs().clone();
1344    cx.background_spawn(async move {
1345        for mut path_to_check in fs_paths_to_check {
1346            if let Some(fs_path_to_check) = fs.canonicalize(&path_to_check.path).await.ok() {
1347                if let Some(metadata) = fs.metadata(&fs_path_to_check).await.ok().flatten() {
1348                    path_to_check.path = fs_path_to_check;
1349                    return Some(OpenTarget::File(path_to_check, metadata));
1350                }
1351            }
1352        }
1353
1354        worktree_check_task.await
1355    })
1356}
1357
1358fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
1359    let str = query.as_str();
1360    if query.is_regex() {
1361        if str == "." {
1362            return None;
1363        }
1364        RegexSearch::new(str).ok()
1365    } else {
1366        RegexSearch::new(&regex::escape(str)).ok()
1367    }
1368}
1369
1370impl TerminalView {
1371    fn key_down(&mut self, event: &KeyDownEvent, window: &mut Window, cx: &mut Context<Self>) {
1372        self.clear_bell(cx);
1373        self.pause_cursor_blinking(window, cx);
1374
1375        self.terminal.update(cx, |term, cx| {
1376            let handled = term.try_keystroke(
1377                &event.keystroke,
1378                TerminalSettings::get_global(cx).option_as_meta,
1379            );
1380            if handled {
1381                cx.stop_propagation();
1382            }
1383        });
1384    }
1385
1386    fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1387        self.terminal.update(cx, |terminal, _| {
1388            terminal.set_cursor_shape(self.cursor_shape);
1389            terminal.focus_in();
1390        });
1391        self.blink_cursors(self.blink_epoch, window, cx);
1392        window.invalidate_character_coordinates();
1393        cx.notify();
1394    }
1395
1396    fn focus_out(&mut self, _: &mut Window, cx: &mut Context<Self>) {
1397        self.terminal.update(cx, |terminal, _| {
1398            terminal.focus_out();
1399            terminal.set_cursor_shape(CursorShape::Hollow);
1400        });
1401        self.hide_scrollbar(cx);
1402        cx.notify();
1403    }
1404}
1405
1406impl Render for TerminalView {
1407    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1408        let terminal_handle = self.terminal.clone();
1409        let terminal_view_handle = cx.entity().clone();
1410
1411        let focused = self.focus_handle.is_focused(window);
1412
1413        div()
1414            .id("terminal-view")
1415            .size_full()
1416            .relative()
1417            .track_focus(&self.focus_handle(cx))
1418            .key_context(self.dispatch_context(cx))
1419            .on_action(cx.listener(TerminalView::send_text))
1420            .on_action(cx.listener(TerminalView::send_keystroke))
1421            .on_action(cx.listener(TerminalView::copy))
1422            .on_action(cx.listener(TerminalView::paste))
1423            .on_action(cx.listener(TerminalView::clear))
1424            .on_action(cx.listener(TerminalView::scroll_line_up))
1425            .on_action(cx.listener(TerminalView::scroll_line_down))
1426            .on_action(cx.listener(TerminalView::scroll_page_up))
1427            .on_action(cx.listener(TerminalView::scroll_page_down))
1428            .on_action(cx.listener(TerminalView::scroll_to_top))
1429            .on_action(cx.listener(TerminalView::scroll_to_bottom))
1430            .on_action(cx.listener(TerminalView::toggle_vi_mode))
1431            .on_action(cx.listener(TerminalView::show_character_palette))
1432            .on_action(cx.listener(TerminalView::select_all))
1433            .on_action(cx.listener(TerminalView::rerun_task))
1434            .on_key_down(cx.listener(Self::key_down))
1435            .on_mouse_down(
1436                MouseButton::Right,
1437                cx.listener(|this, event: &MouseDownEvent, window, cx| {
1438                    if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) {
1439                        if this.terminal.read(cx).last_content.selection.is_none() {
1440                            this.terminal.update(cx, |terminal, _| {
1441                                terminal.select_word_at_event_position(event);
1442                            });
1443                        };
1444                        this.deploy_context_menu(event.position, window, cx);
1445                        cx.notify();
1446                    }
1447                }),
1448            )
1449            .on_hover(cx.listener(|this, hovered, window, cx| {
1450                if *hovered {
1451                    this.show_scrollbar = true;
1452                    this.hide_scrollbar_task.take();
1453                    cx.notify();
1454                } else if !this.focus_handle.contains_focused(window, cx) {
1455                    this.hide_scrollbar(cx);
1456                }
1457            }))
1458            .child(
1459                // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu
1460                div()
1461                    .size_full()
1462                    .child(TerminalElement::new(
1463                        terminal_handle,
1464                        terminal_view_handle,
1465                        self.workspace.clone(),
1466                        self.focus_handle.clone(),
1467                        focused,
1468                        self.should_show_cursor(focused, cx),
1469                        self.block_below_cursor.clone(),
1470                        self.embedded,
1471                    ))
1472                    .when_some(self.render_scrollbar(cx), |div, scrollbar| {
1473                        div.child(scrollbar)
1474                    }),
1475            )
1476            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1477                deferred(
1478                    anchored()
1479                        .position(*position)
1480                        .anchor(gpui::Corner::TopLeft)
1481                        .child(menu.clone()),
1482                )
1483                .with_priority(1)
1484            }))
1485    }
1486}
1487
1488impl Item for TerminalView {
1489    type Event = ItemEvent;
1490
1491    fn tab_tooltip_content(&self, cx: &App) -> Option<TabTooltipContent> {
1492        let terminal = self.terminal().read(cx);
1493        let title = terminal.title(false);
1494        let pid = terminal.pty_info.pid_getter().fallback_pid();
1495
1496        Some(TabTooltipContent::Custom(Box::new(move |_window, cx| {
1497            cx.new(|_| TerminalTooltip::new(title.clone(), pid)).into()
1498        })))
1499    }
1500
1501    fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
1502        let terminal = self.terminal().read(cx);
1503        let title = terminal.title(true);
1504
1505        let (icon, icon_color, rerun_button) = match terminal.task() {
1506            Some(terminal_task) => match &terminal_task.status {
1507                TaskStatus::Running => (
1508                    IconName::Play,
1509                    Color::Disabled,
1510                    TerminalView::rerun_button(&terminal_task),
1511                ),
1512                TaskStatus::Unknown => (
1513                    IconName::Warning,
1514                    Color::Warning,
1515                    TerminalView::rerun_button(&terminal_task),
1516                ),
1517                TaskStatus::Completed { success } => {
1518                    let rerun_button = TerminalView::rerun_button(&terminal_task);
1519
1520                    if *success {
1521                        (IconName::Check, Color::Success, rerun_button)
1522                    } else {
1523                        (IconName::XCircle, Color::Error, rerun_button)
1524                    }
1525                }
1526            },
1527            None => (IconName::Terminal, Color::Muted, None),
1528        };
1529
1530        h_flex()
1531            .gap_1()
1532            .group("term-tab-icon")
1533            .child(
1534                h_flex()
1535                    .group("term-tab-icon")
1536                    .child(
1537                        div()
1538                            .when(rerun_button.is_some(), |this| {
1539                                this.hover(|style| style.invisible().w_0())
1540                            })
1541                            .child(Icon::new(icon).color(icon_color)),
1542                    )
1543                    .when_some(rerun_button, |this, rerun_button| {
1544                        this.child(
1545                            div()
1546                                .absolute()
1547                                .visible_on_hover("term-tab-icon")
1548                                .child(rerun_button),
1549                        )
1550                    }),
1551            )
1552            .child(Label::new(title).color(params.text_color()))
1553            .into_any()
1554    }
1555
1556    fn tab_content_text(&self, detail: usize, cx: &App) -> SharedString {
1557        let terminal = self.terminal().read(cx);
1558        terminal.title(detail == 0).into()
1559    }
1560
1561    fn telemetry_event_text(&self) -> Option<&'static str> {
1562        None
1563    }
1564
1565    fn clone_on_split(
1566        &self,
1567        workspace_id: Option<WorkspaceId>,
1568        window: &mut Window,
1569        cx: &mut Context<Self>,
1570    ) -> Option<Entity<Self>> {
1571        let window_handle = window.window_handle();
1572        let terminal = self
1573            .project
1574            .update(cx, |project, cx| {
1575                let terminal = self.terminal().read(cx);
1576                let working_directory = terminal
1577                    .working_directory()
1578                    .or_else(|| Some(project.active_project_directory(cx)?.to_path_buf()));
1579                let python_venv_directory = terminal.python_venv_directory.clone();
1580                project.create_terminal_with_venv(
1581                    TerminalKind::Shell(working_directory),
1582                    python_venv_directory,
1583                    window_handle,
1584                    cx,
1585                )
1586            })
1587            .ok()?
1588            .log_err()?;
1589
1590        Some(cx.new(|cx| {
1591            TerminalView::new(
1592                terminal,
1593                self.workspace.clone(),
1594                workspace_id,
1595                self.project.clone(),
1596                false,
1597                window,
1598                cx,
1599            )
1600        }))
1601    }
1602
1603    fn is_dirty(&self, cx: &gpui::App) -> bool {
1604        match self.terminal.read(cx).task() {
1605            Some(task) => task.status == TaskStatus::Running,
1606            None => self.has_bell(),
1607        }
1608    }
1609
1610    fn has_conflict(&self, _cx: &App) -> bool {
1611        false
1612    }
1613
1614    fn can_save_as(&self, _cx: &App) -> bool {
1615        false
1616    }
1617
1618    fn is_singleton(&self, _cx: &App) -> bool {
1619        true
1620    }
1621
1622    fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1623        Some(Box::new(handle.clone()))
1624    }
1625
1626    fn breadcrumb_location(&self, cx: &App) -> ToolbarItemLocation {
1627        if self.show_breadcrumbs && !self.terminal().read(cx).breadcrumb_text.trim().is_empty() {
1628            ToolbarItemLocation::PrimaryLeft
1629        } else {
1630            ToolbarItemLocation::Hidden
1631        }
1632    }
1633
1634    fn breadcrumbs(&self, _: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
1635        Some(vec![BreadcrumbText {
1636            text: self.terminal().read(cx).breadcrumb_text.clone(),
1637            highlights: None,
1638            font: None,
1639        }])
1640    }
1641
1642    fn added_to_workspace(
1643        &mut self,
1644        workspace: &mut Workspace,
1645        _: &mut Window,
1646        cx: &mut Context<Self>,
1647    ) {
1648        if self.terminal().read(cx).task().is_none() {
1649            if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) {
1650                log::debug!(
1651                    "Updating workspace id for the terminal, old: {old_id:?}, new: {new_id:?}",
1652                );
1653                cx.background_spawn(TERMINAL_DB.update_workspace_id(
1654                    new_id,
1655                    old_id,
1656                    cx.entity_id().as_u64(),
1657                ))
1658                .detach();
1659            }
1660            self.workspace_id = workspace.database_id();
1661        }
1662    }
1663
1664    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1665        f(*event)
1666    }
1667}
1668
1669impl SerializableItem for TerminalView {
1670    fn serialized_item_kind() -> &'static str {
1671        "Terminal"
1672    }
1673
1674    fn cleanup(
1675        workspace_id: WorkspaceId,
1676        alive_items: Vec<workspace::ItemId>,
1677        _window: &mut Window,
1678        cx: &mut App,
1679    ) -> Task<anyhow::Result<()>> {
1680        delete_unloaded_items(alive_items, workspace_id, "terminals", &TERMINAL_DB, cx)
1681    }
1682
1683    fn serialize(
1684        &mut self,
1685        _workspace: &mut Workspace,
1686        item_id: workspace::ItemId,
1687        _closing: bool,
1688        _: &mut Window,
1689        cx: &mut Context<Self>,
1690    ) -> Option<Task<anyhow::Result<()>>> {
1691        let terminal = self.terminal().read(cx);
1692        if terminal.task().is_some() {
1693            return None;
1694        }
1695
1696        if let Some((cwd, workspace_id)) = terminal.working_directory().zip(self.workspace_id) {
1697            self.cwd_serialized = true;
1698            Some(cx.background_spawn(async move {
1699                TERMINAL_DB
1700                    .save_working_directory(item_id, workspace_id, cwd)
1701                    .await
1702            }))
1703        } else {
1704            None
1705        }
1706    }
1707
1708    fn should_serialize(&self, _: &Self::Event) -> bool {
1709        !self.cwd_serialized
1710    }
1711
1712    fn deserialize(
1713        project: Entity<Project>,
1714        workspace: WeakEntity<Workspace>,
1715        workspace_id: workspace::WorkspaceId,
1716        item_id: workspace::ItemId,
1717        window: &mut Window,
1718        cx: &mut App,
1719    ) -> Task<anyhow::Result<Entity<Self>>> {
1720        let window_handle = window.window_handle();
1721        window.spawn(cx, async move |cx| {
1722            let cwd = cx
1723                .update(|_window, cx| {
1724                    let from_db = TERMINAL_DB
1725                        .get_working_directory(item_id, workspace_id)
1726                        .log_err()
1727                        .flatten();
1728                    if from_db
1729                        .as_ref()
1730                        .is_some_and(|from_db| !from_db.as_os_str().is_empty())
1731                    {
1732                        from_db
1733                    } else {
1734                        workspace
1735                            .upgrade()
1736                            .and_then(|workspace| default_working_directory(workspace.read(cx), cx))
1737                    }
1738                })
1739                .ok()
1740                .flatten();
1741
1742            let terminal = project
1743                .update(cx, |project, cx| {
1744                    project.create_terminal(TerminalKind::Shell(cwd), window_handle, cx)
1745                })?
1746                .await?;
1747            cx.update(|window, cx| {
1748                cx.new(|cx| {
1749                    TerminalView::new(
1750                        terminal,
1751                        workspace,
1752                        Some(workspace_id),
1753                        project.downgrade(),
1754                        false,
1755                        window,
1756                        cx,
1757                    )
1758                })
1759            })
1760        })
1761    }
1762}
1763
1764impl SearchableItem for TerminalView {
1765    type Match = RangeInclusive<Point>;
1766
1767    fn supported_options(&self) -> SearchOptions {
1768        SearchOptions {
1769            case: false,
1770            word: false,
1771            regex: true,
1772            replacement: false,
1773            selection: false,
1774            find_in_results: false,
1775        }
1776    }
1777
1778    /// Clear stored matches
1779    fn clear_matches(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1780        self.terminal().update(cx, |term, _| term.matches.clear())
1781    }
1782
1783    /// Store matches returned from find_matches somewhere for rendering
1784    fn update_matches(
1785        &mut self,
1786        matches: &[Self::Match],
1787        _window: &mut Window,
1788        cx: &mut Context<Self>,
1789    ) {
1790        self.terminal()
1791            .update(cx, |term, _| term.matches = matches.to_vec())
1792    }
1793
1794    /// Returns the selection content to pre-load into this search
1795    fn query_suggestion(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> String {
1796        self.terminal()
1797            .read(cx)
1798            .last_content
1799            .selection_text
1800            .clone()
1801            .unwrap_or_default()
1802    }
1803
1804    /// Focus match at given index into the Vec of matches
1805    fn activate_match(
1806        &mut self,
1807        index: usize,
1808        _: &[Self::Match],
1809        _window: &mut Window,
1810        cx: &mut Context<Self>,
1811    ) {
1812        self.terminal()
1813            .update(cx, |term, _| term.activate_match(index));
1814        cx.notify();
1815    }
1816
1817    /// Add selections for all matches given.
1818    fn select_matches(&mut self, matches: &[Self::Match], _: &mut Window, cx: &mut Context<Self>) {
1819        self.terminal()
1820            .update(cx, |term, _| term.select_matches(matches));
1821        cx.notify();
1822    }
1823
1824    /// Get all of the matches for this query, should be done on the background
1825    fn find_matches(
1826        &mut self,
1827        query: Arc<SearchQuery>,
1828        _: &mut Window,
1829        cx: &mut Context<Self>,
1830    ) -> Task<Vec<Self::Match>> {
1831        if let Some(s) = regex_search_for_query(&query) {
1832            self.terminal()
1833                .update(cx, |term, cx| term.find_matches(s, cx))
1834        } else {
1835            Task::ready(vec![])
1836        }
1837    }
1838
1839    /// Reports back to the search toolbar what the active match should be (the selection)
1840    fn active_match_index(
1841        &mut self,
1842        direction: Direction,
1843        matches: &[Self::Match],
1844        _: &mut Window,
1845        cx: &mut Context<Self>,
1846    ) -> Option<usize> {
1847        // Selection head might have a value if there's a selection that isn't
1848        // associated with a match. Therefore, if there are no matches, we should
1849        // report None, no matter the state of the terminal
1850        let res = if !matches.is_empty() {
1851            if let Some(selection_head) = self.terminal().read(cx).selection_head {
1852                // If selection head is contained in a match. Return that match
1853                match direction {
1854                    Direction::Prev => {
1855                        // If no selection before selection head, return the first match
1856                        Some(
1857                            matches
1858                                .iter()
1859                                .enumerate()
1860                                .rev()
1861                                .find(|(_, search_match)| {
1862                                    search_match.contains(&selection_head)
1863                                        || search_match.start() < &selection_head
1864                                })
1865                                .map(|(ix, _)| ix)
1866                                .unwrap_or(0),
1867                        )
1868                    }
1869                    Direction::Next => {
1870                        // If no selection after selection head, return the last match
1871                        Some(
1872                            matches
1873                                .iter()
1874                                .enumerate()
1875                                .find(|(_, search_match)| {
1876                                    search_match.contains(&selection_head)
1877                                        || search_match.start() > &selection_head
1878                                })
1879                                .map(|(ix, _)| ix)
1880                                .unwrap_or(matches.len().saturating_sub(1)),
1881                        )
1882                    }
1883                }
1884            } else {
1885                // Matches found but no active selection, return the first last one (closest to cursor)
1886                Some(matches.len().saturating_sub(1))
1887            }
1888        } else {
1889            None
1890        };
1891
1892        res
1893    }
1894    fn replace(
1895        &mut self,
1896        _: &Self::Match,
1897        _: &SearchQuery,
1898        _window: &mut Window,
1899        _: &mut Context<Self>,
1900    ) {
1901        // Replacement is not supported in terminal view, so this is a no-op.
1902    }
1903}
1904
1905///Gets the working directory for the given workspace, respecting the user's settings.
1906/// None implies "~" on whichever machine we end up on.
1907pub(crate) fn default_working_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
1908    match &TerminalSettings::get_global(cx).working_directory {
1909        WorkingDirectory::CurrentProjectDirectory => workspace
1910            .project()
1911            .read(cx)
1912            .active_project_directory(cx)
1913            .as_deref()
1914            .map(Path::to_path_buf),
1915        WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
1916        WorkingDirectory::AlwaysHome => None,
1917        WorkingDirectory::Always { directory } => {
1918            shellexpand::full(&directory) //TODO handle this better
1919                .ok()
1920                .map(|dir| Path::new(&dir.to_string()).to_path_buf())
1921                .filter(|dir| dir.is_dir())
1922        }
1923    }
1924}
1925///Gets the first project's home directory, or the home directory
1926fn first_project_directory(workspace: &Workspace, cx: &App) -> Option<PathBuf> {
1927    let worktree = workspace.worktrees(cx).next()?.read(cx);
1928    if !worktree.root_entry()?.is_dir() {
1929        return None;
1930    }
1931    Some(worktree.abs_path().to_path_buf())
1932}
1933
1934#[cfg(test)]
1935mod tests {
1936    use super::*;
1937    use gpui::TestAppContext;
1938    use project::{Entry, Project, ProjectPath, Worktree};
1939    use std::path::Path;
1940    use workspace::AppState;
1941
1942    // Working directory calculation tests
1943
1944    // No Worktrees in project -> home_dir()
1945    #[gpui::test]
1946    async fn no_worktree(cx: &mut TestAppContext) {
1947        let (project, workspace) = init_test(cx).await;
1948        cx.read(|cx| {
1949            let workspace = workspace.read(cx);
1950            let active_entry = project.read(cx).active_entry();
1951
1952            //Make sure environment is as expected
1953            assert!(active_entry.is_none());
1954            assert!(workspace.worktrees(cx).next().is_none());
1955
1956            let res = default_working_directory(workspace, cx);
1957            assert_eq!(res, None);
1958            let res = first_project_directory(workspace, cx);
1959            assert_eq!(res, None);
1960        });
1961    }
1962
1963    // No active entry, but a worktree, worktree is a file -> home_dir()
1964    #[gpui::test]
1965    async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
1966        let (project, workspace) = init_test(cx).await;
1967
1968        create_file_wt(project.clone(), "/root.txt", cx).await;
1969        cx.read(|cx| {
1970            let workspace = workspace.read(cx);
1971            let active_entry = project.read(cx).active_entry();
1972
1973            //Make sure environment is as expected
1974            assert!(active_entry.is_none());
1975            assert!(workspace.worktrees(cx).next().is_some());
1976
1977            let res = default_working_directory(workspace, cx);
1978            assert_eq!(res, None);
1979            let res = first_project_directory(workspace, cx);
1980            assert_eq!(res, None);
1981        });
1982    }
1983
1984    // No active entry, but a worktree, worktree is a folder -> worktree_folder
1985    #[gpui::test]
1986    async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
1987        let (project, workspace) = init_test(cx).await;
1988
1989        let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
1990        cx.update(|cx| {
1991            let workspace = workspace.read(cx);
1992            let active_entry = project.read(cx).active_entry();
1993
1994            assert!(active_entry.is_none());
1995            assert!(workspace.worktrees(cx).next().is_some());
1996
1997            let res = default_working_directory(workspace, cx);
1998            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
1999            let res = first_project_directory(workspace, cx);
2000            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
2001        });
2002    }
2003
2004    // Active entry with a work tree, worktree is a file -> worktree_folder()
2005    #[gpui::test]
2006    async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
2007        let (project, workspace) = init_test(cx).await;
2008
2009        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
2010        let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
2011        insert_active_entry_for(wt2, entry2, project.clone(), cx);
2012
2013        cx.update(|cx| {
2014            let workspace = workspace.read(cx);
2015            let active_entry = project.read(cx).active_entry();
2016
2017            assert!(active_entry.is_some());
2018
2019            let res = default_working_directory(workspace, cx);
2020            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
2021            let res = first_project_directory(workspace, cx);
2022            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
2023        });
2024    }
2025
2026    // Active entry, with a worktree, worktree is a folder -> worktree_folder
2027    #[gpui::test]
2028    async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
2029        let (project, workspace) = init_test(cx).await;
2030
2031        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
2032        let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
2033        insert_active_entry_for(wt2, entry2, project.clone(), cx);
2034
2035        cx.update(|cx| {
2036            let workspace = workspace.read(cx);
2037            let active_entry = project.read(cx).active_entry();
2038
2039            assert!(active_entry.is_some());
2040
2041            let res = default_working_directory(workspace, cx);
2042            assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
2043            let res = first_project_directory(workspace, cx);
2044            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
2045        });
2046    }
2047
2048    /// Creates a worktree with 1 file: /root.txt
2049    pub async fn init_test(cx: &mut TestAppContext) -> (Entity<Project>, Entity<Workspace>) {
2050        let params = cx.update(AppState::test);
2051        cx.update(|cx| {
2052            terminal::init(cx);
2053            theme::init(theme::LoadThemes::JustBase, cx);
2054            Project::init_settings(cx);
2055            language::init(cx);
2056        });
2057
2058        let project = Project::test(params.fs.clone(), [], cx).await;
2059        let workspace = cx
2060            .add_window(|window, cx| Workspace::test_new(project.clone(), window, cx))
2061            .root(cx)
2062            .unwrap();
2063
2064        (project, workspace)
2065    }
2066
2067    /// Creates a worktree with 1 folder: /root{suffix}/
2068    async fn create_folder_wt(
2069        project: Entity<Project>,
2070        path: impl AsRef<Path>,
2071        cx: &mut TestAppContext,
2072    ) -> (Entity<Worktree>, Entry) {
2073        create_wt(project, true, path, cx).await
2074    }
2075
2076    /// Creates a worktree with 1 file: /root{suffix}.txt
2077    async fn create_file_wt(
2078        project: Entity<Project>,
2079        path: impl AsRef<Path>,
2080        cx: &mut TestAppContext,
2081    ) -> (Entity<Worktree>, Entry) {
2082        create_wt(project, false, path, cx).await
2083    }
2084
2085    async fn create_wt(
2086        project: Entity<Project>,
2087        is_dir: bool,
2088        path: impl AsRef<Path>,
2089        cx: &mut TestAppContext,
2090    ) -> (Entity<Worktree>, Entry) {
2091        let (wt, _) = project
2092            .update(cx, |project, cx| {
2093                project.find_or_create_worktree(path, true, cx)
2094            })
2095            .await
2096            .unwrap();
2097
2098        let entry = cx
2099            .update(|cx| {
2100                wt.update(cx, |wt, cx| {
2101                    wt.create_entry(Path::new(""), is_dir, None, cx)
2102                })
2103            })
2104            .await
2105            .unwrap()
2106            .to_included()
2107            .unwrap();
2108
2109        (wt, entry)
2110    }
2111
2112    pub fn insert_active_entry_for(
2113        wt: Entity<Worktree>,
2114        entry: Entry,
2115        project: Entity<Project>,
2116        cx: &mut TestAppContext,
2117    ) {
2118        cx.update(|cx| {
2119            let p = ProjectPath {
2120                worktree_id: wt.read(cx).id(),
2121                path: entry.path,
2122            };
2123            project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
2124        });
2125    }
2126}