terminal_view.rs

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