terminal_view.rs

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