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