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