terminal_view.rs

   1mod persistence;
   2pub mod terminal_element;
   3pub mod terminal_panel;
   4
   5use collections::HashSet;
   6use editor::{actions::SelectAll, scroll::Autoscroll, Editor};
   7use futures::{stream::FuturesUnordered, StreamExt};
   8use gpui::{
   9    anchored, deferred, div, impl_actions, AnyElement, AppContext, DismissEvent, EventEmitter,
  10    FocusHandle, FocusableView, KeyContext, KeyDownEvent, Keystroke, Model, MouseButton,
  11    MouseDownEvent, Pixels, Render, ScrollWheelEvent, Styled, Subscription, Task, View,
  12    VisualContext, WeakView,
  13};
  14use language::Bias;
  15use persistence::TERMINAL_DB;
  16use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project};
  17use terminal::{
  18    alacritty_terminal::{
  19        index::Point,
  20        term::{search::RegexSearch, TermMode},
  21    },
  22    terminal_settings::{CursorShape, TerminalBlink, TerminalSettings, WorkingDirectory},
  23    Clear, Copy, Event, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp, ScrollPageDown,
  24    ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskStatus, Terminal,
  25    TerminalSize, ToggleViMode,
  26};
  27use terminal_element::{is_blank, TerminalElement};
  28use terminal_panel::TerminalPanel;
  29use ui::{h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Tooltip};
  30use util::{paths::PathWithPosition, ResultExt};
  31use workspace::{
  32    item::{BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams},
  33    notifications::NotifyResultExt,
  34    register_serializable_item,
  35    searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
  36    CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,
  37    WorkspaceId,
  38};
  39
  40use anyhow::Context;
  41use serde::Deserialize;
  42use settings::{Settings, SettingsStore};
  43use smol::Timer;
  44use zed_actions::InlineAssist;
  45
  46use std::{
  47    cmp,
  48    ops::RangeInclusive,
  49    path::{Path, PathBuf},
  50    rc::Rc,
  51    sync::Arc,
  52    time::Duration,
  53};
  54
  55const REGEX_SPECIAL_CHARS: &[char] = &[
  56    '\\', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '^', '$',
  57];
  58
  59const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
  60
  61const GIT_DIFF_PATH_PREFIXES: &[char] = &['a', 'b'];
  62
  63///Event to transmit the scroll from the element to the view
  64#[derive(Clone, Debug, PartialEq)]
  65pub struct ScrollTerminal(pub i32);
  66
  67#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
  68pub struct SendText(String);
  69
  70#[derive(Clone, Debug, Default, Deserialize, PartialEq)]
  71pub struct SendKeystroke(String);
  72
  73impl_actions!(terminal, [SendText, SendKeystroke]);
  74
  75pub fn init(cx: &mut AppContext) {
  76    terminal_panel::init(cx);
  77    terminal::init(cx);
  78
  79    register_serializable_item::<TerminalView>(cx);
  80
  81    cx.observe_new_views(|workspace: &mut Workspace, _| {
  82        workspace.register_action(TerminalView::deploy);
  83    })
  84    .detach();
  85}
  86
  87pub struct BlockProperties {
  88    pub height: u8,
  89    pub render: Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>,
  90}
  91
  92pub struct BlockContext<'a, 'b> {
  93    pub context: &'b mut WindowContext<'a>,
  94    pub dimensions: TerminalSize,
  95}
  96
  97///A terminal view, maintains the PTY's file handles and communicates with the terminal
  98pub struct TerminalView {
  99    terminal: Model<Terminal>,
 100    workspace: WeakView<Workspace>,
 101    focus_handle: FocusHandle,
 102    //Currently using iTerm bell, show bell emoji in tab until input is received
 103    has_bell: bool,
 104    context_menu: Option<(View<ContextMenu>, gpui::Point<Pixels>, Subscription)>,
 105    cursor_shape: CursorShape,
 106    blink_state: bool,
 107    blinking_terminal_enabled: bool,
 108    blinking_paused: bool,
 109    blink_epoch: usize,
 110    can_navigate_to_selected_word: bool,
 111    workspace_id: Option<WorkspaceId>,
 112    show_breadcrumbs: bool,
 113    block_below_cursor: Option<Rc<BlockProperties>>,
 114    scroll_top: Pixels,
 115    _subscriptions: Vec<Subscription>,
 116    _terminal_subscriptions: Vec<Subscription>,
 117}
 118
 119impl EventEmitter<Event> for TerminalView {}
 120impl EventEmitter<ItemEvent> for TerminalView {}
 121impl EventEmitter<SearchEvent> for TerminalView {}
 122
 123impl FocusableView for TerminalView {
 124    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
 125        self.focus_handle.clone()
 126    }
 127}
 128
 129impl TerminalView {
 130    ///Create a new Terminal in the current working directory or the user's home directory
 131    pub fn deploy(
 132        workspace: &mut Workspace,
 133        _: &NewCenterTerminal,
 134        cx: &mut ViewContext<Workspace>,
 135    ) {
 136        let working_directory = default_working_directory(workspace, cx);
 137
 138        let window = cx.window_handle();
 139        let project = workspace.project().downgrade();
 140        cx.spawn(move |workspace, mut cx| async move {
 141            let terminal = project
 142                .update(&mut cx, |project, cx| {
 143                    project.create_terminal(TerminalKind::Shell(working_directory), window, cx)
 144                })
 145                .ok()?
 146                .await;
 147            let terminal = workspace
 148                .update(&mut cx, |workspace, cx| terminal.notify_err(workspace, cx))
 149                .ok()
 150                .flatten()?;
 151
 152            workspace
 153                .update(&mut cx, |workspace, cx| {
 154                    let view = cx.new_view(|cx| {
 155                        TerminalView::new(
 156                            terminal,
 157                            workspace.weak_handle(),
 158                            workspace.database_id(),
 159                            cx,
 160                        )
 161                    });
 162                    workspace.add_item_to_active_pane(Box::new(view), None, true, cx);
 163                })
 164                .ok();
 165
 166            Some(())
 167        })
 168        .detach()
 169    }
 170
 171    pub fn new(
 172        terminal: Model<Terminal>,
 173        workspace: WeakView<Workspace>,
 174        workspace_id: Option<WorkspaceId>,
 175        cx: &mut ViewContext<Self>,
 176    ) -> Self {
 177        let workspace_handle = workspace.clone();
 178        let terminal_subscriptions = subscribe_for_terminal_events(&terminal, workspace, cx);
 179
 180        let focus_handle = cx.focus_handle();
 181        let focus_in = cx.on_focus_in(&focus_handle, |terminal_view, cx| {
 182            terminal_view.focus_in(cx);
 183        });
 184        let focus_out = cx.on_focus_out(&focus_handle, |terminal_view, _event, cx| {
 185            terminal_view.focus_out(cx);
 186        });
 187        let cursor_shape = TerminalSettings::get_global(cx)
 188            .cursor_shape
 189            .unwrap_or_default();
 190
 191        Self {
 192            terminal,
 193            workspace: workspace_handle,
 194            has_bell: false,
 195            focus_handle,
 196            context_menu: None,
 197            cursor_shape,
 198            blink_state: true,
 199            blinking_terminal_enabled: false,
 200            blinking_paused: false,
 201            blink_epoch: 0,
 202            can_navigate_to_selected_word: false,
 203            workspace_id,
 204            show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,
 205            block_below_cursor: None,
 206            scroll_top: Pixels::ZERO,
 207            _subscriptions: vec![
 208                focus_in,
 209                focus_out,
 210                cx.observe_global::<SettingsStore>(Self::settings_changed),
 211            ],
 212            _terminal_subscriptions: terminal_subscriptions,
 213        }
 214    }
 215
 216    pub fn model(&self) -> &Model<Terminal> {
 217        &self.terminal
 218    }
 219
 220    pub fn has_bell(&self) -> bool {
 221        self.has_bell
 222    }
 223
 224    pub fn clear_bell(&mut self, cx: &mut ViewContext<TerminalView>) {
 225        self.has_bell = false;
 226        cx.emit(Event::Wakeup);
 227    }
 228
 229    pub fn deploy_context_menu(
 230        &mut self,
 231        position: gpui::Point<Pixels>,
 232        cx: &mut ViewContext<Self>,
 233    ) {
 234        let assistant_enabled = self
 235            .workspace
 236            .upgrade()
 237            .and_then(|workspace| workspace.read(cx).panel::<TerminalPanel>(cx))
 238            .map_or(false, |terminal_panel| {
 239                terminal_panel.read(cx).assistant_enabled()
 240            });
 241        let context_menu = ContextMenu::build(cx, |menu, _| {
 242            menu.context(self.focus_handle.clone())
 243                .action("New Terminal", Box::new(NewTerminal))
 244                .separator()
 245                .action("Copy", Box::new(Copy))
 246                .action("Paste", Box::new(Paste))
 247                .action("Select All", Box::new(SelectAll))
 248                .action("Clear", Box::new(Clear))
 249                .when(assistant_enabled, |menu| {
 250                    menu.separator()
 251                        .action("Inline Assist", Box::new(InlineAssist::default()))
 252                })
 253                .separator()
 254                .action("Close", Box::new(CloseActiveItem { save_intent: None }))
 255        });
 256
 257        cx.focus_view(&context_menu);
 258        let subscription =
 259            cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
 260                if this.context_menu.as_ref().is_some_and(|context_menu| {
 261                    context_menu.0.focus_handle(cx).contains_focused(cx)
 262                }) {
 263                    cx.focus_self();
 264                }
 265                this.context_menu.take();
 266                cx.notify();
 267            });
 268
 269        self.context_menu = Some((context_menu, position, subscription));
 270    }
 271
 272    fn settings_changed(&mut self, cx: &mut ViewContext<Self>) {
 273        let settings = TerminalSettings::get_global(cx);
 274        self.show_breadcrumbs = settings.toolbar.breadcrumbs;
 275
 276        let new_cursor_shape = settings.cursor_shape.unwrap_or_default();
 277        let old_cursor_shape = self.cursor_shape;
 278        if old_cursor_shape != new_cursor_shape {
 279            self.cursor_shape = new_cursor_shape;
 280            self.terminal.update(cx, |term, _| {
 281                term.set_cursor_shape(self.cursor_shape);
 282            });
 283        }
 284
 285        cx.notify();
 286    }
 287
 288    fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
 289        if self
 290            .terminal
 291            .read(cx)
 292            .last_content
 293            .mode
 294            .contains(TermMode::ALT_SCREEN)
 295        {
 296            self.terminal.update(cx, |term, cx| {
 297                term.try_keystroke(
 298                    &Keystroke::parse("ctrl-cmd-space").unwrap(),
 299                    TerminalSettings::get_global(cx).option_as_meta,
 300                )
 301            });
 302        } else {
 303            cx.show_character_palette();
 304        }
 305    }
 306
 307    fn select_all(&mut self, _: &SelectAll, cx: &mut ViewContext<Self>) {
 308        self.terminal.update(cx, |term, _| term.select_all());
 309        cx.notify();
 310    }
 311
 312    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
 313        self.scroll_top = px(0.);
 314        self.terminal.update(cx, |term, _| term.clear());
 315        cx.notify();
 316    }
 317
 318    fn max_scroll_top(&self, cx: &AppContext) -> Pixels {
 319        let terminal = self.terminal.read(cx);
 320
 321        let Some(block) = self.block_below_cursor.as_ref() else {
 322            return Pixels::ZERO;
 323        };
 324
 325        let line_height = terminal.last_content().size.line_height;
 326        let mut terminal_lines = terminal.total_lines();
 327        let viewport_lines = terminal.viewport_lines();
 328        if terminal.total_lines() == terminal.viewport_lines() {
 329            let mut last_line = None;
 330            for cell in terminal.last_content.cells.iter().rev() {
 331                if !is_blank(cell) {
 332                    break;
 333                }
 334
 335                let last_line = last_line.get_or_insert(cell.point.line);
 336                if *last_line != cell.point.line {
 337                    terminal_lines -= 1;
 338                }
 339                *last_line = cell.point.line;
 340            }
 341        }
 342
 343        let max_scroll_top_in_lines =
 344            (block.height as usize).saturating_sub(viewport_lines.saturating_sub(terminal_lines));
 345
 346        max_scroll_top_in_lines as f32 * line_height
 347    }
 348
 349    fn scroll_wheel(
 350        &mut self,
 351        event: &ScrollWheelEvent,
 352        origin: gpui::Point<Pixels>,
 353        cx: &mut ViewContext<Self>,
 354    ) {
 355        let terminal_content = self.terminal.read(cx).last_content();
 356
 357        if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
 358            let line_height = terminal_content.size.line_height;
 359            let y_delta = event.delta.pixel_delta(line_height).y;
 360            if y_delta < Pixels::ZERO || self.scroll_top > Pixels::ZERO {
 361                self.scroll_top = cmp::max(
 362                    Pixels::ZERO,
 363                    cmp::min(self.scroll_top - y_delta, self.max_scroll_top(cx)),
 364                );
 365                cx.notify();
 366                return;
 367            }
 368        }
 369
 370        self.terminal
 371            .update(cx, |term, _| term.scroll_wheel(event, origin));
 372    }
 373
 374    fn scroll_line_up(&mut self, _: &ScrollLineUp, cx: &mut ViewContext<Self>) {
 375        let terminal_content = self.terminal.read(cx).last_content();
 376        if self.block_below_cursor.is_some()
 377            && terminal_content.display_offset == 0
 378            && self.scroll_top > Pixels::ZERO
 379        {
 380            let line_height = terminal_content.size.line_height;
 381            self.scroll_top = cmp::max(self.scroll_top - line_height, Pixels::ZERO);
 382            return;
 383        }
 384
 385        self.terminal.update(cx, |term, _| term.scroll_line_up());
 386        cx.notify();
 387    }
 388
 389    fn scroll_line_down(&mut self, _: &ScrollLineDown, cx: &mut ViewContext<Self>) {
 390        let terminal_content = self.terminal.read(cx).last_content();
 391        if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {
 392            let max_scroll_top = self.max_scroll_top(cx);
 393            if self.scroll_top < max_scroll_top {
 394                let line_height = terminal_content.size.line_height;
 395                self.scroll_top = cmp::min(self.scroll_top + line_height, max_scroll_top);
 396            }
 397            return;
 398        }
 399
 400        self.terminal.update(cx, |term, _| term.scroll_line_down());
 401        cx.notify();
 402    }
 403
 404    fn scroll_page_up(&mut self, _: &ScrollPageUp, cx: &mut ViewContext<Self>) {
 405        if self.scroll_top == Pixels::ZERO {
 406            self.terminal.update(cx, |term, _| term.scroll_page_up());
 407        } else {
 408            let line_height = self.terminal.read(cx).last_content.size.line_height();
 409            let visible_block_lines = (self.scroll_top / line_height) as usize;
 410            let viewport_lines = self.terminal.read(cx).viewport_lines();
 411            let visible_content_lines = viewport_lines - visible_block_lines;
 412
 413            if visible_block_lines >= viewport_lines {
 414                self.scroll_top = ((visible_block_lines - viewport_lines) as f32) * line_height;
 415            } else {
 416                self.scroll_top = px(0.);
 417                self.terminal
 418                    .update(cx, |term, _| term.scroll_up_by(visible_content_lines));
 419            }
 420        }
 421        cx.notify();
 422    }
 423
 424    fn scroll_page_down(&mut self, _: &ScrollPageDown, cx: &mut ViewContext<Self>) {
 425        self.terminal.update(cx, |term, _| term.scroll_page_down());
 426        let terminal = self.terminal.read(cx);
 427        if terminal.last_content().display_offset < terminal.viewport_lines() {
 428            self.scroll_top = self.max_scroll_top(cx);
 429        }
 430        cx.notify();
 431    }
 432
 433    fn scroll_to_top(&mut self, _: &ScrollToTop, cx: &mut ViewContext<Self>) {
 434        self.terminal.update(cx, |term, _| term.scroll_to_top());
 435        cx.notify();
 436    }
 437
 438    fn scroll_to_bottom(&mut self, _: &ScrollToBottom, cx: &mut ViewContext<Self>) {
 439        self.terminal.update(cx, |term, _| term.scroll_to_bottom());
 440        if self.block_below_cursor.is_some() {
 441            self.scroll_top = self.max_scroll_top(cx);
 442        }
 443        cx.notify();
 444    }
 445
 446    fn toggle_vi_mode(&mut self, _: &ToggleViMode, cx: &mut ViewContext<Self>) {
 447        self.terminal.update(cx, |term, _| term.toggle_vi_mode());
 448        cx.notify();
 449    }
 450
 451    pub fn should_show_cursor(&self, focused: bool, cx: &mut gpui::ViewContext<Self>) -> bool {
 452        //Don't blink the cursor when not focused, blinking is disabled, or paused
 453        if !focused
 454            || self.blinking_paused
 455            || self
 456                .terminal
 457                .read(cx)
 458                .last_content
 459                .mode
 460                .contains(TermMode::ALT_SCREEN)
 461        {
 462            return true;
 463        }
 464
 465        match TerminalSettings::get_global(cx).blinking {
 466            //If the user requested to never blink, don't blink it.
 467            TerminalBlink::Off => true,
 468            //If the terminal is controlling it, check terminal mode
 469            TerminalBlink::TerminalControlled => {
 470                !self.blinking_terminal_enabled || self.blink_state
 471            }
 472            TerminalBlink::On => self.blink_state,
 473        }
 474    }
 475
 476    fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
 477        if epoch == self.blink_epoch && !self.blinking_paused {
 478            self.blink_state = !self.blink_state;
 479            cx.notify();
 480
 481            let epoch = self.next_blink_epoch();
 482            cx.spawn(|this, mut cx| async move {
 483                Timer::after(CURSOR_BLINK_INTERVAL).await;
 484                this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx))
 485                    .ok();
 486            })
 487            .detach();
 488        }
 489    }
 490
 491    pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
 492        self.blink_state = true;
 493        cx.notify();
 494
 495        let epoch = self.next_blink_epoch();
 496        cx.spawn(|this, mut cx| async move {
 497            Timer::after(CURSOR_BLINK_INTERVAL).await;
 498            this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
 499                .ok();
 500        })
 501        .detach();
 502    }
 503
 504    pub fn terminal(&self) -> &Model<Terminal> {
 505        &self.terminal
 506    }
 507
 508    pub fn set_block_below_cursor(&mut self, block: BlockProperties, cx: &mut ViewContext<Self>) {
 509        self.block_below_cursor = Some(Rc::new(block));
 510        self.scroll_to_bottom(&ScrollToBottom, cx);
 511        cx.notify();
 512    }
 513
 514    pub fn clear_block_below_cursor(&mut self, cx: &mut ViewContext<Self>) {
 515        self.block_below_cursor = None;
 516        self.scroll_top = Pixels::ZERO;
 517        cx.notify();
 518    }
 519
 520    fn next_blink_epoch(&mut self) -> usize {
 521        self.blink_epoch += 1;
 522        self.blink_epoch
 523    }
 524
 525    fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
 526        if epoch == self.blink_epoch {
 527            self.blinking_paused = false;
 528            self.blink_cursors(epoch, cx);
 529        }
 530    }
 531
 532    ///Attempt to paste the clipboard into the terminal
 533    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
 534        self.terminal.update(cx, |term, _| term.copy());
 535        cx.notify();
 536    }
 537
 538    ///Attempt to paste the clipboard into the terminal
 539    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
 540        if let Some(clipboard_string) = cx.read_from_clipboard().and_then(|item| item.text()) {
 541            self.terminal
 542                .update(cx, |terminal, _cx| terminal.paste(&clipboard_string));
 543        }
 544    }
 545
 546    fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
 547        self.clear_bell(cx);
 548        self.terminal.update(cx, |term, _| {
 549            term.input(text.0.to_string());
 550        });
 551    }
 552
 553    fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
 554        if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
 555            self.clear_bell(cx);
 556            self.terminal.update(cx, |term, cx| {
 557                term.try_keystroke(&keystroke, TerminalSettings::get_global(cx).option_as_meta);
 558            });
 559        }
 560    }
 561
 562    fn dispatch_context(&self, cx: &AppContext) -> KeyContext {
 563        let mut dispatch_context = KeyContext::new_with_defaults();
 564        dispatch_context.add("Terminal");
 565
 566        let mode = self.terminal.read(cx).last_content.mode;
 567        dispatch_context.set(
 568            "screen",
 569            if mode.contains(TermMode::ALT_SCREEN) {
 570                "alt"
 571            } else {
 572                "normal"
 573            },
 574        );
 575
 576        if mode.contains(TermMode::APP_CURSOR) {
 577            dispatch_context.add("DECCKM");
 578        }
 579        if mode.contains(TermMode::APP_KEYPAD) {
 580            dispatch_context.add("DECPAM");
 581        } else {
 582            dispatch_context.add("DECPNM");
 583        }
 584        if mode.contains(TermMode::SHOW_CURSOR) {
 585            dispatch_context.add("DECTCEM");
 586        }
 587        if mode.contains(TermMode::LINE_WRAP) {
 588            dispatch_context.add("DECAWM");
 589        }
 590        if mode.contains(TermMode::ORIGIN) {
 591            dispatch_context.add("DECOM");
 592        }
 593        if mode.contains(TermMode::INSERT) {
 594            dispatch_context.add("IRM");
 595        }
 596        //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
 597        if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
 598            dispatch_context.add("LNM");
 599        }
 600        if mode.contains(TermMode::FOCUS_IN_OUT) {
 601            dispatch_context.add("report_focus");
 602        }
 603        if mode.contains(TermMode::ALTERNATE_SCROLL) {
 604            dispatch_context.add("alternate_scroll");
 605        }
 606        if mode.contains(TermMode::BRACKETED_PASTE) {
 607            dispatch_context.add("bracketed_paste");
 608        }
 609        if mode.intersects(TermMode::MOUSE_MODE) {
 610            dispatch_context.add("any_mouse_reporting");
 611        }
 612        {
 613            let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
 614                "click"
 615            } else if mode.contains(TermMode::MOUSE_DRAG) {
 616                "drag"
 617            } else if mode.contains(TermMode::MOUSE_MOTION) {
 618                "motion"
 619            } else {
 620                "off"
 621            };
 622            dispatch_context.set("mouse_reporting", mouse_reporting);
 623        }
 624        {
 625            let format = if mode.contains(TermMode::SGR_MOUSE) {
 626                "sgr"
 627            } else if mode.contains(TermMode::UTF8_MOUSE) {
 628                "utf8"
 629            } else {
 630                "normal"
 631            };
 632            dispatch_context.set("mouse_format", format);
 633        };
 634        dispatch_context
 635    }
 636
 637    fn set_terminal(&mut self, terminal: Model<Terminal>, cx: &mut ViewContext<'_, TerminalView>) {
 638        self._terminal_subscriptions =
 639            subscribe_for_terminal_events(&terminal, self.workspace.clone(), cx);
 640        self.terminal = terminal;
 641    }
 642}
 643
 644fn subscribe_for_terminal_events(
 645    terminal: &Model<Terminal>,
 646    workspace: WeakView<Workspace>,
 647    cx: &mut ViewContext<'_, TerminalView>,
 648) -> Vec<Subscription> {
 649    let terminal_subscription = cx.observe(terminal, |_, _, cx| cx.notify());
 650    let terminal_events_subscription =
 651        cx.subscribe(terminal, move |this, _, event, cx| match event {
 652            Event::Wakeup => {
 653                cx.notify();
 654                cx.emit(Event::Wakeup);
 655                cx.emit(ItemEvent::UpdateTab);
 656                cx.emit(SearchEvent::MatchesInvalidated);
 657            }
 658
 659            Event::Bell => {
 660                this.has_bell = true;
 661                cx.emit(Event::Wakeup);
 662            }
 663
 664            Event::BlinkChanged(blinking) => {
 665                if matches!(
 666                    TerminalSettings::get_global(cx).blinking,
 667                    TerminalBlink::TerminalControlled
 668                ) {
 669                    this.blinking_terminal_enabled = *blinking;
 670                }
 671            }
 672
 673            Event::TitleChanged => {
 674                cx.emit(ItemEvent::UpdateTab);
 675            }
 676
 677            Event::NewNavigationTarget(maybe_navigation_target) => {
 678                this.can_navigate_to_selected_word = match maybe_navigation_target {
 679                    Some(MaybeNavigationTarget::Url(_)) => true,
 680                    Some(MaybeNavigationTarget::PathLike(path_like_target)) => {
 681                        if let Ok(fs) = workspace.update(cx, |workspace, cx| {
 682                            workspace.project().read(cx).fs().clone()
 683                        }) {
 684                            let valid_files_to_open_task = possible_open_targets(
 685                                fs,
 686                                &workspace,
 687                                &path_like_target.terminal_dir,
 688                                &path_like_target.maybe_path,
 689                                cx,
 690                            );
 691                            !smol::block_on(valid_files_to_open_task).is_empty()
 692                        } else {
 693                            false
 694                        }
 695                    }
 696                    None => false,
 697                }
 698            }
 699
 700            Event::Open(maybe_navigation_target) => match maybe_navigation_target {
 701                MaybeNavigationTarget::Url(url) => cx.open_url(url),
 702
 703                MaybeNavigationTarget::PathLike(path_like_target) => {
 704                    if !this.can_navigate_to_selected_word {
 705                        return;
 706                    }
 707                    let task_workspace = workspace.clone();
 708                    let Some(fs) = workspace
 709                        .update(cx, |workspace, cx| {
 710                            workspace.project().read(cx).fs().clone()
 711                        })
 712                        .ok()
 713                    else {
 714                        return;
 715                    };
 716
 717                    let path_like_target = path_like_target.clone();
 718                    cx.spawn(|terminal_view, mut cx| async move {
 719                        let valid_files_to_open = terminal_view
 720                            .update(&mut cx, |_, cx| {
 721                                possible_open_targets(
 722                                    fs,
 723                                    &task_workspace,
 724                                    &path_like_target.terminal_dir,
 725                                    &path_like_target.maybe_path,
 726                                    cx,
 727                                )
 728                            })?
 729                            .await;
 730                        let paths_to_open = valid_files_to_open
 731                            .iter()
 732                            .map(|(p, _)| p.path.clone())
 733                            .collect();
 734                        let opened_items = task_workspace
 735                            .update(&mut cx, |workspace, cx| {
 736                                workspace.open_paths(
 737                                    paths_to_open,
 738                                    OpenVisible::OnlyDirectories,
 739                                    None,
 740                                    cx,
 741                                )
 742                            })
 743                            .context("workspace update")?
 744                            .await;
 745
 746                        let mut has_dirs = false;
 747                        for ((path, metadata), opened_item) in valid_files_to_open
 748                            .into_iter()
 749                            .zip(opened_items.into_iter())
 750                        {
 751                            if metadata.is_dir {
 752                                has_dirs = true;
 753                            } else if let Some(Ok(opened_item)) = opened_item {
 754                                if let Some(row) = path.row {
 755                                    let col = path.column.unwrap_or(0);
 756                                    if let Some(active_editor) = opened_item.downcast::<Editor>() {
 757                                        active_editor
 758                                            .downgrade()
 759                                            .update(&mut cx, |editor, cx| {
 760                                                let snapshot = editor.snapshot(cx).display_snapshot;
 761                                                let point = snapshot.buffer_snapshot.clip_point(
 762                                                    language::Point::new(
 763                                                        row.saturating_sub(1),
 764                                                        col.saturating_sub(1),
 765                                                    ),
 766                                                    Bias::Left,
 767                                                );
 768                                                editor.change_selections(
 769                                                    Some(Autoscroll::center()),
 770                                                    cx,
 771                                                    |s| s.select_ranges([point..point]),
 772                                                );
 773                                            })
 774                                            .log_err();
 775                                    }
 776                                }
 777                            }
 778                        }
 779
 780                        if has_dirs {
 781                            task_workspace.update(&mut cx, |workspace, cx| {
 782                                workspace.project().update(cx, |_, cx| {
 783                                    cx.emit(project::Event::ActivateProjectPanel);
 784                                })
 785                            })?;
 786                        }
 787
 788                        anyhow::Ok(())
 789                    })
 790                    .detach_and_log_err(cx)
 791                }
 792            },
 793            Event::BreadcrumbsChanged => cx.emit(ItemEvent::UpdateBreadcrumbs),
 794            Event::CloseTerminal => cx.emit(ItemEvent::CloseItem),
 795            Event::SelectionsChanged => {
 796                cx.invalidate_character_coordinates();
 797                cx.emit(SearchEvent::ActiveMatchChanged)
 798            }
 799        });
 800    vec![terminal_subscription, terminal_events_subscription]
 801}
 802
 803fn possible_open_paths_metadata(
 804    fs: Arc<dyn Fs>,
 805    row: Option<u32>,
 806    column: Option<u32>,
 807    potential_paths: HashSet<PathBuf>,
 808    cx: &mut ViewContext<TerminalView>,
 809) -> Task<Vec<(PathWithPosition, Metadata)>> {
 810    cx.background_executor().spawn(async move {
 811        let mut paths_with_metadata = Vec::with_capacity(potential_paths.len());
 812
 813        let mut fetch_metadata_tasks = potential_paths
 814            .into_iter()
 815            .map(|potential_path| async {
 816                let metadata = fs.metadata(&potential_path).await.ok().flatten();
 817                (
 818                    PathWithPosition {
 819                        path: potential_path,
 820                        row,
 821                        column,
 822                    },
 823                    metadata,
 824                )
 825            })
 826            .collect::<FuturesUnordered<_>>();
 827
 828        while let Some((path, metadata)) = fetch_metadata_tasks.next().await {
 829            if let Some(metadata) = metadata {
 830                paths_with_metadata.push((path, metadata));
 831            }
 832        }
 833
 834        paths_with_metadata
 835    })
 836}
 837
 838fn possible_open_targets(
 839    fs: Arc<dyn Fs>,
 840    workspace: &WeakView<Workspace>,
 841    cwd: &Option<PathBuf>,
 842    maybe_path: &String,
 843    cx: &mut ViewContext<TerminalView>,
 844) -> Task<Vec<(PathWithPosition, Metadata)>> {
 845    let path_position = PathWithPosition::parse_str(maybe_path.as_str());
 846    let row = path_position.row;
 847    let column = path_position.column;
 848    let maybe_path = path_position.path;
 849
 850    let abs_path = if maybe_path.is_absolute() {
 851        Some(maybe_path)
 852    } else if maybe_path.starts_with("~") {
 853        maybe_path
 854            .strip_prefix("~")
 855            .ok()
 856            .and_then(|maybe_path| Some(dirs::home_dir()?.join(maybe_path)))
 857    } else {
 858        let mut potential_cwd_and_workspace_paths = HashSet::default();
 859        if let Some(cwd) = cwd {
 860            let abs_path = Path::join(cwd, &maybe_path);
 861            let canonicalized_path = abs_path.canonicalize().unwrap_or(abs_path);
 862            potential_cwd_and_workspace_paths.insert(canonicalized_path);
 863        }
 864        if let Some(workspace) = workspace.upgrade() {
 865            workspace.update(cx, |workspace, cx| {
 866                for potential_worktree_path in workspace
 867                    .worktrees(cx)
 868                    .map(|worktree| worktree.read(cx).abs_path().join(&maybe_path))
 869                {
 870                    potential_cwd_and_workspace_paths.insert(potential_worktree_path);
 871                }
 872
 873                for prefix in GIT_DIFF_PATH_PREFIXES {
 874                    let prefix_str = &prefix.to_string();
 875                    if maybe_path.starts_with(prefix_str) {
 876                        let stripped = maybe_path.strip_prefix(prefix_str).unwrap_or(&maybe_path);
 877                        for potential_worktree_path in workspace
 878                            .worktrees(cx)
 879                            .map(|worktree| worktree.read(cx).abs_path().join(&stripped))
 880                        {
 881                            potential_cwd_and_workspace_paths.insert(potential_worktree_path);
 882                        }
 883                    }
 884                }
 885            });
 886        }
 887
 888        return possible_open_paths_metadata(
 889            fs,
 890            row,
 891            column,
 892            potential_cwd_and_workspace_paths,
 893            cx,
 894        );
 895    };
 896
 897    let canonicalized_paths = match abs_path {
 898        Some(abs_path) => match abs_path.canonicalize() {
 899            Ok(path) => HashSet::from_iter([path]),
 900            Err(_) => HashSet::default(),
 901        },
 902        None => HashSet::default(),
 903    };
 904
 905    possible_open_paths_metadata(fs, row, column, canonicalized_paths, cx)
 906}
 907
 908fn regex_to_literal(regex: &str) -> String {
 909    regex
 910        .chars()
 911        .flat_map(|c| {
 912            if REGEX_SPECIAL_CHARS.contains(&c) {
 913                vec!['\\', c]
 914            } else {
 915                vec![c]
 916            }
 917        })
 918        .collect()
 919}
 920
 921pub fn regex_search_for_query(query: &project::search::SearchQuery) -> Option<RegexSearch> {
 922    let query = query.as_str();
 923    if query == "." {
 924        return None;
 925    }
 926    let searcher = RegexSearch::new(query);
 927    searcher.ok()
 928}
 929
 930impl TerminalView {
 931    fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) {
 932        self.clear_bell(cx);
 933        self.pause_cursor_blinking(cx);
 934
 935        self.terminal.update(cx, |term, cx| {
 936            let handled = term.try_keystroke(
 937                &event.keystroke,
 938                TerminalSettings::get_global(cx).option_as_meta,
 939            );
 940            if handled {
 941                cx.stop_propagation();
 942            }
 943        });
 944    }
 945
 946    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 947        self.terminal.update(cx, |terminal, _| {
 948            terminal.set_cursor_shape(self.cursor_shape);
 949            terminal.focus_in();
 950        });
 951        self.blink_cursors(self.blink_epoch, cx);
 952        cx.invalidate_character_coordinates();
 953        cx.notify();
 954    }
 955
 956    fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
 957        self.terminal.update(cx, |terminal, _| {
 958            terminal.focus_out();
 959            terminal.set_cursor_shape(CursorShape::Hollow);
 960        });
 961        cx.notify();
 962    }
 963}
 964
 965impl Render for TerminalView {
 966    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 967        let terminal_handle = self.terminal.clone();
 968        let terminal_view_handle = cx.view().clone();
 969
 970        let focused = self.focus_handle.is_focused(cx);
 971
 972        div()
 973            .size_full()
 974            .relative()
 975            .track_focus(&self.focus_handle(cx))
 976            .key_context(self.dispatch_context(cx))
 977            .on_action(cx.listener(TerminalView::send_text))
 978            .on_action(cx.listener(TerminalView::send_keystroke))
 979            .on_action(cx.listener(TerminalView::copy))
 980            .on_action(cx.listener(TerminalView::paste))
 981            .on_action(cx.listener(TerminalView::clear))
 982            .on_action(cx.listener(TerminalView::scroll_line_up))
 983            .on_action(cx.listener(TerminalView::scroll_line_down))
 984            .on_action(cx.listener(TerminalView::scroll_page_up))
 985            .on_action(cx.listener(TerminalView::scroll_page_down))
 986            .on_action(cx.listener(TerminalView::scroll_to_top))
 987            .on_action(cx.listener(TerminalView::scroll_to_bottom))
 988            .on_action(cx.listener(TerminalView::toggle_vi_mode))
 989            .on_action(cx.listener(TerminalView::show_character_palette))
 990            .on_action(cx.listener(TerminalView::select_all))
 991            .on_key_down(cx.listener(Self::key_down))
 992            .on_mouse_down(
 993                MouseButton::Right,
 994                cx.listener(|this, event: &MouseDownEvent, cx| {
 995                    if !this.terminal.read(cx).mouse_mode(event.modifiers.shift) {
 996                        this.deploy_context_menu(event.position, cx);
 997                        cx.notify();
 998                    }
 999                }),
1000            )
1001            .child(
1002                // TODO: Oddly this wrapper div is needed for TerminalElement to not steal events from the context menu
1003                div().size_full().child(TerminalElement::new(
1004                    terminal_handle,
1005                    terminal_view_handle,
1006                    self.workspace.clone(),
1007                    self.focus_handle.clone(),
1008                    focused,
1009                    self.should_show_cursor(focused, cx),
1010                    self.can_navigate_to_selected_word,
1011                    self.block_below_cursor.clone(),
1012                )),
1013            )
1014            .children(self.context_menu.as_ref().map(|(menu, position, _)| {
1015                deferred(
1016                    anchored()
1017                        .position(*position)
1018                        .anchor(gpui::AnchorCorner::TopLeft)
1019                        .child(menu.clone()),
1020                )
1021                .with_priority(1)
1022            }))
1023    }
1024}
1025
1026impl Item for TerminalView {
1027    type Event = ItemEvent;
1028
1029    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<SharedString> {
1030        Some(self.terminal().read(cx).title(false).into())
1031    }
1032
1033    fn tab_content(&self, params: TabContentParams, cx: &WindowContext) -> AnyElement {
1034        let terminal = self.terminal().read(cx);
1035        let title = terminal.title(true);
1036        let rerun_button = |task_id: task::TaskId| {
1037            IconButton::new("rerun-icon", IconName::Rerun)
1038                .icon_size(IconSize::Small)
1039                .size(ButtonSize::Compact)
1040                .icon_color(Color::Default)
1041                .shape(ui::IconButtonShape::Square)
1042                .tooltip(|cx| Tooltip::text("Rerun task", cx))
1043                .on_click(move |_, cx| {
1044                    cx.dispatch_action(Box::new(zed_actions::Rerun {
1045                        task_id: Some(task_id.0.clone()),
1046                        allow_concurrent_runs: Some(true),
1047                        use_new_terminal: Some(false),
1048                        reevaluate_context: false,
1049                    }));
1050                })
1051        };
1052
1053        let (icon, icon_color, rerun_button) = match terminal.task() {
1054            Some(terminal_task) => match &terminal_task.status {
1055                TaskStatus::Running => (
1056                    IconName::Play,
1057                    Color::Disabled,
1058                    Some(rerun_button(terminal_task.id.clone())),
1059                ),
1060                TaskStatus::Unknown => (
1061                    IconName::Warning,
1062                    Color::Warning,
1063                    Some(rerun_button(terminal_task.id.clone())),
1064                ),
1065                TaskStatus::Completed { success } => {
1066                    let rerun_button = rerun_button(terminal_task.id.clone());
1067                    if *success {
1068                        (IconName::Check, Color::Success, Some(rerun_button))
1069                    } else {
1070                        (IconName::XCircle, Color::Error, Some(rerun_button))
1071                    }
1072                }
1073            },
1074            None => (IconName::Terminal, Color::Muted, None),
1075        };
1076
1077        h_flex()
1078            .gap_1()
1079            .group("term-tab-icon")
1080            .child(
1081                h_flex()
1082                    .group("term-tab-icon")
1083                    .child(
1084                        div()
1085                            .when(rerun_button.is_some(), |this| {
1086                                this.hover(|style| style.invisible().w_0())
1087                            })
1088                            .child(Icon::new(icon).color(icon_color)),
1089                    )
1090                    .when_some(rerun_button, |this, rerun_button| {
1091                        this.child(
1092                            div()
1093                                .absolute()
1094                                .visible_on_hover("term-tab-icon")
1095                                .child(rerun_button),
1096                        )
1097                    }),
1098            )
1099            .child(Label::new(title).color(params.text_color()))
1100            .into_any()
1101    }
1102
1103    fn telemetry_event_text(&self) -> Option<&'static str> {
1104        None
1105    }
1106
1107    fn clone_on_split(
1108        &self,
1109        _workspace_id: Option<WorkspaceId>,
1110        _cx: &mut ViewContext<Self>,
1111    ) -> Option<View<Self>> {
1112        //From what I can tell, there's no  way to tell the current working
1113        //Directory of the terminal from outside the shell. There might be
1114        //solutions to this, but they are non-trivial and require more IPC
1115
1116        // Some(TerminalContainer::new(
1117        //     Err(anyhow::anyhow!("failed to instantiate terminal")),
1118        //     workspace_id,
1119        //     cx,
1120        // ))
1121
1122        // TODO
1123        None
1124    }
1125
1126    fn is_dirty(&self, cx: &gpui::AppContext) -> bool {
1127        match self.terminal.read(cx).task() {
1128            Some(task) => task.status == TaskStatus::Running,
1129            None => self.has_bell(),
1130        }
1131    }
1132
1133    fn has_conflict(&self, _cx: &AppContext) -> bool {
1134        false
1135    }
1136
1137    fn is_singleton(&self, _cx: &AppContext) -> bool {
1138        true
1139    }
1140
1141    fn as_searchable(&self, handle: &View<Self>) -> Option<Box<dyn SearchableItemHandle>> {
1142        Some(Box::new(handle.clone()))
1143    }
1144
1145    fn breadcrumb_location(&self, cx: &AppContext) -> ToolbarItemLocation {
1146        if self.show_breadcrumbs && !self.terminal().read(cx).breadcrumb_text.trim().is_empty() {
1147            ToolbarItemLocation::PrimaryLeft
1148        } else {
1149            ToolbarItemLocation::Hidden
1150        }
1151    }
1152
1153    fn breadcrumbs(&self, _: &theme::Theme, cx: &AppContext) -> Option<Vec<BreadcrumbText>> {
1154        Some(vec![BreadcrumbText {
1155            text: self.terminal().read(cx).breadcrumb_text.clone(),
1156            highlights: None,
1157            font: None,
1158        }])
1159    }
1160
1161    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
1162        if self.terminal().read(cx).task().is_none() {
1163            if let Some((new_id, old_id)) = workspace.database_id().zip(self.workspace_id) {
1164                cx.background_executor()
1165                    .spawn(TERMINAL_DB.update_workspace_id(new_id, old_id, cx.entity_id().as_u64()))
1166                    .detach();
1167            }
1168            self.workspace_id = workspace.database_id();
1169        }
1170    }
1171
1172    fn to_item_events(event: &Self::Event, mut f: impl FnMut(ItemEvent)) {
1173        f(*event)
1174    }
1175}
1176
1177impl SerializableItem for TerminalView {
1178    fn serialized_item_kind() -> &'static str {
1179        "Terminal"
1180    }
1181
1182    fn cleanup(
1183        workspace_id: WorkspaceId,
1184        alive_items: Vec<workspace::ItemId>,
1185        cx: &mut WindowContext,
1186    ) -> Task<gpui::Result<()>> {
1187        cx.spawn(|_| TERMINAL_DB.delete_unloaded_items(workspace_id, alive_items))
1188    }
1189
1190    fn serialize(
1191        &mut self,
1192        _workspace: &mut Workspace,
1193        item_id: workspace::ItemId,
1194        _closing: bool,
1195        cx: &mut ViewContext<Self>,
1196    ) -> Option<Task<gpui::Result<()>>> {
1197        let terminal = self.terminal().read(cx);
1198        if terminal.task().is_some() {
1199            return None;
1200        }
1201
1202        if let Some((cwd, workspace_id)) = terminal.working_directory().zip(self.workspace_id) {
1203            Some(cx.background_executor().spawn(async move {
1204                TERMINAL_DB
1205                    .save_working_directory(item_id, workspace_id, cwd)
1206                    .await
1207            }))
1208        } else {
1209            None
1210        }
1211    }
1212
1213    fn should_serialize(&self, event: &Self::Event) -> bool {
1214        matches!(event, ItemEvent::UpdateTab)
1215    }
1216
1217    fn deserialize(
1218        project: Model<Project>,
1219        workspace: WeakView<Workspace>,
1220        workspace_id: workspace::WorkspaceId,
1221        item_id: workspace::ItemId,
1222        cx: &mut WindowContext,
1223    ) -> Task<anyhow::Result<View<Self>>> {
1224        let window = cx.window_handle();
1225        cx.spawn(|mut cx| async move {
1226            let cwd = cx
1227                .update(|cx| {
1228                    let from_db = TERMINAL_DB
1229                        .get_working_directory(item_id, workspace_id)
1230                        .log_err()
1231                        .flatten();
1232                    if from_db
1233                        .as_ref()
1234                        .is_some_and(|from_db| !from_db.as_os_str().is_empty())
1235                    {
1236                        from_db
1237                    } else {
1238                        workspace
1239                            .upgrade()
1240                            .and_then(|workspace| default_working_directory(workspace.read(cx), cx))
1241                    }
1242                })
1243                .ok()
1244                .flatten();
1245
1246            let terminal = project
1247                .update(&mut cx, |project, cx| {
1248                    project.create_terminal(TerminalKind::Shell(cwd), window, cx)
1249                })?
1250                .await?;
1251            cx.update(|cx| {
1252                cx.new_view(|cx| TerminalView::new(terminal, workspace, Some(workspace_id), cx))
1253            })
1254        })
1255    }
1256}
1257
1258impl SearchableItem for TerminalView {
1259    type Match = RangeInclusive<Point>;
1260
1261    fn supported_options() -> SearchOptions {
1262        SearchOptions {
1263            case: false,
1264            word: false,
1265            regex: true,
1266            replacement: false,
1267            selection: false,
1268        }
1269    }
1270
1271    /// Clear stored matches
1272    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
1273        self.terminal().update(cx, |term, _| term.matches.clear())
1274    }
1275
1276    /// Store matches returned from find_matches somewhere for rendering
1277    fn update_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
1278        self.terminal()
1279            .update(cx, |term, _| term.matches = matches.to_vec())
1280    }
1281
1282    /// Returns the selection content to pre-load into this search
1283    fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
1284        self.terminal()
1285            .read(cx)
1286            .last_content
1287            .selection_text
1288            .clone()
1289            .unwrap_or_default()
1290    }
1291
1292    /// Focus match at given index into the Vec of matches
1293    fn activate_match(&mut self, index: usize, _: &[Self::Match], cx: &mut ViewContext<Self>) {
1294        self.terminal()
1295            .update(cx, |term, _| term.activate_match(index));
1296        cx.notify();
1297    }
1298
1299    /// Add selections for all matches given.
1300    fn select_matches(&mut self, matches: &[Self::Match], cx: &mut ViewContext<Self>) {
1301        self.terminal()
1302            .update(cx, |term, _| term.select_matches(matches));
1303        cx.notify();
1304    }
1305
1306    /// Get all of the matches for this query, should be done on the background
1307    fn find_matches(
1308        &mut self,
1309        query: Arc<SearchQuery>,
1310        cx: &mut ViewContext<Self>,
1311    ) -> Task<Vec<Self::Match>> {
1312        let searcher = match &*query {
1313            SearchQuery::Text { .. } => regex_search_for_query(
1314                &(SearchQuery::text(
1315                    regex_to_literal(query.as_str()),
1316                    query.whole_word(),
1317                    query.case_sensitive(),
1318                    query.include_ignored(),
1319                    query.files_to_include().clone(),
1320                    query.files_to_exclude().clone(),
1321                    None,
1322                )
1323                .unwrap()),
1324            ),
1325            SearchQuery::Regex { .. } => regex_search_for_query(&query),
1326        };
1327
1328        if let Some(s) = searcher {
1329            self.terminal()
1330                .update(cx, |term, cx| term.find_matches(s, cx))
1331        } else {
1332            Task::ready(vec![])
1333        }
1334    }
1335
1336    /// Reports back to the search toolbar what the active match should be (the selection)
1337    fn active_match_index(
1338        &mut self,
1339        matches: &[Self::Match],
1340        cx: &mut ViewContext<Self>,
1341    ) -> Option<usize> {
1342        // Selection head might have a value if there's a selection that isn't
1343        // associated with a match. Therefore, if there are no matches, we should
1344        // report None, no matter the state of the terminal
1345        let res = if !matches.is_empty() {
1346            if let Some(selection_head) = self.terminal().read(cx).selection_head {
1347                // If selection head is contained in a match. Return that match
1348                if let Some(ix) = matches
1349                    .iter()
1350                    .enumerate()
1351                    .find(|(_, search_match)| {
1352                        search_match.contains(&selection_head)
1353                            || search_match.start() > &selection_head
1354                    })
1355                    .map(|(ix, _)| ix)
1356                {
1357                    Some(ix)
1358                } else {
1359                    // If no selection after selection head, return the last match
1360                    Some(matches.len().saturating_sub(1))
1361                }
1362            } else {
1363                // Matches found but no active selection, return the first last one (closest to cursor)
1364                Some(matches.len().saturating_sub(1))
1365            }
1366        } else {
1367            None
1368        };
1369
1370        res
1371    }
1372    fn replace(&mut self, _: &Self::Match, _: &SearchQuery, _: &mut ViewContext<Self>) {
1373        // Replacement is not supported in terminal view, so this is a no-op.
1374    }
1375}
1376
1377///Gets the working directory for the given workspace, respecting the user's settings.
1378/// None implies "~" on whichever machine we end up on.
1379pub(crate) fn default_working_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
1380    match &TerminalSettings::get_global(cx).working_directory {
1381        WorkingDirectory::CurrentProjectDirectory => workspace
1382            .project()
1383            .read(cx)
1384            .active_project_directory(cx)
1385            .as_deref()
1386            .map(Path::to_path_buf),
1387        WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
1388        WorkingDirectory::AlwaysHome => None,
1389        WorkingDirectory::Always { directory } => {
1390            shellexpand::full(&directory) //TODO handle this better
1391                .ok()
1392                .map(|dir| Path::new(&dir.to_string()).to_path_buf())
1393                .filter(|dir| dir.is_dir())
1394        }
1395    }
1396}
1397///Gets the first project's home directory, or the home directory
1398fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
1399    let worktree = workspace.worktrees(cx).next()?.read(cx);
1400    if !worktree.root_entry()?.is_dir() {
1401        return None;
1402    }
1403    Some(worktree.abs_path().to_path_buf())
1404}
1405
1406#[cfg(test)]
1407mod tests {
1408    use super::*;
1409    use gpui::TestAppContext;
1410    use project::{Entry, Project, ProjectPath, Worktree};
1411    use std::path::Path;
1412    use workspace::AppState;
1413
1414    // Working directory calculation tests
1415
1416    // No Worktrees in project -> home_dir()
1417    #[gpui::test]
1418    async fn no_worktree(cx: &mut TestAppContext) {
1419        let (project, workspace) = init_test(cx).await;
1420        cx.read(|cx| {
1421            let workspace = workspace.read(cx);
1422            let active_entry = project.read(cx).active_entry();
1423
1424            //Make sure environment is as expected
1425            assert!(active_entry.is_none());
1426            assert!(workspace.worktrees(cx).next().is_none());
1427
1428            let res = default_working_directory(workspace, cx);
1429            assert_eq!(res, None);
1430            let res = first_project_directory(workspace, cx);
1431            assert_eq!(res, None);
1432        });
1433    }
1434
1435    // No active entry, but a worktree, worktree is a file -> home_dir()
1436    #[gpui::test]
1437    async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
1438        let (project, workspace) = init_test(cx).await;
1439
1440        create_file_wt(project.clone(), "/root.txt", cx).await;
1441        cx.read(|cx| {
1442            let workspace = workspace.read(cx);
1443            let active_entry = project.read(cx).active_entry();
1444
1445            //Make sure environment is as expected
1446            assert!(active_entry.is_none());
1447            assert!(workspace.worktrees(cx).next().is_some());
1448
1449            let res = default_working_directory(workspace, cx);
1450            assert_eq!(res, None);
1451            let res = first_project_directory(workspace, cx);
1452            assert_eq!(res, None);
1453        });
1454    }
1455
1456    // No active entry, but a worktree, worktree is a folder -> worktree_folder
1457    #[gpui::test]
1458    async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
1459        let (project, workspace) = init_test(cx).await;
1460
1461        let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
1462        cx.update(|cx| {
1463            let workspace = workspace.read(cx);
1464            let active_entry = project.read(cx).active_entry();
1465
1466            assert!(active_entry.is_none());
1467            assert!(workspace.worktrees(cx).next().is_some());
1468
1469            let res = default_working_directory(workspace, cx);
1470            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
1471            let res = first_project_directory(workspace, cx);
1472            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
1473        });
1474    }
1475
1476    // Active entry with a work tree, worktree is a file -> worktree_folder()
1477    #[gpui::test]
1478    async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
1479        let (project, workspace) = init_test(cx).await;
1480
1481        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
1482        let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
1483        insert_active_entry_for(wt2, entry2, project.clone(), cx);
1484
1485        cx.update(|cx| {
1486            let workspace = workspace.read(cx);
1487            let active_entry = project.read(cx).active_entry();
1488
1489            assert!(active_entry.is_some());
1490
1491            let res = default_working_directory(workspace, cx);
1492            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
1493            let res = first_project_directory(workspace, cx);
1494            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
1495        });
1496    }
1497
1498    // Active entry, with a worktree, worktree is a folder -> worktree_folder
1499    #[gpui::test]
1500    async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
1501        let (project, workspace) = init_test(cx).await;
1502
1503        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
1504        let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
1505        insert_active_entry_for(wt2, entry2, project.clone(), cx);
1506
1507        cx.update(|cx| {
1508            let workspace = workspace.read(cx);
1509            let active_entry = project.read(cx).active_entry();
1510
1511            assert!(active_entry.is_some());
1512
1513            let res = default_working_directory(workspace, cx);
1514            assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
1515            let res = first_project_directory(workspace, cx);
1516            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
1517        });
1518    }
1519
1520    /// Creates a worktree with 1 file: /root.txt
1521    pub async fn init_test(cx: &mut TestAppContext) -> (Model<Project>, View<Workspace>) {
1522        let params = cx.update(AppState::test);
1523        cx.update(|cx| {
1524            terminal::init(cx);
1525            theme::init(theme::LoadThemes::JustBase, cx);
1526            Project::init_settings(cx);
1527            language::init(cx);
1528        });
1529
1530        let project = Project::test(params.fs.clone(), [], cx).await;
1531        let workspace = cx
1532            .add_window(|cx| Workspace::test_new(project.clone(), cx))
1533            .root_view(cx)
1534            .unwrap();
1535
1536        (project, workspace)
1537    }
1538
1539    /// Creates a worktree with 1 folder: /root{suffix}/
1540    async fn create_folder_wt(
1541        project: Model<Project>,
1542        path: impl AsRef<Path>,
1543        cx: &mut TestAppContext,
1544    ) -> (Model<Worktree>, Entry) {
1545        create_wt(project, true, path, cx).await
1546    }
1547
1548    /// Creates a worktree with 1 file: /root{suffix}.txt
1549    async fn create_file_wt(
1550        project: Model<Project>,
1551        path: impl AsRef<Path>,
1552        cx: &mut TestAppContext,
1553    ) -> (Model<Worktree>, Entry) {
1554        create_wt(project, false, path, cx).await
1555    }
1556
1557    async fn create_wt(
1558        project: Model<Project>,
1559        is_dir: bool,
1560        path: impl AsRef<Path>,
1561        cx: &mut TestAppContext,
1562    ) -> (Model<Worktree>, Entry) {
1563        let (wt, _) = project
1564            .update(cx, |project, cx| {
1565                project.find_or_create_worktree(path, true, cx)
1566            })
1567            .await
1568            .unwrap();
1569
1570        let entry = cx
1571            .update(|cx| wt.update(cx, |wt, cx| wt.create_entry(Path::new(""), is_dir, cx)))
1572            .await
1573            .unwrap()
1574            .to_included()
1575            .unwrap();
1576
1577        (wt, entry)
1578    }
1579
1580    pub fn insert_active_entry_for(
1581        wt: Model<Worktree>,
1582        entry: Entry,
1583        project: Model<Project>,
1584        cx: &mut TestAppContext,
1585    ) {
1586        cx.update(|cx| {
1587            let p = ProjectPath {
1588                worktree_id: wt.read(cx).id(),
1589                path: entry.path,
1590            };
1591            project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
1592        });
1593    }
1594
1595    #[test]
1596    fn escapes_only_special_characters() {
1597        assert_eq!(regex_to_literal(r"test(\w)"), r"test\(\\w\)".to_string());
1598    }
1599
1600    #[test]
1601    fn empty_string_stays_empty() {
1602        assert_eq!(regex_to_literal(""), "".to_string());
1603    }
1604}