terminal2.rs

   1pub mod mappings;
   2pub use alacritty_terminal;
   3pub mod terminal_settings;
   4
   5use alacritty_terminal::{
   6    ansi::{ClearMode, Handler},
   7    config::{Config, Program, PtyConfig, Scrolling},
   8    event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},
   9    event_loop::{EventLoop, Msg, Notifier},
  10    grid::{Dimensions, Scroll as AlacScroll},
  11    index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint},
  12    selection::{Selection, SelectionRange, SelectionType},
  13    sync::FairMutex,
  14    term::{
  15        cell::Cell,
  16        color::Rgb,
  17        search::{Match, RegexIter, RegexSearch},
  18        RenderableCursor, TermMode,
  19    },
  20    tty::{self, setup_env},
  21    Term,
  22};
  23use anyhow::{bail, Result};
  24
  25use futures::{
  26    channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},
  27    FutureExt,
  28};
  29
  30use mappings::mouse::{
  31    alt_scroll, grid_point, mouse_button_report, mouse_moved_report, mouse_side, scroll_report,
  32};
  33
  34use procinfo::LocalProcessInfo;
  35use serde::{Deserialize, Serialize};
  36use settings::Settings;
  37use terminal_settings::{AlternateScroll, Shell, TerminalBlink, TerminalSettings};
  38use util::truncate_and_trailoff;
  39
  40use std::{
  41    cmp::{self, min},
  42    collections::{HashMap, VecDeque},
  43    fmt::Display,
  44    ops::{Deref, Index, RangeInclusive},
  45    os::unix::prelude::AsRawFd,
  46    path::PathBuf,
  47    sync::Arc,
  48    time::{Duration, Instant},
  49};
  50use thiserror::Error;
  51
  52use gpui::{
  53    actions, px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla, Keystroke,
  54    ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
  55    Point, ScrollWheelEvent, Size, Task, TouchPhase,
  56};
  57
  58use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str};
  59use lazy_static::lazy_static;
  60
  61actions!(
  62    terminal,
  63    [
  64        Clear,
  65        Copy,
  66        Paste,
  67        ShowCharacterPalette,
  68        SearchTest,
  69        SendText,
  70        SendKeystroke,
  71    ]
  72);
  73
  74///Scrolling is unbearably sluggish by default. Alacritty supports a configurable
  75///Scroll multiplier that is set to 3 by default. This will be removed when I
  76///Implement scroll bars.
  77const SCROLL_MULTIPLIER: f32 = 4.;
  78const MAX_SEARCH_LINES: usize = 100;
  79const DEBUG_TERMINAL_WIDTH: Pixels = px(500.);
  80const DEBUG_TERMINAL_HEIGHT: Pixels = px(30.);
  81const DEBUG_CELL_WIDTH: Pixels = px(5.);
  82const DEBUG_LINE_HEIGHT: Pixels = px(5.);
  83
  84lazy_static! {
  85    // Regex Copied from alacritty's ui_config.rs and modified its declaration slightly:
  86    // * avoid Rust-specific escaping.
  87    // * use more strict regex for `file://` protocol matching: original regex has `file:` inside, but we want to avoid matching `some::file::module` strings.
  88    static ref URL_REGEX: RegexSearch = RegexSearch::new(r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#).unwrap();
  89
  90    static ref WORD_REGEX: RegexSearch = RegexSearch::new(r#"[\w.\[\]:/@\-~]+"#).unwrap();
  91}
  92
  93///Upward flowing events, for changing the title and such
  94#[derive(Clone, Debug)]
  95pub enum Event {
  96    TitleChanged,
  97    BreadcrumbsChanged,
  98    CloseTerminal,
  99    Bell,
 100    Wakeup,
 101    BlinkChanged,
 102    SelectionsChanged,
 103    NewNavigationTarget(Option<MaybeNavigationTarget>),
 104    Open(MaybeNavigationTarget),
 105}
 106
 107/// A string inside terminal, potentially useful as a URI that can be opened.
 108#[derive(Clone, Debug)]
 109pub enum MaybeNavigationTarget {
 110    /// HTTP, git, etc. string determined by the [`URL_REGEX`] regex.
 111    Url(String),
 112    /// File system path, absolute or relative, existing or not.
 113    /// Might have line and column number(s) attached as `file.rs:1:23`
 114    PathLike(String),
 115}
 116
 117#[derive(Clone)]
 118enum InternalEvent {
 119    ColorRequest(usize, Arc<dyn Fn(Rgb) -> String + Sync + Send + 'static>),
 120    Resize(TerminalSize),
 121    Clear,
 122    // FocusNextMatch,
 123    Scroll(AlacScroll),
 124    ScrollToAlacPoint(AlacPoint),
 125    SetSelection(Option<(Selection, AlacPoint)>),
 126    UpdateSelection(Point<Pixels>),
 127    // Adjusted mouse position, should open
 128    FindHyperlink(Point<Pixels>, bool),
 129    Copy,
 130}
 131
 132///A translation struct for Alacritty to communicate with us from their event loop
 133#[derive(Clone)]
 134pub struct ZedListener(UnboundedSender<AlacTermEvent>);
 135
 136impl EventListener for ZedListener {
 137    fn send_event(&self, event: AlacTermEvent) {
 138        self.0.unbounded_send(event).ok();
 139    }
 140}
 141
 142pub fn init(cx: &mut AppContext) {
 143    TerminalSettings::register(cx);
 144}
 145
 146#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
 147pub struct TerminalSize {
 148    pub cell_width: Pixels,
 149    pub line_height: Pixels,
 150    pub size: Size<Pixels>,
 151}
 152
 153impl TerminalSize {
 154    pub fn new(line_height: Pixels, cell_width: Pixels, size: Size<Pixels>) -> Self {
 155        TerminalSize {
 156            cell_width,
 157            line_height,
 158            size,
 159        }
 160    }
 161
 162    pub fn num_lines(&self) -> usize {
 163        f32::from((self.size.height / self.line_height).floor()) as usize
 164    }
 165
 166    pub fn num_columns(&self) -> usize {
 167        f32::from((self.size.width / self.cell_width).floor()) as usize
 168    }
 169
 170    pub fn height(&self) -> Pixels {
 171        self.size.height
 172    }
 173
 174    pub fn width(&self) -> Pixels {
 175        self.size.width
 176    }
 177
 178    pub fn cell_width(&self) -> Pixels {
 179        self.cell_width
 180    }
 181
 182    pub fn line_height(&self) -> Pixels {
 183        self.line_height
 184    }
 185}
 186impl Default for TerminalSize {
 187    fn default() -> Self {
 188        TerminalSize::new(
 189            DEBUG_LINE_HEIGHT,
 190            DEBUG_CELL_WIDTH,
 191            Size {
 192                width: DEBUG_TERMINAL_WIDTH,
 193                height: DEBUG_TERMINAL_HEIGHT,
 194            },
 195        )
 196    }
 197}
 198
 199impl From<TerminalSize> for WindowSize {
 200    fn from(val: TerminalSize) -> Self {
 201        WindowSize {
 202            num_lines: val.num_lines() as u16,
 203            num_cols: val.num_columns() as u16,
 204            cell_width: f32::from(val.cell_width()) as u16,
 205            cell_height: f32::from(val.line_height()) as u16,
 206        }
 207    }
 208}
 209
 210impl Dimensions for TerminalSize {
 211    /// Note: this is supposed to be for the back buffer's length,
 212    /// but we exclusively use it to resize the terminal, which does not
 213    /// use this method. We still have to implement it for the trait though,
 214    /// hence, this comment.
 215    fn total_lines(&self) -> usize {
 216        self.screen_lines()
 217    }
 218
 219    fn screen_lines(&self) -> usize {
 220        self.num_lines()
 221    }
 222
 223    fn columns(&self) -> usize {
 224        self.num_columns()
 225    }
 226}
 227
 228#[derive(Error, Debug)]
 229pub struct TerminalError {
 230    pub directory: Option<PathBuf>,
 231    pub shell: Shell,
 232    pub source: std::io::Error,
 233}
 234
 235impl TerminalError {
 236    pub fn fmt_directory(&self) -> String {
 237        self.directory
 238            .clone()
 239            .map(|path| {
 240                match path
 241                    .into_os_string()
 242                    .into_string()
 243                    .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))
 244                {
 245                    Ok(s) => s,
 246                    Err(s) => s,
 247                }
 248            })
 249            .unwrap_or_else(|| {
 250                let default_dir =
 251                    dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());
 252                match default_dir {
 253                    Some(dir) => format!("<none specified, using home directory> {}", dir),
 254                    None => "<none specified, could not find home directory>".to_string(),
 255                }
 256            })
 257    }
 258
 259    pub fn shell_to_string(&self) -> String {
 260        match &self.shell {
 261            Shell::System => "<system shell>".to_string(),
 262            Shell::Program(p) => p.to_string(),
 263            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
 264        }
 265    }
 266
 267    pub fn fmt_shell(&self) -> String {
 268        match &self.shell {
 269            Shell::System => "<system defined shell>".to_string(),
 270            Shell::Program(s) => s.to_string(),
 271            Shell::WithArguments { program, args } => format!("{} {}", program, args.join(" ")),
 272        }
 273    }
 274}
 275
 276impl Display for TerminalError {
 277    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 278        let dir_string: String = self.fmt_directory();
 279        let shell = self.fmt_shell();
 280
 281        write!(
 282            f,
 283            "Working directory: {} Shell command: `{}`, IOError: {}",
 284            dir_string, shell, self.source
 285        )
 286    }
 287}
 288
 289pub struct TerminalBuilder {
 290    terminal: Terminal,
 291    events_rx: UnboundedReceiver<AlacTermEvent>,
 292}
 293
 294impl TerminalBuilder {
 295    pub fn new(
 296        working_directory: Option<PathBuf>,
 297        shell: Shell,
 298        mut env: HashMap<String, String>,
 299        blink_settings: Option<TerminalBlink>,
 300        alternate_scroll: AlternateScroll,
 301        window: AnyWindowHandle,
 302        color_for_index: impl Fn(usize, &mut AppContext) -> Hsla + Send + Sync + 'static,
 303    ) -> Result<TerminalBuilder> {
 304        let pty_config = {
 305            let alac_shell = match shell.clone() {
 306                Shell::System => None,
 307                Shell::Program(program) => Some(Program::Just(program)),
 308                Shell::WithArguments { program, args } => Some(Program::WithArgs { program, args }),
 309            };
 310
 311            PtyConfig {
 312                shell: alac_shell,
 313                working_directory: working_directory.clone(),
 314                hold: false,
 315            }
 316        };
 317
 318        //TODO: Properly set the current locale,
 319        env.insert("LC_ALL".to_string(), "en_US.UTF-8".to_string());
 320        env.insert("ZED_TERM".to_string(), true.to_string());
 321
 322        let alac_scrolling = Scrolling::default();
 323        // alac_scrolling.set_history((BACK_BUFFER_SIZE * 2) as u32);
 324
 325        let config = Config {
 326            pty_config: pty_config.clone(),
 327            env,
 328            scrolling: alac_scrolling,
 329            ..Default::default()
 330        };
 331
 332        setup_env(&config);
 333
 334        //Spawn a task so the Alacritty EventLoop can communicate with us in a view context
 335        //TODO: Remove with a bounded sender which can be dispatched on &self
 336        let (events_tx, events_rx) = unbounded();
 337        //Set up the terminal...
 338        let mut term = Term::new(
 339            &config,
 340            &TerminalSize::default(),
 341            ZedListener(events_tx.clone()),
 342        );
 343
 344        //Start off blinking if we need to
 345        if let Some(TerminalBlink::On) = blink_settings {
 346            term.set_mode(alacritty_terminal::ansi::Mode::BlinkingCursor)
 347        }
 348
 349        //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.
 350        if let AlternateScroll::Off = alternate_scroll {
 351            term.unset_mode(alacritty_terminal::ansi::Mode::AlternateScroll)
 352        }
 353
 354        let term = Arc::new(FairMutex::new(term));
 355
 356        //Setup the pty...
 357        let pty = match tty::new(
 358            &pty_config,
 359            TerminalSize::default().into(),
 360            window.window_id().as_u64(),
 361        ) {
 362            Ok(pty) => pty,
 363            Err(error) => {
 364                bail!(TerminalError {
 365                    directory: working_directory,
 366                    shell,
 367                    source: error,
 368                });
 369            }
 370        };
 371
 372        let fd = pty.file().as_raw_fd();
 373        let shell_pid = pty.child().id();
 374
 375        //And connect them together
 376        let event_loop = EventLoop::new(
 377            term.clone(),
 378            ZedListener(events_tx.clone()),
 379            pty,
 380            pty_config.hold,
 381            false,
 382        );
 383
 384        //Kick things off
 385        let pty_tx = event_loop.channel();
 386        let _io_thread = event_loop.spawn();
 387
 388        let terminal = Terminal {
 389            pty_tx: Notifier(pty_tx),
 390            term,
 391            events: VecDeque::with_capacity(10), //Should never get this high.
 392            last_content: Default::default(),
 393            last_mouse: None,
 394            matches: Vec::new(),
 395            last_synced: Instant::now(),
 396            sync_task: None,
 397            selection_head: None,
 398            shell_fd: fd as u32,
 399            shell_pid,
 400            foreground_process_info: None,
 401            breadcrumb_text: String::new(),
 402            scroll_px: px(0.),
 403            last_mouse_position: None,
 404            next_link_id: 0,
 405            selection_phase: SelectionPhase::Ended,
 406            cmd_pressed: false,
 407            hovered_word: false,
 408            color_for_index: Box::new(color_for_index),
 409        };
 410
 411        Ok(TerminalBuilder {
 412            terminal,
 413            events_rx,
 414        })
 415    }
 416
 417    pub fn subscribe(mut self, cx: &mut ModelContext<Terminal>) -> Terminal {
 418        //Event loop
 419        cx.spawn(|this, mut cx| async move {
 420            use futures::StreamExt;
 421
 422            while let Some(event) = self.events_rx.next().await {
 423                this.update(&mut cx, |this, cx| {
 424                    //Process the first event immediately for lowered latency
 425                    this.process_event(&event, cx);
 426                })?;
 427
 428                'outer: loop {
 429                    let mut events = vec![];
 430                    let mut timer = cx
 431                        .background_executor()
 432                        .timer(Duration::from_millis(4))
 433                        .fuse();
 434                    let mut wakeup = false;
 435                    loop {
 436                        futures::select_biased! {
 437                            _ = timer => break,
 438                            event = self.events_rx.next() => {
 439                                if let Some(event) = event {
 440                                    if matches!(event, AlacTermEvent::Wakeup) {
 441                                        wakeup = true;
 442                                    } else {
 443                                        events.push(event);
 444                                    }
 445
 446                                    if events.len() > 100 {
 447                                        break;
 448                                    }
 449                                } else {
 450                                    break;
 451                                }
 452                            },
 453                        }
 454                    }
 455
 456                    if events.is_empty() && wakeup == false {
 457                        smol::future::yield_now().await;
 458                        break 'outer;
 459                    } else {
 460                        this.update(&mut cx, |this, cx| {
 461                            if wakeup {
 462                                this.process_event(&AlacTermEvent::Wakeup, cx);
 463                            }
 464
 465                            for event in events {
 466                                this.process_event(&event, cx);
 467                            }
 468                        })?;
 469                        smol::future::yield_now().await;
 470                    }
 471                }
 472            }
 473
 474            anyhow::Ok(())
 475        })
 476        .detach();
 477
 478        self.terminal
 479    }
 480}
 481
 482#[derive(Debug, Clone, Deserialize, Serialize)]
 483pub struct IndexedCell {
 484    pub point: AlacPoint,
 485    pub cell: Cell,
 486}
 487
 488impl Deref for IndexedCell {
 489    type Target = Cell;
 490
 491    #[inline]
 492    fn deref(&self) -> &Cell {
 493        &self.cell
 494    }
 495}
 496
 497// TODO: Un-pub
 498#[derive(Clone)]
 499pub struct TerminalContent {
 500    pub cells: Vec<IndexedCell>,
 501    pub mode: TermMode,
 502    pub display_offset: usize,
 503    pub selection_text: Option<String>,
 504    pub selection: Option<SelectionRange>,
 505    pub cursor: RenderableCursor,
 506    pub cursor_char: char,
 507    pub size: TerminalSize,
 508    pub last_hovered_word: Option<HoveredWord>,
 509}
 510
 511#[derive(Clone)]
 512pub struct HoveredWord {
 513    pub word: String,
 514    pub word_match: RangeInclusive<AlacPoint>,
 515    pub id: usize,
 516}
 517
 518impl Default for TerminalContent {
 519    fn default() -> Self {
 520        TerminalContent {
 521            cells: Default::default(),
 522            mode: Default::default(),
 523            display_offset: Default::default(),
 524            selection_text: Default::default(),
 525            selection: Default::default(),
 526            cursor: RenderableCursor {
 527                shape: alacritty_terminal::ansi::CursorShape::Block,
 528                point: AlacPoint::new(Line(0), Column(0)),
 529            },
 530            cursor_char: Default::default(),
 531            size: Default::default(),
 532            last_hovered_word: None,
 533        }
 534    }
 535}
 536
 537#[derive(PartialEq, Eq)]
 538pub enum SelectionPhase {
 539    Selecting,
 540    Ended,
 541}
 542
 543pub struct Terminal {
 544    pty_tx: Notifier,
 545    term: Arc<FairMutex<Term<ZedListener>>>,
 546    events: VecDeque<InternalEvent>,
 547    /// This is only used for mouse mode cell change detection
 548    last_mouse: Option<(AlacPoint, AlacDirection)>,
 549    /// This is only used for terminal hovered word checking
 550    last_mouse_position: Option<Point<Pixels>>,
 551    pub matches: Vec<RangeInclusive<AlacPoint>>,
 552    pub last_content: TerminalContent,
 553    last_synced: Instant,
 554    sync_task: Option<Task<()>>,
 555    pub selection_head: Option<AlacPoint>,
 556    pub breadcrumb_text: String,
 557    shell_pid: u32,
 558    shell_fd: u32,
 559    pub foreground_process_info: Option<LocalProcessInfo>,
 560    scroll_px: Pixels,
 561    next_link_id: usize,
 562    selection_phase: SelectionPhase,
 563    cmd_pressed: bool,
 564    hovered_word: bool,
 565    // An implementation of the 8 bit ANSI color palette
 566    color_for_index: Box<dyn Fn(usize, &mut AppContext) -> Hsla + Send + Sync + 'static>,
 567}
 568
 569impl Terminal {
 570    fn process_event(&mut self, event: &AlacTermEvent, cx: &mut ModelContext<Self>) {
 571        match event {
 572            AlacTermEvent::Title(title) => {
 573                self.breadcrumb_text = title.to_string();
 574                cx.emit(Event::BreadcrumbsChanged);
 575            }
 576            AlacTermEvent::ResetTitle => {
 577                self.breadcrumb_text = String::new();
 578                cx.emit(Event::BreadcrumbsChanged);
 579            }
 580            AlacTermEvent::ClipboardStore(_, data) => {
 581                cx.write_to_clipboard(ClipboardItem::new(data.to_string()))
 582            }
 583            AlacTermEvent::ClipboardLoad(_, format) => self.write_to_pty(format(
 584                &cx.read_from_clipboard()
 585                    .map(|ci| ci.text().to_string())
 586                    .unwrap_or_else(|| "".to_string()),
 587            )),
 588            AlacTermEvent::PtyWrite(out) => self.write_to_pty(out.clone()),
 589            AlacTermEvent::TextAreaSizeRequest(format) => {
 590                self.write_to_pty(format(self.last_content.size.into()))
 591            }
 592            AlacTermEvent::CursorBlinkingChange => {
 593                cx.emit(Event::BlinkChanged);
 594            }
 595            AlacTermEvent::Bell => {
 596                cx.emit(Event::Bell);
 597            }
 598            AlacTermEvent::Exit => cx.emit(Event::CloseTerminal),
 599            AlacTermEvent::MouseCursorDirty => {
 600                //NOOP, Handled in render
 601            }
 602            AlacTermEvent::Wakeup => {
 603                cx.emit(Event::Wakeup);
 604
 605                if self.update_process_info() {
 606                    cx.emit(Event::TitleChanged);
 607                }
 608            }
 609            AlacTermEvent::ColorRequest(idx, fun_ptr) => {
 610                self.events
 611                    .push_back(InternalEvent::ColorRequest(*idx, fun_ptr.clone()));
 612            }
 613        }
 614    }
 615
 616    /// Update the cached process info, returns whether the Zed-relevant info has changed
 617    fn update_process_info(&mut self) -> bool {
 618        let mut pid = unsafe { libc::tcgetpgrp(self.shell_fd as i32) };
 619        if pid < 0 {
 620            pid = self.shell_pid as i32;
 621        }
 622
 623        if let Some(process_info) = LocalProcessInfo::with_root_pid(pid as u32) {
 624            let res = self
 625                .foreground_process_info
 626                .as_ref()
 627                .map(|old_info| {
 628                    process_info.cwd != old_info.cwd || process_info.name != old_info.name
 629                })
 630                .unwrap_or(true);
 631
 632            self.foreground_process_info = Some(process_info.clone());
 633
 634            res
 635        } else {
 636            false
 637        }
 638    }
 639
 640    ///Takes events from Alacritty and translates them to behavior on this view
 641    fn process_terminal_event(
 642        &mut self,
 643        event: &InternalEvent,
 644        term: &mut Term<ZedListener>,
 645        cx: &mut ModelContext<Self>,
 646    ) {
 647        match event {
 648            InternalEvent::ColorRequest(index, format) => {
 649                let color = term.colors()[*index]
 650                    .unwrap_or_else(|| to_alac_rgb((self.color_for_index)(*index, cx)));
 651                self.write_to_pty(format(color))
 652            }
 653            InternalEvent::Resize(mut new_size) => {
 654                new_size.size.height = cmp::max(new_size.line_height, new_size.height());
 655                new_size.size.width = cmp::max(new_size.cell_width, new_size.width());
 656
 657                self.last_content.size = new_size.clone();
 658
 659                self.pty_tx.0.send(Msg::Resize(new_size.into())).ok();
 660
 661                term.resize(new_size);
 662            }
 663            InternalEvent::Clear => {
 664                // Clear back buffer
 665                term.clear_screen(ClearMode::Saved);
 666
 667                let cursor = term.grid().cursor.point;
 668
 669                // Clear the lines above
 670                term.grid_mut().reset_region(..cursor.line);
 671
 672                // Copy the current line up
 673                let line = term.grid()[cursor.line][..Column(term.grid().columns())]
 674                    .iter()
 675                    .cloned()
 676                    .enumerate()
 677                    .collect::<Vec<(usize, Cell)>>();
 678
 679                for (i, cell) in line {
 680                    term.grid_mut()[Line(0)][Column(i)] = cell;
 681                }
 682
 683                // Reset the cursor
 684                term.grid_mut().cursor.point =
 685                    AlacPoint::new(Line(0), term.grid_mut().cursor.point.column);
 686                let new_cursor = term.grid().cursor.point;
 687
 688                // Clear the lines below the new cursor
 689                if (new_cursor.line.0 as usize) < term.screen_lines() - 1 {
 690                    term.grid_mut().reset_region((new_cursor.line + 1)..);
 691                }
 692
 693                cx.emit(Event::Wakeup);
 694            }
 695            InternalEvent::Scroll(scroll) => {
 696                term.scroll_display(*scroll);
 697                self.refresh_hovered_word();
 698            }
 699            InternalEvent::SetSelection(selection) => {
 700                term.selection = selection.as_ref().map(|(sel, _)| sel.clone());
 701
 702                if let Some((_, head)) = selection {
 703                    self.selection_head = Some(*head);
 704                }
 705                cx.emit(Event::SelectionsChanged)
 706            }
 707            InternalEvent::UpdateSelection(position) => {
 708                if let Some(mut selection) = term.selection.take() {
 709                    let point = grid_point(
 710                        *position,
 711                        self.last_content.size,
 712                        term.grid().display_offset(),
 713                    );
 714
 715                    let side = mouse_side(*position, self.last_content.size);
 716
 717                    selection.update(point, side);
 718                    term.selection = Some(selection);
 719
 720                    self.selection_head = Some(point);
 721                    cx.emit(Event::SelectionsChanged)
 722                }
 723            }
 724
 725            InternalEvent::Copy => {
 726                if let Some(txt) = term.selection_to_string() {
 727                    cx.write_to_clipboard(ClipboardItem::new(txt))
 728                }
 729            }
 730            InternalEvent::ScrollToAlacPoint(point) => {
 731                term.scroll_to_point(*point);
 732                self.refresh_hovered_word();
 733            }
 734            InternalEvent::FindHyperlink(position, open) => {
 735                let prev_hovered_word = self.last_content.last_hovered_word.take();
 736
 737                let point = grid_point(
 738                    *position,
 739                    self.last_content.size,
 740                    term.grid().display_offset(),
 741                )
 742                .grid_clamp(term, Boundary::Grid);
 743
 744                let link = term.grid().index(point).hyperlink();
 745                let found_word = if link.is_some() {
 746                    let mut min_index = point;
 747                    loop {
 748                        let new_min_index = min_index.sub(term, Boundary::Cursor, 1);
 749                        if new_min_index == min_index {
 750                            break;
 751                        } else if term.grid().index(new_min_index).hyperlink() != link {
 752                            break;
 753                        } else {
 754                            min_index = new_min_index
 755                        }
 756                    }
 757
 758                    let mut max_index = point;
 759                    loop {
 760                        let new_max_index = max_index.add(term, Boundary::Cursor, 1);
 761                        if new_max_index == max_index {
 762                            break;
 763                        } else if term.grid().index(new_max_index).hyperlink() != link {
 764                            break;
 765                        } else {
 766                            max_index = new_max_index
 767                        }
 768                    }
 769
 770                    let url = link.unwrap().uri().to_owned();
 771                    let url_match = min_index..=max_index;
 772
 773                    Some((url, true, url_match))
 774                } else if let Some(word_match) = regex_match_at(term, point, &WORD_REGEX) {
 775                    let maybe_url_or_path =
 776                        term.bounds_to_string(*word_match.start(), *word_match.end());
 777                    let original_match = word_match.clone();
 778                    let (sanitized_match, sanitized_word) =
 779                        if maybe_url_or_path.starts_with('[') && maybe_url_or_path.ends_with(']') {
 780                            (
 781                                Match::new(
 782                                    word_match.start().add(term, Boundary::Cursor, 1),
 783                                    word_match.end().sub(term, Boundary::Cursor, 1),
 784                                ),
 785                                maybe_url_or_path[1..maybe_url_or_path.len() - 1].to_owned(),
 786                            )
 787                        } else {
 788                            (word_match, maybe_url_or_path)
 789                        };
 790
 791                    let is_url = match regex_match_at(term, point, &URL_REGEX) {
 792                        Some(url_match) => {
 793                            // `]` is a valid symbol in the `file://` URL, so the regex match will include it
 794                            // consider that when ensuring that the URL match is the same as the original word
 795                            if sanitized_match != original_match {
 796                                url_match.start() == sanitized_match.start()
 797                                    && url_match.end() == original_match.end()
 798                            } else {
 799                                url_match == sanitized_match
 800                            }
 801                        }
 802                        None => false,
 803                    };
 804                    Some((sanitized_word, is_url, sanitized_match))
 805                } else {
 806                    None
 807                };
 808
 809                match found_word {
 810                    Some((maybe_url_or_path, is_url, url_match)) => {
 811                        if *open {
 812                            let target = if is_url {
 813                                MaybeNavigationTarget::Url(maybe_url_or_path)
 814                            } else {
 815                                MaybeNavigationTarget::PathLike(maybe_url_or_path)
 816                            };
 817                            cx.emit(Event::Open(target));
 818                        } else {
 819                            self.update_selected_word(
 820                                prev_hovered_word,
 821                                url_match,
 822                                maybe_url_or_path,
 823                                is_url,
 824                                cx,
 825                            );
 826                        }
 827                        self.hovered_word = true;
 828                    }
 829                    None => {
 830                        if self.hovered_word {
 831                            cx.emit(Event::NewNavigationTarget(None));
 832                        }
 833                        self.hovered_word = false;
 834                    }
 835                }
 836            }
 837        }
 838    }
 839
 840    fn update_selected_word(
 841        &mut self,
 842        prev_word: Option<HoveredWord>,
 843        word_match: RangeInclusive<AlacPoint>,
 844        word: String,
 845        is_url: bool,
 846        cx: &mut ModelContext<Self>,
 847    ) {
 848        if let Some(prev_word) = prev_word {
 849            if prev_word.word == word && prev_word.word_match == word_match {
 850                self.last_content.last_hovered_word = Some(HoveredWord {
 851                    word,
 852                    word_match,
 853                    id: prev_word.id,
 854                });
 855                return;
 856            }
 857        }
 858
 859        self.last_content.last_hovered_word = Some(HoveredWord {
 860            word: word.clone(),
 861            word_match,
 862            id: self.next_link_id(),
 863        });
 864        let navigation_target = if is_url {
 865            MaybeNavigationTarget::Url(word)
 866        } else {
 867            MaybeNavigationTarget::PathLike(word)
 868        };
 869        cx.emit(Event::NewNavigationTarget(Some(navigation_target)));
 870    }
 871
 872    fn next_link_id(&mut self) -> usize {
 873        let res = self.next_link_id;
 874        self.next_link_id = self.next_link_id.wrapping_add(1);
 875        res
 876    }
 877
 878    pub fn last_content(&self) -> &TerminalContent {
 879        &self.last_content
 880    }
 881
 882    //To test:
 883    //- Activate match on terminal (scrolling and selection)
 884    //- Editor search snapping behavior
 885
 886    pub fn activate_match(&mut self, index: usize) {
 887        if let Some(search_match) = self.matches.get(index).cloned() {
 888            self.set_selection(Some((make_selection(&search_match), *search_match.end())));
 889
 890            self.events
 891                .push_back(InternalEvent::ScrollToAlacPoint(*search_match.start()));
 892        }
 893    }
 894
 895    pub fn select_matches(&mut self, matches: Vec<RangeInclusive<AlacPoint>>) {
 896        let matches_to_select = self
 897            .matches
 898            .iter()
 899            .filter(|self_match| matches.contains(self_match))
 900            .cloned()
 901            .collect::<Vec<_>>();
 902        for match_to_select in matches_to_select {
 903            self.set_selection(Some((
 904                make_selection(&match_to_select),
 905                *match_to_select.end(),
 906            )));
 907        }
 908    }
 909
 910    pub fn select_all(&mut self) {
 911        let term = self.term.lock();
 912        let start = AlacPoint::new(term.topmost_line(), Column(0));
 913        let end = AlacPoint::new(term.bottommost_line(), term.last_column());
 914        drop(term);
 915        self.set_selection(Some((make_selection(&(start..=end)), end)));
 916    }
 917
 918    fn set_selection(&mut self, selection: Option<(Selection, AlacPoint)>) {
 919        self.events
 920            .push_back(InternalEvent::SetSelection(selection));
 921    }
 922
 923    pub fn copy(&mut self) {
 924        self.events.push_back(InternalEvent::Copy);
 925    }
 926
 927    pub fn clear(&mut self) {
 928        self.events.push_back(InternalEvent::Clear)
 929    }
 930
 931    ///Resize the terminal and the PTY.
 932    pub fn set_size(&mut self, new_size: TerminalSize) {
 933        self.events.push_back(InternalEvent::Resize(new_size))
 934    }
 935
 936    ///Write the Input payload to the tty.
 937    fn write_to_pty(&self, input: String) {
 938        self.pty_tx.notify(input.into_bytes());
 939    }
 940
 941    fn write_bytes_to_pty(&self, input: Vec<u8>) {
 942        self.pty_tx.notify(input);
 943    }
 944
 945    pub fn input(&mut self, input: String) {
 946        self.events
 947            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
 948        self.events.push_back(InternalEvent::SetSelection(None));
 949
 950        self.write_to_pty(input);
 951    }
 952
 953    pub fn input_bytes(&mut self, input: Vec<u8>) {
 954        self.events
 955            .push_back(InternalEvent::Scroll(AlacScroll::Bottom));
 956        self.events.push_back(InternalEvent::SetSelection(None));
 957
 958        self.write_bytes_to_pty(input);
 959    }
 960
 961    pub fn try_keystroke(&mut self, keystroke: &Keystroke, alt_is_meta: bool) -> bool {
 962        let esc = to_esc_str(keystroke, &self.last_content.mode, alt_is_meta);
 963        if let Some(esc) = esc {
 964            self.input(esc);
 965            true
 966        } else {
 967            false
 968        }
 969    }
 970
 971    pub fn try_modifiers_change(&mut self, modifiers: &Modifiers) -> bool {
 972        let changed = self.cmd_pressed != modifiers.command;
 973        if !self.cmd_pressed && modifiers.command {
 974            self.refresh_hovered_word();
 975        }
 976        self.cmd_pressed = modifiers.command;
 977        changed
 978    }
 979
 980    ///Paste text into the terminal
 981    pub fn paste(&mut self, text: &str) {
 982        let paste_text = if self.last_content.mode.contains(TermMode::BRACKETED_PASTE) {
 983            format!("{}{}{}", "\x1b[200~", text.replace('\x1b', ""), "\x1b[201~")
 984        } else {
 985            text.replace("\r\n", "\r").replace('\n', "\r")
 986        };
 987
 988        self.input(paste_text);
 989    }
 990
 991    pub fn try_sync(&mut self, cx: &mut ModelContext<Self>) {
 992        let term = self.term.clone();
 993
 994        let mut terminal = if let Some(term) = term.try_lock_unfair() {
 995            term
 996        } else if self.last_synced.elapsed().as_secs_f32() > 0.25 {
 997            term.lock_unfair() //It's been too long, force block
 998        } else if let None = self.sync_task {
 999            //Skip this frame
1000            let delay = cx.background_executor().timer(Duration::from_millis(16));
1001            self.sync_task = Some(cx.spawn(|weak_handle, mut cx| async move {
1002                delay.await;
1003                if let Some(handle) = weak_handle.upgrade() {
1004                    handle
1005                        .update(&mut cx, |terminal, cx| {
1006                            terminal.sync_task.take();
1007                            cx.notify();
1008                        })
1009                        .ok();
1010                }
1011            }));
1012            return;
1013        } else {
1014            //No lock and delayed rendering already scheduled, nothing to do
1015            return;
1016        };
1017
1018        //Note that the ordering of events matters for event processing
1019        while let Some(e) = self.events.pop_front() {
1020            self.process_terminal_event(&e, &mut terminal, cx)
1021        }
1022
1023        self.last_content = Self::make_content(&terminal, &self.last_content);
1024        self.last_synced = Instant::now();
1025    }
1026
1027    fn make_content(term: &Term<ZedListener>, last_content: &TerminalContent) -> TerminalContent {
1028        let content = term.renderable_content();
1029        TerminalContent {
1030            cells: content
1031                .display_iter
1032                //TODO: Add this once there's a way to retain empty lines
1033                // .filter(|ic| {
1034                //     !ic.flags.contains(Flags::HIDDEN)
1035                //         && !(ic.bg == Named(NamedColor::Background)
1036                //             && ic.c == ' '
1037                //             && !ic.flags.contains(Flags::INVERSE))
1038                // })
1039                .map(|ic| IndexedCell {
1040                    point: ic.point,
1041                    cell: ic.cell.clone(),
1042                })
1043                .collect::<Vec<IndexedCell>>(),
1044            mode: content.mode,
1045            display_offset: content.display_offset,
1046            selection_text: term.selection_to_string(),
1047            selection: content.selection,
1048            cursor: content.cursor,
1049            cursor_char: term.grid()[content.cursor.point].c,
1050            size: last_content.size,
1051            last_hovered_word: last_content.last_hovered_word.clone(),
1052        }
1053    }
1054
1055    pub fn focus_in(&self) {
1056        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1057            self.write_to_pty("\x1b[I".to_string());
1058        }
1059    }
1060
1061    pub fn focus_out(&mut self) {
1062        self.last_mouse_position = None;
1063        if self.last_content.mode.contains(TermMode::FOCUS_IN_OUT) {
1064            self.write_to_pty("\x1b[O".to_string());
1065        }
1066    }
1067
1068    pub fn mouse_changed(&mut self, point: AlacPoint, side: AlacDirection) -> bool {
1069        match self.last_mouse {
1070            Some((old_point, old_side)) => {
1071                if old_point == point && old_side == side {
1072                    false
1073                } else {
1074                    self.last_mouse = Some((point, side));
1075                    true
1076                }
1077            }
1078            None => {
1079                self.last_mouse = Some((point, side));
1080                true
1081            }
1082        }
1083    }
1084
1085    pub fn mouse_mode(&self, shift: bool) -> bool {
1086        self.last_content.mode.intersects(TermMode::MOUSE_MODE) && !shift
1087    }
1088
1089    pub fn mouse_move(&mut self, e: &MouseMoveEvent, origin: Point<Pixels>) {
1090        let position = e.position - origin;
1091        self.last_mouse_position = Some(position);
1092        if self.mouse_mode(e.modifiers.shift) {
1093            let point = grid_point(
1094                position,
1095                self.last_content.size,
1096                self.last_content.display_offset,
1097            );
1098            let side = mouse_side(position, self.last_content.size);
1099
1100            if self.mouse_changed(point, side) {
1101                if let Some(bytes) = mouse_moved_report(point, e, self.last_content.mode) {
1102                    self.pty_tx.notify(bytes);
1103                }
1104            }
1105        } else if self.cmd_pressed {
1106            self.word_from_position(Some(position));
1107        }
1108    }
1109
1110    fn word_from_position(&mut self, position: Option<Point<Pixels>>) {
1111        if self.selection_phase == SelectionPhase::Selecting {
1112            self.last_content.last_hovered_word = None;
1113        } else if let Some(position) = position {
1114            self.events
1115                .push_back(InternalEvent::FindHyperlink(position, false));
1116        }
1117    }
1118
1119    pub fn mouse_drag(
1120        &mut self,
1121        e: &MouseMoveEvent,
1122        origin: Point<Pixels>,
1123        region: Bounds<Pixels>,
1124    ) {
1125        let position = e.position - origin;
1126        self.last_mouse_position = Some(position);
1127
1128        if !self.mouse_mode(e.modifiers.shift) {
1129            self.selection_phase = SelectionPhase::Selecting;
1130            // Alacritty has the same ordering, of first updating the selection
1131            // then scrolling 15ms later
1132            self.events
1133                .push_back(InternalEvent::UpdateSelection(position));
1134
1135            // Doesn't make sense to scroll the alt screen
1136            if !self.last_content.mode.contains(TermMode::ALT_SCREEN) {
1137                let scroll_delta = match self.drag_line_delta(e, region) {
1138                    Some(value) => value,
1139                    None => return,
1140                };
1141
1142                let scroll_lines = (scroll_delta / self.last_content.size.line_height) as i32;
1143
1144                self.events
1145                    .push_back(InternalEvent::Scroll(AlacScroll::Delta(scroll_lines)));
1146            }
1147        }
1148    }
1149
1150    fn drag_line_delta(&mut self, e: &MouseMoveEvent, region: Bounds<Pixels>) -> Option<Pixels> {
1151        //TODO: Why do these need to be doubled? Probably the same problem that the IME has
1152        let top = region.origin.y + (self.last_content.size.line_height * 2.);
1153        let bottom = region.lower_left().y - (self.last_content.size.line_height * 2.);
1154        let scroll_delta = if e.position.y < top {
1155            (top - e.position.y).pow(1.1)
1156        } else if e.position.y > bottom {
1157            -((e.position.y - bottom).pow(1.1))
1158        } else {
1159            return None; //Nothing to do
1160        };
1161        Some(scroll_delta)
1162    }
1163
1164    pub fn mouse_down(&mut self, e: &MouseDownEvent, origin: Point<Pixels>) {
1165        let position = e.position - origin;
1166        let point = grid_point(
1167            position,
1168            self.last_content.size,
1169            self.last_content.display_offset,
1170        );
1171
1172        if self.mouse_mode(e.modifiers.shift) {
1173            if let Some(bytes) =
1174                mouse_button_report(point, e.button, e.modifiers, true, self.last_content.mode)
1175            {
1176                self.pty_tx.notify(bytes);
1177            }
1178        } else if e.button == MouseButton::Left {
1179            let position = e.position - origin;
1180            let point = grid_point(
1181                position,
1182                self.last_content.size,
1183                self.last_content.display_offset,
1184            );
1185
1186            // Use .opposite so that selection is inclusive of the cell clicked.
1187            let side = mouse_side(position, self.last_content.size);
1188
1189            let selection_type = match e.click_count {
1190                0 => return, //This is a release
1191                1 => Some(SelectionType::Simple),
1192                2 => Some(SelectionType::Semantic),
1193                3 => Some(SelectionType::Lines),
1194                _ => None,
1195            };
1196
1197            let selection =
1198                selection_type.map(|selection_type| Selection::new(selection_type, point, side));
1199
1200            if let Some(sel) = selection {
1201                self.events
1202                    .push_back(InternalEvent::SetSelection(Some((sel, point))));
1203            }
1204        }
1205    }
1206
1207    pub fn mouse_up(
1208        &mut self,
1209        e: &MouseUpEvent,
1210        origin: Point<Pixels>,
1211        cx: &mut ModelContext<Self>,
1212    ) {
1213        let setting = TerminalSettings::get_global(cx);
1214
1215        let position = e.position - origin;
1216        if self.mouse_mode(e.modifiers.shift) {
1217            let point = grid_point(
1218                position,
1219                self.last_content.size,
1220                self.last_content.display_offset,
1221            );
1222
1223            if let Some(bytes) =
1224                mouse_button_report(point, e.button, e.modifiers, false, self.last_content.mode)
1225            {
1226                self.pty_tx.notify(bytes);
1227            }
1228        } else {
1229            if e.button == MouseButton::Left && setting.copy_on_select {
1230                self.copy();
1231            }
1232
1233            //Hyperlinks
1234            if self.selection_phase == SelectionPhase::Ended {
1235                let mouse_cell_index = content_index_for_mouse(position, &self.last_content.size);
1236                if let Some(link) = self.last_content.cells[mouse_cell_index].hyperlink() {
1237                    cx.open_url(link.uri());
1238                } else if self.cmd_pressed {
1239                    self.events
1240                        .push_back(InternalEvent::FindHyperlink(position, true));
1241                }
1242            }
1243        }
1244
1245        self.selection_phase = SelectionPhase::Ended;
1246        self.last_mouse = None;
1247    }
1248
1249    ///Scroll the terminal
1250    pub fn scroll_wheel(&mut self, e: &ScrollWheelEvent, origin: Point<Pixels>) {
1251        let mouse_mode = self.mouse_mode(e.shift);
1252
1253        if let Some(scroll_lines) = self.determine_scroll_lines(&e, mouse_mode) {
1254            if mouse_mode {
1255                let point = grid_point(
1256                    e.position - origin,
1257                    self.last_content.size,
1258                    self.last_content.display_offset,
1259                );
1260
1261                if let Some(scrolls) =
1262                    scroll_report(point, scroll_lines as i32, &e, self.last_content.mode)
1263                {
1264                    for scroll in scrolls {
1265                        self.pty_tx.notify(scroll);
1266                    }
1267                };
1268            } else if self
1269                .last_content
1270                .mode
1271                .contains(TermMode::ALT_SCREEN | TermMode::ALTERNATE_SCROLL)
1272                && !e.shift
1273            {
1274                self.pty_tx.notify(alt_scroll(scroll_lines))
1275            } else {
1276                if scroll_lines != 0 {
1277                    let scroll = AlacScroll::Delta(scroll_lines);
1278
1279                    self.events.push_back(InternalEvent::Scroll(scroll));
1280                }
1281            }
1282        }
1283    }
1284
1285    fn refresh_hovered_word(&mut self) {
1286        self.word_from_position(self.last_mouse_position);
1287    }
1288
1289    fn determine_scroll_lines(&mut self, e: &ScrollWheelEvent, mouse_mode: bool) -> Option<i32> {
1290        let scroll_multiplier = if mouse_mode { 1. } else { SCROLL_MULTIPLIER };
1291        let line_height = self.last_content.size.line_height;
1292        match e.touch_phase {
1293            /* Reset scroll state on started */
1294            TouchPhase::Started => {
1295                self.scroll_px = px(0.);
1296                None
1297            }
1298            /* Calculate the appropriate scroll lines */
1299            TouchPhase::Moved => {
1300                let old_offset = (self.scroll_px / line_height) as i32;
1301
1302                self.scroll_px += e.delta.pixel_delta(line_height).y * scroll_multiplier;
1303
1304                let new_offset = (self.scroll_px / line_height) as i32;
1305
1306                // Whenever we hit the edges, reset our stored scroll to 0
1307                // so we can respond to changes in direction quickly
1308                self.scroll_px %= self.last_content.size.height();
1309
1310                Some(new_offset - old_offset)
1311            }
1312            TouchPhase::Ended => None,
1313        }
1314    }
1315
1316    pub fn find_matches(
1317        &mut self,
1318        searcher: RegexSearch,
1319        cx: &mut ModelContext<Self>,
1320    ) -> Task<Vec<RangeInclusive<AlacPoint>>> {
1321        let term = self.term.clone();
1322        cx.background_executor().spawn(async move {
1323            let term = term.lock();
1324
1325            all_search_matches(&term, &searcher).collect()
1326        })
1327    }
1328
1329    pub fn title(&self) -> String {
1330        self.foreground_process_info
1331            .as_ref()
1332            .map(|fpi| {
1333                format!(
1334                    "{}{}",
1335                    truncate_and_trailoff(
1336                        &fpi.cwd
1337                            .file_name()
1338                            .map(|name| name.to_string_lossy().to_string())
1339                            .unwrap_or_default(),
1340                        25
1341                    ),
1342                    truncate_and_trailoff(
1343                        &{
1344                            format!(
1345                                "{}{}",
1346                                fpi.name,
1347                                if fpi.argv.len() >= 1 {
1348                                    format!(" {}", (&fpi.argv[1..]).join(" "))
1349                                } else {
1350                                    "".to_string()
1351                                }
1352                            )
1353                        },
1354                        25
1355                    )
1356                )
1357            })
1358            .unwrap_or_else(|| "Terminal".to_string())
1359    }
1360
1361    pub fn can_navigate_to_selected_word(&self) -> bool {
1362        self.cmd_pressed && self.hovered_word
1363    }
1364}
1365
1366impl Drop for Terminal {
1367    fn drop(&mut self) {
1368        self.pty_tx.0.send(Msg::Shutdown).ok();
1369    }
1370}
1371
1372impl EventEmitter<Event> for Terminal {}
1373
1374/// Based on alacritty/src/display/hint.rs > regex_match_at
1375/// Retrieve the match, if the specified point is inside the content matching the regex.
1376fn regex_match_at<T>(term: &Term<T>, point: AlacPoint, regex: &RegexSearch) -> Option<Match> {
1377    visible_regex_match_iter(term, regex).find(|rm| rm.contains(&point))
1378}
1379
1380/// Copied from alacritty/src/display/hint.rs:
1381/// Iterate over all visible regex matches.
1382pub fn visible_regex_match_iter<'a, T>(
1383    term: &'a Term<T>,
1384    regex: &'a RegexSearch,
1385) -> impl Iterator<Item = Match> + 'a {
1386    let viewport_start = Line(-(term.grid().display_offset() as i32));
1387    let viewport_end = viewport_start + term.bottommost_line();
1388    let mut start = term.line_search_left(AlacPoint::new(viewport_start, Column(0)));
1389    let mut end = term.line_search_right(AlacPoint::new(viewport_end, Column(0)));
1390    start.line = start.line.max(viewport_start - MAX_SEARCH_LINES);
1391    end.line = end.line.min(viewport_end + MAX_SEARCH_LINES);
1392
1393    RegexIter::new(start, end, AlacDirection::Right, term, regex)
1394        .skip_while(move |rm| rm.end().line < viewport_start)
1395        .take_while(move |rm| rm.start().line <= viewport_end)
1396}
1397
1398fn make_selection(range: &RangeInclusive<AlacPoint>) -> Selection {
1399    let mut selection = Selection::new(SelectionType::Simple, *range.start(), AlacDirection::Left);
1400    selection.update(*range.end(), AlacDirection::Right);
1401    selection
1402}
1403
1404fn all_search_matches<'a, T>(
1405    term: &'a Term<T>,
1406    regex: &'a RegexSearch,
1407) -> impl Iterator<Item = Match> + 'a {
1408    let start = AlacPoint::new(term.grid().topmost_line(), Column(0));
1409    let end = AlacPoint::new(term.grid().bottommost_line(), term.grid().last_column());
1410    RegexIter::new(start, end, AlacDirection::Right, term, regex)
1411}
1412
1413fn content_index_for_mouse(pos: Point<Pixels>, size: &TerminalSize) -> usize {
1414    let col = (pos.x / size.cell_width()).round() as usize;
1415    let clamped_col = min(col, size.columns() - 1);
1416    let row = (pos.y / size.line_height()).round() as usize;
1417    let clamped_row = min(row, size.screen_lines() - 1);
1418    clamped_row * size.columns() + clamped_col
1419}
1420
1421#[cfg(test)]
1422mod tests {
1423    use alacritty_terminal::{
1424        index::{Column, Line, Point as AlacPoint},
1425        term::cell::Cell,
1426    };
1427    use gpui::{point, size, Pixels};
1428    use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng};
1429
1430    use crate::{content_index_for_mouse, IndexedCell, TerminalContent, TerminalSize};
1431
1432    #[test]
1433    fn test_mouse_to_cell_test() {
1434        let mut rng = thread_rng();
1435        const ITERATIONS: usize = 10;
1436        const PRECISION: usize = 1000;
1437
1438        for _ in 0..ITERATIONS {
1439            let viewport_cells = rng.gen_range(15..20);
1440            let cell_size = rng.gen_range(5 * PRECISION..20 * PRECISION) as f32 / PRECISION as f32;
1441
1442            let size = crate::TerminalSize {
1443                cell_width: Pixels::from(cell_size),
1444                line_height: Pixels::from(cell_size),
1445                size: size(
1446                    Pixels::from(cell_size * (viewport_cells as f32)),
1447                    Pixels::from(cell_size * (viewport_cells as f32)),
1448                ),
1449            };
1450
1451            let cells = get_cells(size, &mut rng);
1452            let content = convert_cells_to_content(size, &cells);
1453
1454            for row in 0..(viewport_cells - 1) {
1455                let row = row as usize;
1456                for col in 0..(viewport_cells - 1) {
1457                    let col = col as usize;
1458
1459                    let row_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
1460                    let col_offset = rng.gen_range(0..PRECISION) as f32 / PRECISION as f32;
1461
1462                    let mouse_pos = point(
1463                        Pixels::from(col as f32 * cell_size + col_offset),
1464                        Pixels::from(row as f32 * cell_size + row_offset),
1465                    );
1466
1467                    let content_index = content_index_for_mouse(mouse_pos, &content.size);
1468                    let mouse_cell = content.cells[content_index].c;
1469                    let real_cell = cells[row][col];
1470
1471                    assert_eq!(mouse_cell, real_cell);
1472                }
1473            }
1474        }
1475    }
1476
1477    #[test]
1478    fn test_mouse_to_cell_clamp() {
1479        let mut rng = thread_rng();
1480
1481        let size = crate::TerminalSize {
1482            cell_width: Pixels::from(10.),
1483            line_height: Pixels::from(10.),
1484            size: size(Pixels::from(100.), Pixels::from(100.)),
1485        };
1486
1487        let cells = get_cells(size, &mut rng);
1488        let content = convert_cells_to_content(size, &cells);
1489
1490        assert_eq!(
1491            content.cells[content_index_for_mouse(
1492                point(Pixels::from(-10.), Pixels::from(-10.)),
1493                &content.size
1494            )]
1495            .c,
1496            cells[0][0]
1497        );
1498        assert_eq!(
1499            content.cells[content_index_for_mouse(
1500                point(Pixels::from(1000.), Pixels::from(1000.)),
1501                &content.size
1502            )]
1503            .c,
1504            cells[9][9]
1505        );
1506    }
1507
1508    fn get_cells(size: TerminalSize, rng: &mut ThreadRng) -> Vec<Vec<char>> {
1509        let mut cells = Vec::new();
1510
1511        for _ in 0..(f32::from(size.height() / size.line_height()) as usize) {
1512            let mut row_vec = Vec::new();
1513            for _ in 0..(f32::from(size.width() / size.cell_width()) as usize) {
1514                let cell_char = rng.sample(Alphanumeric) as char;
1515                row_vec.push(cell_char)
1516            }
1517            cells.push(row_vec)
1518        }
1519
1520        cells
1521    }
1522
1523    fn convert_cells_to_content(size: TerminalSize, cells: &Vec<Vec<char>>) -> TerminalContent {
1524        let mut ic = Vec::new();
1525
1526        for row in 0..cells.len() {
1527            for col in 0..cells[row].len() {
1528                let cell_char = cells[row][col];
1529                ic.push(IndexedCell {
1530                    point: AlacPoint::new(Line(row as i32), Column(col)),
1531                    cell: Cell {
1532                        c: cell_char,
1533                        ..Default::default()
1534                    },
1535                });
1536            }
1537        }
1538
1539        TerminalContent {
1540            cells: ic,
1541            size,
1542            ..Default::default()
1543        }
1544    }
1545}