terminal_view.rs

   1mod persistence;
   2pub mod terminal_button;
   3pub mod terminal_element;
   4
   5use std::{
   6    borrow::Cow,
   7    ops::RangeInclusive,
   8    path::{Path, PathBuf},
   9    time::Duration,
  10};
  11
  12use context_menu::{ContextMenu, ContextMenuItem};
  13use dirs::home_dir;
  14use gpui::{
  15    actions,
  16    elements::{AnchorCorner, ChildView, Flex, Label, ParentElement, Stack, Text},
  17    geometry::vector::Vector2F,
  18    impl_actions, impl_internal_actions,
  19    keymap_matcher::{KeymapContext, Keystroke},
  20    platform::KeyDownEvent,
  21    AnyViewHandle, AppContext, Element, ElementBox, Entity, ModelHandle, Task, View, ViewContext,
  22    ViewHandle, WeakViewHandle,
  23};
  24use project::{LocalWorktree, Project};
  25use serde::Deserialize;
  26use settings::{Settings, TerminalBlink, WorkingDirectory};
  27use smallvec::{smallvec, SmallVec};
  28use smol::Timer;
  29use terminal::{
  30    alacritty_terminal::{
  31        index::Point,
  32        term::{search::RegexSearch, TermMode},
  33    },
  34    Event, Terminal,
  35};
  36use util::ResultExt;
  37use workspace::{
  38    item::{Item, ItemEvent},
  39    notifications::NotifyResultExt,
  40    pane, register_deserializable_item,
  41    searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},
  42    Pane, ToolbarItemLocation, Workspace, WorkspaceId,
  43};
  44
  45use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement};
  46
  47const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500);
  48
  49///Event to transmit the scroll from the element to the view
  50#[derive(Clone, Debug, PartialEq)]
  51pub struct ScrollTerminal(pub i32);
  52
  53#[derive(Clone, PartialEq)]
  54pub struct DeployContextMenu {
  55    pub position: Vector2F,
  56}
  57
  58#[derive(Clone, Default, Deserialize, PartialEq)]
  59pub struct SendText(String);
  60
  61#[derive(Clone, Default, Deserialize, PartialEq)]
  62pub struct SendKeystroke(String);
  63
  64actions!(
  65    terminal,
  66    [Clear, Copy, Paste, ShowCharacterPalette, SearchTest]
  67);
  68
  69impl_actions!(terminal, [SendText, SendKeystroke]);
  70
  71impl_internal_actions!(project_panel, [DeployContextMenu]);
  72
  73pub fn init(cx: &mut AppContext) {
  74    cx.add_action(TerminalView::deploy);
  75
  76    register_deserializable_item::<TerminalView>(cx);
  77
  78    //Useful terminal views
  79    cx.add_action(TerminalView::send_text);
  80    cx.add_action(TerminalView::send_keystroke);
  81    cx.add_action(TerminalView::deploy_context_menu);
  82    cx.add_action(TerminalView::copy);
  83    cx.add_action(TerminalView::paste);
  84    cx.add_action(TerminalView::clear);
  85    cx.add_action(TerminalView::show_character_palette);
  86}
  87
  88///A terminal view, maintains the PTY's file handles and communicates with the terminal
  89pub struct TerminalView {
  90    terminal: ModelHandle<Terminal>,
  91    has_new_content: bool,
  92    //Currently using iTerm bell, show bell emoji in tab until input is received
  93    has_bell: bool,
  94    context_menu: ViewHandle<ContextMenu>,
  95    blink_state: bool,
  96    blinking_on: bool,
  97    blinking_paused: bool,
  98    blink_epoch: usize,
  99    workspace_id: WorkspaceId,
 100}
 101
 102impl Entity for TerminalView {
 103    type Event = Event;
 104}
 105
 106impl TerminalView {
 107    ///Create a new Terminal in the current working directory or the user's home directory
 108    pub fn deploy(
 109        workspace: &mut Workspace,
 110        _: &workspace::NewTerminal,
 111        cx: &mut ViewContext<Workspace>,
 112    ) {
 113        let strategy = cx.global::<Settings>().terminal_strategy();
 114
 115        let working_directory = get_working_directory(workspace, cx, strategy);
 116
 117        let window_id = cx.window_id();
 118        let terminal = workspace
 119            .project()
 120            .update(cx, |project, cx| {
 121                project.create_terminal(working_directory, window_id, cx)
 122            })
 123            .notify_err(workspace, cx);
 124
 125        if let Some(terminal) = terminal {
 126            let view = cx.add_view(|cx| TerminalView::new(terminal, workspace.database_id(), cx));
 127            workspace.add_item(Box::new(view), cx)
 128        }
 129    }
 130
 131    pub fn new(
 132        terminal: ModelHandle<Terminal>,
 133        workspace_id: WorkspaceId,
 134        cx: &mut ViewContext<Self>,
 135    ) -> Self {
 136        cx.observe(&terminal, |_, _, cx| cx.notify()).detach();
 137        cx.subscribe(&terminal, |this, _, event, cx| match event {
 138            Event::Wakeup => {
 139                if !cx.is_self_focused() {
 140                    this.has_new_content = true;
 141                    cx.notify();
 142                }
 143                cx.emit(Event::Wakeup);
 144            }
 145            Event::Bell => {
 146                this.has_bell = true;
 147                cx.emit(Event::Wakeup);
 148            }
 149            Event::BlinkChanged => this.blinking_on = !this.blinking_on,
 150            Event::TitleChanged => {
 151                if let Some(foreground_info) = &this.terminal().read(cx).foreground_process_info {
 152                    let cwd = foreground_info.cwd.clone();
 153
 154                    let item_id = cx.view_id();
 155                    let workspace_id = this.workspace_id;
 156                    cx.background()
 157                        .spawn(async move {
 158                            TERMINAL_DB
 159                                .save_working_directory(item_id, workspace_id, cwd)
 160                                .await
 161                                .log_err();
 162                        })
 163                        .detach();
 164                }
 165            }
 166            _ => cx.emit(*event),
 167        })
 168        .detach();
 169
 170        Self {
 171            terminal,
 172            has_new_content: true,
 173            has_bell: false,
 174            context_menu: cx.add_view(ContextMenu::new),
 175            blink_state: true,
 176            blinking_on: false,
 177            blinking_paused: false,
 178            blink_epoch: 0,
 179            workspace_id,
 180        }
 181    }
 182
 183    pub fn model(&self) -> &ModelHandle<Terminal> {
 184        &self.terminal
 185    }
 186
 187    pub fn has_new_content(&self) -> bool {
 188        self.has_new_content
 189    }
 190
 191    pub fn has_bell(&self) -> bool {
 192        self.has_bell
 193    }
 194
 195    pub fn clear_bel(&mut self, cx: &mut ViewContext<TerminalView>) {
 196        self.has_bell = false;
 197        cx.emit(Event::Wakeup);
 198    }
 199
 200    pub fn deploy_context_menu(&mut self, action: &DeployContextMenu, cx: &mut ViewContext<Self>) {
 201        let menu_entries = vec![
 202            ContextMenuItem::item("Clear", Clear),
 203            ContextMenuItem::item("Close", pane::CloseActiveItem),
 204        ];
 205
 206        self.context_menu.update(cx, |menu, cx| {
 207            menu.show(action.position, AnchorCorner::TopLeft, menu_entries, cx)
 208        });
 209
 210        cx.notify();
 211    }
 212
 213    fn show_character_palette(&mut self, _: &ShowCharacterPalette, cx: &mut ViewContext<Self>) {
 214        if !self
 215            .terminal
 216            .read(cx)
 217            .last_content
 218            .mode
 219            .contains(TermMode::ALT_SCREEN)
 220        {
 221            cx.show_character_palette();
 222        } else {
 223            self.terminal.update(cx, |term, cx| {
 224                term.try_keystroke(
 225                    &Keystroke::parse("ctrl-cmd-space").unwrap(),
 226                    cx.global::<Settings>()
 227                        .terminal_overrides
 228                        .option_as_meta
 229                        .unwrap_or(false),
 230                )
 231            });
 232        }
 233    }
 234
 235    fn clear(&mut self, _: &Clear, cx: &mut ViewContext<Self>) {
 236        self.terminal.update(cx, |term, _| term.clear());
 237        cx.notify();
 238    }
 239
 240    pub fn should_show_cursor(
 241        &self,
 242        focused: bool,
 243        cx: &mut gpui::RenderContext<'_, Self>,
 244    ) -> bool {
 245        //Don't blink the cursor when not focused, blinking is disabled, or paused
 246        if !focused
 247            || !self.blinking_on
 248            || self.blinking_paused
 249            || self
 250                .terminal
 251                .read(cx)
 252                .last_content
 253                .mode
 254                .contains(TermMode::ALT_SCREEN)
 255        {
 256            return true;
 257        }
 258
 259        let setting = {
 260            let settings = cx.global::<Settings>();
 261            settings
 262                .terminal_overrides
 263                .blinking
 264                .clone()
 265                .unwrap_or(TerminalBlink::TerminalControlled)
 266        };
 267
 268        match setting {
 269            //If the user requested to never blink, don't blink it.
 270            TerminalBlink::Off => true,
 271            //If the terminal is controlling it, check terminal mode
 272            TerminalBlink::TerminalControlled | TerminalBlink::On => self.blink_state,
 273        }
 274    }
 275
 276    fn blink_cursors(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
 277        if epoch == self.blink_epoch && !self.blinking_paused {
 278            self.blink_state = !self.blink_state;
 279            cx.notify();
 280
 281            let epoch = self.next_blink_epoch();
 282            cx.spawn(|this, mut cx| {
 283                let this = this.downgrade();
 284                async move {
 285                    Timer::after(CURSOR_BLINK_INTERVAL).await;
 286                    if let Some(this) = this.upgrade(&cx) {
 287                        this.update(&mut cx, |this, cx| this.blink_cursors(epoch, cx));
 288                    }
 289                }
 290            })
 291            .detach();
 292        }
 293    }
 294
 295    pub fn pause_cursor_blinking(&mut self, cx: &mut ViewContext<Self>) {
 296        self.blink_state = true;
 297        cx.notify();
 298
 299        let epoch = self.next_blink_epoch();
 300        cx.spawn(|this, mut cx| {
 301            let this = this.downgrade();
 302            async move {
 303                Timer::after(CURSOR_BLINK_INTERVAL).await;
 304                if let Some(this) = this.upgrade(&cx) {
 305                    this.update(&mut cx, |this, cx| this.resume_cursor_blinking(epoch, cx))
 306                }
 307            }
 308        })
 309        .detach();
 310    }
 311
 312    pub fn find_matches(
 313        &mut self,
 314        query: project::search::SearchQuery,
 315        cx: &mut ViewContext<Self>,
 316    ) -> Task<Vec<RangeInclusive<Point>>> {
 317        let searcher = regex_search_for_query(query);
 318
 319        if let Some(searcher) = searcher {
 320            self.terminal
 321                .update(cx, |term, cx| term.find_matches(searcher, cx))
 322        } else {
 323            cx.background().spawn(async { Vec::new() })
 324        }
 325    }
 326
 327    pub fn terminal(&self) -> &ModelHandle<Terminal> {
 328        &self.terminal
 329    }
 330
 331    fn next_blink_epoch(&mut self) -> usize {
 332        self.blink_epoch += 1;
 333        self.blink_epoch
 334    }
 335
 336    fn resume_cursor_blinking(&mut self, epoch: usize, cx: &mut ViewContext<Self>) {
 337        if epoch == self.blink_epoch {
 338            self.blinking_paused = false;
 339            self.blink_cursors(epoch, cx);
 340        }
 341    }
 342
 343    ///Attempt to paste the clipboard into the terminal
 344    fn copy(&mut self, _: &Copy, cx: &mut ViewContext<Self>) {
 345        self.terminal.update(cx, |term, _| term.copy())
 346    }
 347
 348    ///Attempt to paste the clipboard into the terminal
 349    fn paste(&mut self, _: &Paste, cx: &mut ViewContext<Self>) {
 350        if let Some(item) = cx.read_from_clipboard() {
 351            self.terminal
 352                .update(cx, |terminal, _cx| terminal.paste(item.text()));
 353        }
 354    }
 355
 356    fn send_text(&mut self, text: &SendText, cx: &mut ViewContext<Self>) {
 357        self.clear_bel(cx);
 358        self.terminal.update(cx, |term, _| {
 359            term.input(text.0.to_string());
 360        });
 361    }
 362
 363    fn send_keystroke(&mut self, text: &SendKeystroke, cx: &mut ViewContext<Self>) {
 364        if let Some(keystroke) = Keystroke::parse(&text.0).log_err() {
 365            self.clear_bel(cx);
 366            self.terminal.update(cx, |term, cx| {
 367                term.try_keystroke(
 368                    &keystroke,
 369                    cx.global::<Settings>()
 370                        .terminal_overrides
 371                        .option_as_meta
 372                        .unwrap_or(false),
 373                );
 374            });
 375        }
 376    }
 377}
 378
 379pub fn regex_search_for_query(query: project::search::SearchQuery) -> Option<RegexSearch> {
 380    let searcher = match query {
 381        project::search::SearchQuery::Text { query, .. } => RegexSearch::new(&query),
 382        project::search::SearchQuery::Regex { query, .. } => RegexSearch::new(&query),
 383    };
 384    searcher.ok()
 385}
 386
 387impl View for TerminalView {
 388    fn ui_name() -> &'static str {
 389        "Terminal"
 390    }
 391
 392    fn render(&mut self, cx: &mut gpui::RenderContext<'_, Self>) -> ElementBox {
 393        let terminal_handle = self.terminal.clone().downgrade();
 394
 395        let self_id = cx.view_id();
 396        let focused = cx
 397            .focused_view_id(cx.window_id())
 398            .filter(|view_id| *view_id == self_id)
 399            .is_some();
 400
 401        Stack::new()
 402            .with_child(
 403                TerminalElement::new(
 404                    cx.handle(),
 405                    terminal_handle,
 406                    focused,
 407                    self.should_show_cursor(focused, cx),
 408                )
 409                .contained()
 410                .boxed(),
 411            )
 412            .with_child(ChildView::new(&self.context_menu, cx).boxed())
 413            .boxed()
 414    }
 415
 416    fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
 417        self.has_new_content = false;
 418        self.terminal.read(cx).focus_in();
 419        self.blink_cursors(self.blink_epoch, cx);
 420        cx.notify();
 421    }
 422
 423    fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext<Self>) {
 424        self.terminal.update(cx, |terminal, _| {
 425            terminal.focus_out();
 426        });
 427        cx.notify();
 428    }
 429
 430    fn key_down(&mut self, event: &KeyDownEvent, cx: &mut ViewContext<Self>) -> bool {
 431        self.clear_bel(cx);
 432        self.pause_cursor_blinking(cx);
 433
 434        self.terminal.update(cx, |term, cx| {
 435            term.try_keystroke(
 436                &event.keystroke,
 437                cx.global::<Settings>()
 438                    .terminal_overrides
 439                    .option_as_meta
 440                    .unwrap_or(false),
 441            )
 442        })
 443    }
 444
 445    //IME stuff
 446    fn selected_text_range(&self, cx: &AppContext) -> Option<std::ops::Range<usize>> {
 447        if self
 448            .terminal
 449            .read(cx)
 450            .last_content
 451            .mode
 452            .contains(TermMode::ALT_SCREEN)
 453        {
 454            None
 455        } else {
 456            Some(0..0)
 457        }
 458    }
 459
 460    fn replace_text_in_range(
 461        &mut self,
 462        _: Option<std::ops::Range<usize>>,
 463        text: &str,
 464        cx: &mut ViewContext<Self>,
 465    ) {
 466        self.terminal.update(cx, |terminal, _| {
 467            terminal.input(text.into());
 468        });
 469    }
 470
 471    fn keymap_context(&self, cx: &gpui::AppContext) -> KeymapContext {
 472        let mut context = Self::default_keymap_context();
 473
 474        let mode = self.terminal.read(cx).last_content.mode;
 475        context.add_key(
 476            "screen",
 477            if mode.contains(TermMode::ALT_SCREEN) {
 478                "alt"
 479            } else {
 480                "normal"
 481            },
 482        );
 483
 484        if mode.contains(TermMode::APP_CURSOR) {
 485            context.add_identifier("DECCKM");
 486        }
 487        if mode.contains(TermMode::APP_KEYPAD) {
 488            context.add_identifier("DECPAM");
 489        } else {
 490            context.add_identifier("DECPNM");
 491        }
 492        if mode.contains(TermMode::SHOW_CURSOR) {
 493            context.add_identifier("DECTCEM");
 494        }
 495        if mode.contains(TermMode::LINE_WRAP) {
 496            context.add_identifier("DECAWM");
 497        }
 498        if mode.contains(TermMode::ORIGIN) {
 499            context.add_identifier("DECOM");
 500        }
 501        if mode.contains(TermMode::INSERT) {
 502            context.add_identifier("IRM");
 503        }
 504        //LNM is apparently the name for this. https://vt100.net/docs/vt510-rm/LNM.html
 505        if mode.contains(TermMode::LINE_FEED_NEW_LINE) {
 506            context.add_identifier("LNM");
 507        }
 508        if mode.contains(TermMode::FOCUS_IN_OUT) {
 509            context.add_identifier("report_focus");
 510        }
 511        if mode.contains(TermMode::ALTERNATE_SCROLL) {
 512            context.add_identifier("alternate_scroll");
 513        }
 514        if mode.contains(TermMode::BRACKETED_PASTE) {
 515            context.add_identifier("bracketed_paste");
 516        }
 517        if mode.intersects(TermMode::MOUSE_MODE) {
 518            context.add_identifier("any_mouse_reporting");
 519        }
 520        {
 521            let mouse_reporting = if mode.contains(TermMode::MOUSE_REPORT_CLICK) {
 522                "click"
 523            } else if mode.contains(TermMode::MOUSE_DRAG) {
 524                "drag"
 525            } else if mode.contains(TermMode::MOUSE_MOTION) {
 526                "motion"
 527            } else {
 528                "off"
 529            };
 530            context.add_key("mouse_reporting", mouse_reporting);
 531        }
 532        {
 533            let format = if mode.contains(TermMode::SGR_MOUSE) {
 534                "sgr"
 535            } else if mode.contains(TermMode::UTF8_MOUSE) {
 536                "utf8"
 537            } else {
 538                "normal"
 539            };
 540            context.add_key("mouse_format", format);
 541        }
 542        context
 543    }
 544}
 545
 546impl Item for TerminalView {
 547    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
 548        Some(self.terminal().read(cx).title().into())
 549    }
 550
 551    fn tab_content(
 552        &self,
 553        _detail: Option<usize>,
 554        tab_theme: &theme::Tab,
 555        cx: &gpui::AppContext,
 556    ) -> ElementBox {
 557        let title = self.terminal().read(cx).title();
 558
 559        Flex::row()
 560            .with_child(
 561                gpui::elements::Svg::new("icons/terminal_12.svg")
 562                    .with_color(tab_theme.label.text.color)
 563                    .constrained()
 564                    .with_width(tab_theme.type_icon_width)
 565                    .aligned()
 566                    .contained()
 567                    .with_margin_right(tab_theme.spacing)
 568                    .boxed(),
 569            )
 570            .with_child(Label::new(title, tab_theme.label.clone()).aligned().boxed())
 571            .boxed()
 572    }
 573
 574    fn clone_on_split(
 575        &self,
 576        _workspace_id: WorkspaceId,
 577        _cx: &mut ViewContext<Self>,
 578    ) -> Option<Self> {
 579        //From what I can tell, there's no  way to tell the current working
 580        //Directory of the terminal from outside the shell. There might be
 581        //solutions to this, but they are non-trivial and require more IPC
 582
 583        // Some(TerminalContainer::new(
 584        //     Err(anyhow::anyhow!("failed to instantiate terminal")),
 585        //     workspace_id,
 586        //     cx,
 587        // ))
 588
 589        // TODO
 590        None
 591    }
 592
 593    fn is_dirty(&self, _cx: &gpui::AppContext) -> bool {
 594        self.has_bell()
 595    }
 596
 597    fn has_conflict(&self, _cx: &AppContext) -> bool {
 598        false
 599    }
 600
 601    fn as_searchable(&self, handle: &ViewHandle<Self>) -> Option<Box<dyn SearchableItemHandle>> {
 602        Some(Box::new(handle.clone()))
 603    }
 604
 605    fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> {
 606        match event {
 607            Event::BreadcrumbsChanged => smallvec![ItemEvent::UpdateBreadcrumbs],
 608            Event::TitleChanged | Event::Wakeup => smallvec![ItemEvent::UpdateTab],
 609            Event::CloseTerminal => smallvec![ItemEvent::CloseItem],
 610            _ => smallvec![],
 611        }
 612    }
 613
 614    fn breadcrumb_location(&self) -> ToolbarItemLocation {
 615        ToolbarItemLocation::PrimaryLeft { flex: None }
 616    }
 617
 618    fn breadcrumbs(&self, theme: &theme::Theme, cx: &AppContext) -> Option<Vec<ElementBox>> {
 619        Some(vec![Text::new(
 620            self.terminal().read(cx).breadcrumb_text.clone(),
 621            theme.workspace.breadcrumbs.default.text.clone(),
 622        )
 623        .boxed()])
 624    }
 625
 626    fn serialized_item_kind() -> Option<&'static str> {
 627        Some("Terminal")
 628    }
 629
 630    fn deserialize(
 631        project: ModelHandle<Project>,
 632        workspace: WeakViewHandle<Workspace>,
 633        workspace_id: workspace::WorkspaceId,
 634        item_id: workspace::ItemId,
 635        cx: &mut ViewContext<Pane>,
 636    ) -> Task<anyhow::Result<ViewHandle<Self>>> {
 637        let window_id = cx.window_id();
 638        cx.spawn(|pane, mut cx| async move {
 639            let cwd = TERMINAL_DB
 640                .get_working_directory(item_id, workspace_id)
 641                .log_err()
 642                .flatten()
 643                .or_else(|| {
 644                    cx.read(|cx| {
 645                        let strategy = cx.global::<Settings>().terminal_strategy();
 646                        workspace
 647                            .upgrade(cx)
 648                            .map(|workspace| {
 649                                get_working_directory(workspace.read(cx), cx, strategy)
 650                            })
 651                            .flatten()
 652                    })
 653                });
 654
 655            cx.update(|cx| {
 656                let terminal = project.update(cx, |project, cx| {
 657                    project.create_terminal(cwd, window_id, cx)
 658                })?;
 659
 660                Ok(cx.add_view(&pane, |cx| TerminalView::new(terminal, workspace_id, cx)))
 661            })
 662        })
 663    }
 664
 665    fn added_to_workspace(&mut self, workspace: &mut Workspace, cx: &mut ViewContext<Self>) {
 666        cx.background()
 667            .spawn(TERMINAL_DB.update_workspace_id(
 668                workspace.database_id(),
 669                self.workspace_id,
 670                cx.view_id(),
 671            ))
 672            .detach();
 673        self.workspace_id = workspace.database_id();
 674    }
 675}
 676
 677impl SearchableItem for TerminalView {
 678    type Match = RangeInclusive<Point>;
 679
 680    fn supported_options() -> SearchOptions {
 681        SearchOptions {
 682            case: false,
 683            word: false,
 684            regex: false,
 685        }
 686    }
 687
 688    /// Convert events raised by this item into search-relevant events (if applicable)
 689    fn to_search_event(event: &Self::Event) -> Option<SearchEvent> {
 690        match event {
 691            Event::Wakeup => Some(SearchEvent::MatchesInvalidated),
 692            Event::SelectionsChanged => Some(SearchEvent::ActiveMatchChanged),
 693            _ => None,
 694        }
 695    }
 696
 697    /// Clear stored matches
 698    fn clear_matches(&mut self, cx: &mut ViewContext<Self>) {
 699        self.terminal().update(cx, |term, _| term.matches.clear())
 700    }
 701
 702    /// Store matches returned from find_matches somewhere for rendering
 703    fn update_matches(&mut self, matches: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
 704        self.terminal().update(cx, |term, _| term.matches = matches)
 705    }
 706
 707    /// Return the selection content to pre-load into this search
 708    fn query_suggestion(&mut self, cx: &mut ViewContext<Self>) -> String {
 709        self.terminal()
 710            .read(cx)
 711            .last_content
 712            .selection_text
 713            .clone()
 714            .unwrap_or_default()
 715    }
 716
 717    /// Focus match at given index into the Vec of matches
 718    fn activate_match(&mut self, index: usize, _: Vec<Self::Match>, cx: &mut ViewContext<Self>) {
 719        self.terminal()
 720            .update(cx, |term, _| term.activate_match(index));
 721        cx.notify();
 722    }
 723
 724    /// Get all of the matches for this query, should be done on the background
 725    fn find_matches(
 726        &mut self,
 727        query: project::search::SearchQuery,
 728        cx: &mut ViewContext<Self>,
 729    ) -> Task<Vec<Self::Match>> {
 730        if let Some(searcher) = regex_search_for_query(query) {
 731            self.terminal()
 732                .update(cx, |term, cx| term.find_matches(searcher, cx))
 733        } else {
 734            Task::ready(vec![])
 735        }
 736    }
 737
 738    /// Reports back to the search toolbar what the active match should be (the selection)
 739    fn active_match_index(
 740        &mut self,
 741        matches: Vec<Self::Match>,
 742        cx: &mut ViewContext<Self>,
 743    ) -> Option<usize> {
 744        // Selection head might have a value if there's a selection that isn't
 745        // associated with a match. Therefore, if there are no matches, we should
 746        // report None, no matter the state of the terminal
 747        let res = if matches.len() > 0 {
 748            if let Some(selection_head) = self.terminal().read(cx).selection_head {
 749                // If selection head is contained in a match. Return that match
 750                if let Some(ix) = matches
 751                    .iter()
 752                    .enumerate()
 753                    .find(|(_, search_match)| {
 754                        search_match.contains(&selection_head)
 755                            || search_match.start() > &selection_head
 756                    })
 757                    .map(|(ix, _)| ix)
 758                {
 759                    Some(ix)
 760                } else {
 761                    // If no selection after selection head, return the last match
 762                    Some(matches.len().saturating_sub(1))
 763                }
 764            } else {
 765                // Matches found but no active selection, return the first last one (closest to cursor)
 766                Some(matches.len().saturating_sub(1))
 767            }
 768        } else {
 769            None
 770        };
 771
 772        res
 773    }
 774}
 775
 776///Get's the working directory for the given workspace, respecting the user's settings.
 777pub fn get_working_directory(
 778    workspace: &Workspace,
 779    cx: &AppContext,
 780    strategy: WorkingDirectory,
 781) -> Option<PathBuf> {
 782    let res = match strategy {
 783        WorkingDirectory::CurrentProjectDirectory => current_project_directory(workspace, cx)
 784            .or_else(|| first_project_directory(workspace, cx)),
 785        WorkingDirectory::FirstProjectDirectory => first_project_directory(workspace, cx),
 786        WorkingDirectory::AlwaysHome => None,
 787        WorkingDirectory::Always { directory } => {
 788            shellexpand::full(&directory) //TODO handle this better
 789                .ok()
 790                .map(|dir| Path::new(&dir.to_string()).to_path_buf())
 791                .filter(|dir| dir.is_dir())
 792        }
 793    };
 794    res.or_else(home_dir)
 795}
 796
 797///Get's the first project's home directory, or the home directory
 798fn first_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
 799    workspace
 800        .worktrees(cx)
 801        .next()
 802        .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
 803        .and_then(get_path_from_wt)
 804}
 805
 806///Gets the intuitively correct working directory from the given workspace
 807///If there is an active entry for this project, returns that entry's worktree root.
 808///If there's no active entry but there is a worktree, returns that worktrees root.
 809///If either of these roots are files, or if there are any other query failures,
 810///  returns the user's home directory
 811fn current_project_directory(workspace: &Workspace, cx: &AppContext) -> Option<PathBuf> {
 812    let project = workspace.project().read(cx);
 813
 814    project
 815        .active_entry()
 816        .and_then(|entry_id| project.worktree_for_entry(entry_id, cx))
 817        .or_else(|| workspace.worktrees(cx).next())
 818        .and_then(|worktree_handle| worktree_handle.read(cx).as_local())
 819        .and_then(get_path_from_wt)
 820}
 821
 822fn get_path_from_wt(wt: &LocalWorktree) -> Option<PathBuf> {
 823    wt.root_entry()
 824        .filter(|re| re.is_dir())
 825        .map(|_| wt.abs_path().to_path_buf())
 826}
 827
 828#[cfg(test)]
 829mod tests {
 830
 831    use super::*;
 832    use gpui::TestAppContext;
 833    use project::{Entry, Project, ProjectPath, Worktree};
 834    use workspace::AppState;
 835
 836    use std::path::Path;
 837
 838    ///Working directory calculation tests
 839
 840    ///No Worktrees in project -> home_dir()
 841    #[gpui::test]
 842    async fn no_worktree(cx: &mut TestAppContext) {
 843        //Setup variables
 844        let (project, workspace) = blank_workspace(cx).await;
 845        //Test
 846        cx.read(|cx| {
 847            let workspace = workspace.read(cx);
 848            let active_entry = project.read(cx).active_entry();
 849
 850            //Make sure enviroment is as expeted
 851            assert!(active_entry.is_none());
 852            assert!(workspace.worktrees(cx).next().is_none());
 853
 854            let res = current_project_directory(workspace, cx);
 855            assert_eq!(res, None);
 856            let res = first_project_directory(workspace, cx);
 857            assert_eq!(res, None);
 858        });
 859    }
 860
 861    ///No active entry, but a worktree, worktree is a file -> home_dir()
 862    #[gpui::test]
 863    async fn no_active_entry_worktree_is_file(cx: &mut TestAppContext) {
 864        //Setup variables
 865
 866        let (project, workspace) = blank_workspace(cx).await;
 867        create_file_wt(project.clone(), "/root.txt", cx).await;
 868
 869        cx.read(|cx| {
 870            let workspace = workspace.read(cx);
 871            let active_entry = project.read(cx).active_entry();
 872
 873            //Make sure enviroment is as expeted
 874            assert!(active_entry.is_none());
 875            assert!(workspace.worktrees(cx).next().is_some());
 876
 877            let res = current_project_directory(workspace, cx);
 878            assert_eq!(res, None);
 879            let res = first_project_directory(workspace, cx);
 880            assert_eq!(res, None);
 881        });
 882    }
 883
 884    //No active entry, but a worktree, worktree is a folder -> worktree_folder
 885    #[gpui::test]
 886    async fn no_active_entry_worktree_is_dir(cx: &mut TestAppContext) {
 887        //Setup variables
 888        let (project, workspace) = blank_workspace(cx).await;
 889        let (_wt, _entry) = create_folder_wt(project.clone(), "/root/", cx).await;
 890
 891        //Test
 892        cx.update(|cx| {
 893            let workspace = workspace.read(cx);
 894            let active_entry = project.read(cx).active_entry();
 895
 896            assert!(active_entry.is_none());
 897            assert!(workspace.worktrees(cx).next().is_some());
 898
 899            let res = current_project_directory(workspace, cx);
 900            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
 901            let res = first_project_directory(workspace, cx);
 902            assert_eq!(res, Some((Path::new("/root/")).to_path_buf()));
 903        });
 904    }
 905
 906    //Active entry with a work tree, worktree is a file -> home_dir()
 907    #[gpui::test]
 908    async fn active_entry_worktree_is_file(cx: &mut TestAppContext) {
 909        //Setup variables
 910
 911        let (project, workspace) = blank_workspace(cx).await;
 912        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
 913        let (wt2, entry2) = create_file_wt(project.clone(), "/root2.txt", cx).await;
 914        insert_active_entry_for(wt2, entry2, project.clone(), cx);
 915
 916        //Test
 917        cx.update(|cx| {
 918            let workspace = workspace.read(cx);
 919            let active_entry = project.read(cx).active_entry();
 920
 921            assert!(active_entry.is_some());
 922
 923            let res = current_project_directory(workspace, cx);
 924            assert_eq!(res, None);
 925            let res = first_project_directory(workspace, cx);
 926            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
 927        });
 928    }
 929
 930    //Active entry, with a worktree, worktree is a folder -> worktree_folder
 931    #[gpui::test]
 932    async fn active_entry_worktree_is_dir(cx: &mut TestAppContext) {
 933        //Setup variables
 934        let (project, workspace) = blank_workspace(cx).await;
 935        let (_wt, _entry) = create_folder_wt(project.clone(), "/root1/", cx).await;
 936        let (wt2, entry2) = create_folder_wt(project.clone(), "/root2/", cx).await;
 937        insert_active_entry_for(wt2, entry2, project.clone(), cx);
 938
 939        //Test
 940        cx.update(|cx| {
 941            let workspace = workspace.read(cx);
 942            let active_entry = project.read(cx).active_entry();
 943
 944            assert!(active_entry.is_some());
 945
 946            let res = current_project_directory(workspace, cx);
 947            assert_eq!(res, Some((Path::new("/root2/")).to_path_buf()));
 948            let res = first_project_directory(workspace, cx);
 949            assert_eq!(res, Some((Path::new("/root1/")).to_path_buf()));
 950        });
 951    }
 952
 953    ///Creates a worktree with 1 file: /root.txt
 954    pub async fn blank_workspace(
 955        cx: &mut TestAppContext,
 956    ) -> (ModelHandle<Project>, ViewHandle<Workspace>) {
 957        let params = cx.update(AppState::test);
 958
 959        let project = Project::test(params.fs.clone(), [], cx).await;
 960        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 961
 962        (project, workspace)
 963    }
 964
 965    ///Creates a worktree with 1 folder: /root{suffix}/
 966    async fn create_folder_wt(
 967        project: ModelHandle<Project>,
 968        path: impl AsRef<Path>,
 969        cx: &mut TestAppContext,
 970    ) -> (ModelHandle<Worktree>, Entry) {
 971        create_wt(project, true, path, cx).await
 972    }
 973
 974    ///Creates a worktree with 1 file: /root{suffix}.txt
 975    async fn create_file_wt(
 976        project: ModelHandle<Project>,
 977        path: impl AsRef<Path>,
 978        cx: &mut TestAppContext,
 979    ) -> (ModelHandle<Worktree>, Entry) {
 980        create_wt(project, false, path, cx).await
 981    }
 982
 983    async fn create_wt(
 984        project: ModelHandle<Project>,
 985        is_dir: bool,
 986        path: impl AsRef<Path>,
 987        cx: &mut TestAppContext,
 988    ) -> (ModelHandle<Worktree>, Entry) {
 989        let (wt, _) = project
 990            .update(cx, |project, cx| {
 991                project.find_or_create_local_worktree(path, true, cx)
 992            })
 993            .await
 994            .unwrap();
 995
 996        let entry = cx
 997            .update(|cx| {
 998                wt.update(cx, |wt, cx| {
 999                    wt.as_local()
1000                        .unwrap()
1001                        .create_entry(Path::new(""), is_dir, cx)
1002                })
1003            })
1004            .await
1005            .unwrap();
1006
1007        (wt, entry)
1008    }
1009
1010    pub fn insert_active_entry_for(
1011        wt: ModelHandle<Worktree>,
1012        entry: Entry,
1013        project: ModelHandle<Project>,
1014        cx: &mut TestAppContext,
1015    ) {
1016        cx.update(|cx| {
1017            let p = ProjectPath {
1018                worktree_id: wt.read(cx).id(),
1019                path: entry.path,
1020            };
1021            project.update(cx, |project, cx| project.set_active_path(Some(p), cx));
1022        });
1023    }
1024}