terminal_view.rs

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