2178 lines · 75.5 KB · Rust
RawDownload
pub mod mappings; pub use alacritty_terminal; mod pty_info;pub mod terminal_settings; use alacritty_terminal::{    event::{Event as AlacTermEvent, EventListener, Notify, WindowSize},    event_loop::{EventLoop, Msg, Notifier},    grid::{Dimensions, Scroll as AlacScroll},    index::{Boundary, Column, Direction as AlacDirection, Line, Point as AlacPoint},    selection::{Selection, SelectionRange, SelectionType},    sync::FairMutex,    term::{        cell::Cell,        search::{Match, RegexIter, RegexSearch},        Config, RenderableCursor, TermMode,    },    tty::{self},    vi_mode::{ViModeCursor, ViMotion},    vte::ansi::{        ClearMode, CursorStyle as AlacCursorStyle, Handler, NamedPrivateMode, PrivateMode,    },    Term,};use anyhow::{bail, Result}; use futures::{    channel::mpsc::{unbounded, UnboundedReceiver, UnboundedSender},    FutureExt,}; use mappings::mouse::{    alt_scroll, grid_point, grid_point_and_side, mouse_button_report, mouse_moved_report,    scroll_report,}; use collections::{HashMap, VecDeque};use futures::StreamExt;use pty_info::PtyProcessInfo;use serde::{Deserialize, Serialize};use settings::Settings;use smol::channel::{Receiver, Sender};use task::{HideStrategy, Shell, TaskId};use terminal_settings::{AlternateScroll, CursorShape, TerminalSettings};use theme::{ActiveTheme, Theme};use util::{paths::home_dir, truncate_and_trailoff}; use std::{    cmp::{self, min},    fmt::Display,    ops::{Deref, Index, RangeInclusive},    path::PathBuf,    sync::Arc,    time::Duration,};use thiserror::Error; use gpui::{    actions, black, px, AnyWindowHandle, AppContext, Bounds, ClipboardItem, EventEmitter, Hsla,    Keystroke, ModelContext, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,    Pixels, Point, Rgba, ScrollWheelEvent, SharedString, Size, Task, TouchPhase,}; use crate::mappings::{colors::to_alac_rgb, keys::to_esc_str}; actions!(    terminal,    [        Clear,        Copy,        Paste,        ShowCharacterPalette,        SearchTest,        ScrollLineUp,        ScrollLineDown,        ScrollPageUp,        ScrollPageDown,        ScrollToTop,        ScrollToBottom,        ToggleViMode,    ]); ///Scrolling is unbearably sluggish by default. Alacritty supports a configurable///Scroll multiplier that is set to 3 by default. This will be removed when I///Implement scroll bars.#[cfg(target_os = "macos")]const SCROLL_MULTIPLIER: f32 = 4.;#[cfg(not(target_os = "macos"))]const SCROLL_MULTIPLIER: f32 = 1.;const MAX_SEARCH_LINES: usize = 100;const DEBUG_TERMINAL_WIDTH: Pixels = px(500.);const DEBUG_TERMINAL_HEIGHT: Pixels = px(30.);const DEBUG_CELL_WIDTH: Pixels = px(5.);const DEBUG_LINE_HEIGHT: Pixels = px(5.); ///Upward flowing events, for changing the title and such#[derive(Clone, Debug)]pub enum Event {    TitleChanged,    BreadcrumbsChanged,    CloseTerminal,    Bell,    Wakeup,    BlinkChanged(bool),    SelectionsChanged,    NewNavigationTarget(Option<MaybeNavigationTarget>),    Open(MaybeNavigationTarget),} #[derive(Clone, Debug)]pub struct PathLikeTarget {    /// File system path, absolute or relative, existing or not.    /// Might have line and column number(s) attached as `file.rs:1:23`    pub maybe_path: String,    /// Current working directory of the terminal    pub terminal_dir: Option<PathBuf>,} /// A string inside terminal, potentially useful as a URI that can be opened.#[derive(Clone, Debug)]pub enum MaybeNavigationTarget {    /// HTTP, git, etc. string determined by the [`URL_REGEX`] regex.    Url(String),    /// File system path, absolute or relative, existing or not.    /// Might have line and column number(s) attached as `file.rs:1:23`    PathLike(PathLikeTarget),} #[derive(Clone)]enum InternalEvent {    Resize(TerminalSize),    Clear,    // FocusNextMatch,    Scroll(AlacScroll),    ScrollToAlacPoint(AlacPoint),    SetSelection(Option<(Selection, AlacPoint)>),    UpdateSelection(Point<Pixels>),    // Adjusted mouse position, should open    FindHyperlink(Point<Pixels>, bool),    Copy,    // Vi mode events    ToggleViMode,    ViMotion(ViMotion),} ///A translation struct for Alacritty to communicate with us from their event loop#[derive(Clone)]pub struct ZedListener(pub UnboundedSender<AlacTermEvent>); impl EventListener for ZedListener {    fn send_event(&self, event: AlacTermEvent) {        self.0.unbounded_send(event).ok();    }} pub fn init(cx: &mut AppContext) {    TerminalSettings::register(cx);} #[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]pub struct TerminalSize {    pub cell_width: Pixels,    pub line_height: Pixels,    pub size: Size<Pixels>,} impl TerminalSize {    pub fn new(line_height: Pixels, cell_width: Pixels, size: Size<Pixels>) -> Self {        TerminalSize {            cell_width,            line_height,            size,        }    }     pub fn num_lines(&self) -> usize {        (self.size.height / self.line_height).floor() as usize    }     pub fn num_columns(&self) -> usize {        (self.size.width / self.cell_width).floor() as usize    }     pub fn height(&self) -> Pixels {        self.size.height    }     pub fn width(&self) -> Pixels {        self.size.width    }     pub fn cell_width(&self) -> Pixels {        self.cell_width    }     pub fn line_height(&self) -> Pixels {        self.line_height    }} impl Default for TerminalSize {    fn default() -> Self {        TerminalSize::new(            DEBUG_LINE_HEIGHT,            DEBUG_CELL_WIDTH,            Size {                width: DEBUG_TERMINAL_WIDTH,                height: DEBUG_TERMINAL_HEIGHT,            },        )    }} impl From<TerminalSize> for WindowSize {    fn from(val: TerminalSize) -> Self {        WindowSize {            num_lines: val.num_lines() as u16,            num_cols: val.num_columns() as u16,            cell_width: f32::from(val.cell_width()) as u16,            cell_height: f32::from(val.line_height()) as u16,        }    }} impl Dimensions for TerminalSize {    /// Note: this is supposed to be for the back buffer's length,    /// but we exclusively use it to resize the terminal, which does not    /// use this method. We still have to implement it for the trait though,    /// hence, this comment.    fn total_lines(&self) -> usize {        self.screen_lines()    }     fn screen_lines(&self) -> usize {        self.num_lines()    }     fn columns(&self) -> usize {        self.num_columns()    }} #[derive(Error, Debug)]pub struct TerminalError {    pub directory: Option<PathBuf>,    pub shell: Shell,    pub source: std::io::Error,} impl TerminalError {    pub fn fmt_directory(&self) -> String {        self.directory            .clone()            .map(|path| {                match path                    .into_os_string()                    .into_string()                    .map_err(|os_str| format!("<non-utf8 path> {}", os_str.to_string_lossy()))                {                    Ok(s) => s,                    Err(s) => s,                }            })            .unwrap_or_else(|| {                let default_dir =                    dirs::home_dir().map(|buf| buf.into_os_string().to_string_lossy().to_string());                match default_dir {                    Some(dir) => format!("<none specified, using home directory> {}", dir),                    None => "<none specified, could not find home directory>".to_string(),                }            })    }     pub fn fmt_shell(&self) -> String {        match &self.shell {            Shell::System => "<system defined shell>".to_string(),            Shell::Program(s) => s.to_string(),            Shell::WithArguments {                program,                args,                title_override,            } => {                if let Some(title_override) = title_override {                    format!("{} {} ({})", program, args.join(" "), title_override)                } else {                    format!("{} {}", program, args.join(" "))                }            }        }    }} impl Display for TerminalError {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        let dir_string: String = self.fmt_directory();        let shell = self.fmt_shell();         write!(            f,            "Working directory: {} Shell command: `{}`, IOError: {}",            dir_string, shell, self.source        )    }} // https://github.com/alacritty/alacritty/blob/cb3a79dbf6472740daca8440d5166c1d4af5029e/extra/man/alacritty.5.scd?plain=1#L207-L213const DEFAULT_SCROLL_HISTORY_LINES: usize = 10_000;const MAX_SCROLL_HISTORY_LINES: usize = 100_000;const URL_REGEX: &str = r#"(ipfs:|ipns:|magnet:|mailto:|gemini://|gopher://|https://|http://|news:|file://|git://|ssh:|ftp://)[^\u{0000}-\u{001F}\u{007F}-\u{009F}<>"\s{-}\^⟨⟩`]+"#;// Optional suffix matches MSBuild diagnostic suffixes for path parsing in PathLikeWithPosition// https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-diagnostic-format-for-tasksconst WORD_REGEX: &str =    r#"[\$\+\w.\[\]:/\\@\-~()]+(?:\((?:\d+|\d+,\d+)\))|[\$\+\w.\[\]:/\\@\-~()]+"#; pub struct TerminalBuilder {    terminal: Terminal,    events_rx: UnboundedReceiver<AlacTermEvent>,} impl TerminalBuilder {    #[allow(clippy::too_many_arguments)]    pub fn new(        working_directory: Option<PathBuf>,        task: Option<TaskState>,        shell: Shell,        mut env: HashMap<String, String>,        cursor_shape: CursorShape,        alternate_scroll: AlternateScroll,        max_scroll_history_lines: Option<usize>,        is_ssh_terminal: bool,        window: AnyWindowHandle,        completion_tx: Sender<()>,        cx: &AppContext,    ) -> Result<TerminalBuilder> {        // If the parent environment doesn't have a locale set        // (As is the case when launched from a .app on MacOS),        // and the Project doesn't have a locale set, then        // set a fallback for our child environment to use.        if std::env::var("LANG").is_err() {            env.entry("LANG".to_string())                .or_insert_with(|| "en_US.UTF-8".to_string());        }         env.insert("ZED_TERM".to_string(), "true".to_string());        env.insert("TERM_PROGRAM".to_string(), "zed".to_string());        env.insert(            "TERM_PROGRAM_VERSION".to_string(),            release_channel::AppVersion::global(cx).to_string(),        );         let mut terminal_title_override = None;         let pty_options = {            let alac_shell = match shell.clone() {                Shell::System => None,                Shell::Program(program) => {                    Some(alacritty_terminal::tty::Shell::new(program, Vec::new()))                }                Shell::WithArguments {                    program,                    args,                    title_override,                } => {                    terminal_title_override = title_override;                    Some(alacritty_terminal::tty::Shell::new(program, args))                }            };             alacritty_terminal::tty::Options {                shell: alac_shell,                working_directory: working_directory                    .clone()                    .or_else(|| Some(home_dir().to_path_buf())),                hold: false,                env: env.into_iter().collect(),            }        };         // Setup Alacritty's env, which modifies the current process's environment        alacritty_terminal::tty::setup_env();         let default_cursor_style = AlacCursorStyle::from(cursor_shape);        let scrolling_history = if task.is_some() {            // Tasks like `cargo build --all` may produce a lot of output, ergo allow maximum scrolling.            // After the task finishes, we do not allow appending to that terminal, so small tasks output should not            // cause excessive memory usage over time.            MAX_SCROLL_HISTORY_LINES        } else {            max_scroll_history_lines                .unwrap_or(DEFAULT_SCROLL_HISTORY_LINES)                .min(MAX_SCROLL_HISTORY_LINES)        };        let config = Config {            scrolling_history,            default_cursor_style,            ..Config::default()        };         //Spawn a task so the Alacritty EventLoop can communicate with us in a view context        //TODO: Remove with a bounded sender which can be dispatched on &self        let (events_tx, events_rx) = unbounded();        //Set up the terminal...        let mut term = Term::new(            config.clone(),            &TerminalSize::default(),            ZedListener(events_tx.clone()),        );         //Alacritty defaults to alternate scrolling being on, so we just need to turn it off.        if let AlternateScroll::Off = alternate_scroll {            term.unset_private_mode(PrivateMode::Named(NamedPrivateMode::AlternateScroll));        }         let term = Arc::new(FairMutex::new(term));         //Setup the pty...        let pty = match tty::new(            &pty_options,            TerminalSize::default().into(),            window.window_id().as_u64(),        ) {            Ok(pty) => pty,            Err(error) => {                bail!(TerminalError {                    directory: working_directory,                    shell,                    source: error,                });            }        };         let pty_info = PtyProcessInfo::new(&pty);         //And connect them together        let event_loop = EventLoop::new(            term.clone(),            ZedListener(events_tx.clone()),            pty,            pty_options.hold,            false,        )?;         //Kick things off        let pty_tx = event_loop.channel();        let _io_thread = event_loop.spawn(); // DANGER         let terminal = Terminal {            task,            pty_tx: Notifier(pty_tx),            completion_tx,            term,            term_config: config,            title_override: terminal_title_override,            events: VecDeque::with_capacity(10), //Should never get this high.            last_content: Default::default(),            last_mouse: None,            matches: Vec::new(),            selection_head: None,            pty_info,            breadcrumb_text: String::new(),            scroll_px: px(0.),            last_mouse_position: None,            next_link_id: 0,            selection_phase: SelectionPhase::Ended,            secondary_pressed: false,            hovered_word: false,            url_regex: RegexSearch::new(URL_REGEX).unwrap(),            word_regex: RegexSearch::new(WORD_REGEX).unwrap(),            vi_mode_enabled: false,            is_ssh_terminal,        };         Ok(TerminalBuilder {            terminal,            events_rx,        })    }     pub fn subscribe(mut self, cx: &ModelContext<Terminal>) -> Terminal {        //Event loop        cx.spawn(|terminal, mut cx| async move {            while let Some(event) = self.events_rx.next().await {                terminal.update(&mut cx, |terminal, cx| {                    //Process the first event immediately for lowered latency                    terminal.process_event(&event, cx);                })?;                 'outer: loop {                    let mut events = Vec::new();                    let mut timer = cx                        .background_executor()                        .timer(Duration::from_millis(4))                        .fuse();                    let mut wakeup = false;                    loop {                        futures::select_biased! {                            _ = timer => break,

Lines 1–500 of 2178.