terminal_view.rs

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