1858 lines · 64.8 KB · Rust
RawDownload
mod persistence;pub mod terminal_element;pub mod terminal_panel;pub mod terminal_scrollbar;pub mod terminal_tab_tooltip; use collections::HashSet;use editor::{actions::SelectAll, scroll::ScrollbarAutoHide, Editor, EditorSettings};use futures::{stream::FuturesUnordered, StreamExt};use gpui::{    anchored, deferred, div, impl_actions, AnyElement, App, DismissEvent, Entity, EventEmitter,    FocusHandle, Focusable, KeyContext, KeyDownEvent, Keystroke, MouseButton, MouseDownEvent,    Pixels, Render, ScrollWheelEvent, Stateful, Styled, Subscription, Task, WeakEntity,};use persistence::TERMINAL_DB;use project::{search::SearchQuery, terminals::TerminalKind, Fs, Metadata, Project};use schemars::JsonSchema;use terminal::{    alacritty_terminal::{        index::Point,        term::{search::RegexSearch, TermMode},    },    terminal_settings::{self, CursorShape, TerminalBlink, TerminalSettings, WorkingDirectory},    Clear, Copy, Event, MaybeNavigationTarget, Paste, ScrollLineDown, ScrollLineUp, ScrollPageDown,    ScrollPageUp, ScrollToBottom, ScrollToTop, ShowCharacterPalette, TaskStatus, Terminal,    TerminalBounds, ToggleViMode,};use terminal_element::{is_blank, TerminalElement};use terminal_panel::TerminalPanel;use terminal_scrollbar::TerminalScrollHandle;use terminal_tab_tooltip::TerminalTooltip;use ui::{    h_flex, prelude::*, ContextMenu, Icon, IconName, Label, Scrollbar, ScrollbarState, Tooltip,};use util::{    paths::{PathWithPosition, SanitizedPath},    ResultExt,};use workspace::{    item::{        BreadcrumbText, Item, ItemEvent, SerializableItem, TabContentParams, TabTooltipContent,    },    register_serializable_item,    searchable::{SearchEvent, SearchOptions, SearchableItem, SearchableItemHandle},    CloseActiveItem, NewCenterTerminal, NewTerminal, OpenVisible, ToolbarItemLocation, Workspace,    WorkspaceId,}; use anyhow::Context as _;use serde::Deserialize;use settings::{Settings, SettingsStore};use smol::Timer;use zed_actions::assistant::InlineAssist; use std::{    cmp,    ops::RangeInclusive,    path::{Path, PathBuf},    rc::Rc,    sync::Arc,    time::Duration,}; const REGEX_SPECIAL_CHARS: &[char] = &[    '\\', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '^', '$',]; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const GIT_DIFF_PATH_PREFIXES: &[char] = &['a', 'b']; /// Event to transmit the scroll from the element to the view#[derive(Clone, Debug, PartialEq)]pub struct ScrollTerminal(pub i32); #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq)]pub struct SendText(String); #[derive(Clone, Debug, Default, Deserialize, JsonSchema, PartialEq)]pub struct SendKeystroke(String); impl_actions!(terminal, [SendText, SendKeystroke]); pub fn init(cx: &mut App) {    terminal_panel::init(cx);    terminal::init(cx);     register_serializable_item::<TerminalView>(cx);     cx.observe_new(|workspace: &mut Workspace, _window, _cx| {        workspace.register_action(TerminalView::deploy);    })    .detach();} pub struct BlockProperties {    pub height: u8,    pub render: Box<dyn Send + Fn(&mut BlockContext) -> AnyElement>,} pub struct BlockContext<'a, 'b> {    pub window: &'a mut Window,    pub context: &'b mut App,    pub dimensions: TerminalBounds,} ///A terminal view, maintains the PTY's file handles and communicates with the terminalpub struct TerminalView {    terminal: Entity<Terminal>,    workspace: WeakEntity<Workspace>,    project: WeakEntity<Project>,    focus_handle: FocusHandle,    //Currently using iTerm bell, show bell emoji in tab until input is received    has_bell: bool,    context_menu: Option<(Entity<ContextMenu>, gpui::Point<Pixels>, Subscription)>,    cursor_shape: CursorShape,    blink_state: bool,    blinking_terminal_enabled: bool,    blinking_paused: bool,    blink_epoch: usize,    can_navigate_to_selected_word: bool,    workspace_id: Option<WorkspaceId>,    show_breadcrumbs: bool,    block_below_cursor: Option<Rc<BlockProperties>>,    scroll_top: Pixels,    scrollbar_state: ScrollbarState,    scroll_handle: TerminalScrollHandle,    show_scrollbar: bool,    hide_scrollbar_task: Option<Task<()>>,    _subscriptions: Vec<Subscription>,    _terminal_subscriptions: Vec<Subscription>,} impl EventEmitter<Event> for TerminalView {}impl EventEmitter<ItemEvent> for TerminalView {}impl EventEmitter<SearchEvent> for TerminalView {} impl Focusable for TerminalView {    fn focus_handle(&self, _cx: &App) -> FocusHandle {        self.focus_handle.clone()    }} impl TerminalView {    ///Create a new Terminal in the current working directory or the user's home directory    pub fn deploy(        workspace: &mut Workspace,        _: &NewCenterTerminal,        window: &mut Window,        cx: &mut Context<Workspace>,    ) {        let working_directory = default_working_directory(workspace, cx);        TerminalPanel::add_center_terminal(            workspace,            TerminalKind::Shell(working_directory),            window,            cx,        )        .detach_and_log_err(cx);    }     pub fn new(        terminal: Entity<Terminal>,        workspace: WeakEntity<Workspace>,        workspace_id: Option<WorkspaceId>,        project: WeakEntity<Project>,        window: &mut Window,        cx: &mut Context<Self>,    ) -> Self {        let workspace_handle = workspace.clone();        let terminal_subscriptions =            subscribe_for_terminal_events(&terminal, workspace, window, cx);         let focus_handle = cx.focus_handle();        let focus_in = cx.on_focus_in(&focus_handle, window, |terminal_view, window, cx| {            terminal_view.focus_in(window, cx);        });        let focus_out = cx.on_focus_out(            &focus_handle,            window,            |terminal_view, _event, window, cx| {                terminal_view.focus_out(window, cx);            },        );        let cursor_shape = TerminalSettings::get_global(cx)            .cursor_shape            .unwrap_or_default();         let scroll_handle = TerminalScrollHandle::new(terminal.read(cx));         Self {            terminal,            workspace: workspace_handle,            project,            has_bell: false,            focus_handle,            context_menu: None,            cursor_shape,            blink_state: true,            blinking_terminal_enabled: false,            blinking_paused: false,            blink_epoch: 0,            can_navigate_to_selected_word: false,            workspace_id,            show_breadcrumbs: TerminalSettings::get_global(cx).toolbar.breadcrumbs,            block_below_cursor: None,            scroll_top: Pixels::ZERO,            scrollbar_state: ScrollbarState::new(scroll_handle.clone()),            scroll_handle,            show_scrollbar: !Self::should_autohide_scrollbar(cx),            hide_scrollbar_task: None,            _subscriptions: vec![                focus_in,                focus_out,                cx.observe_global::<SettingsStore>(Self::settings_changed),            ],            _terminal_subscriptions: terminal_subscriptions,        }    }     pub fn entity(&self) -> &Entity<Terminal> {        &self.terminal    }     pub fn has_bell(&self) -> bool {        self.has_bell    }     pub fn clear_bell(&mut self, cx: &mut Context<TerminalView>) {        self.has_bell = false;        cx.emit(Event::Wakeup);    }     pub fn deploy_context_menu(        &mut self,        position: gpui::Point<Pixels>,        window: &mut Window,        cx: &mut Context<Self>,    ) {        let assistant_enabled = self            .workspace            .upgrade()            .and_then(|workspace| workspace.read(cx).panel::<TerminalPanel>(cx))            .map_or(false, |terminal_panel| {                terminal_panel.read(cx).assistant_enabled()            });        let context_menu = ContextMenu::build(window, cx, |menu, _, _| {            menu.context(self.focus_handle.clone())                .action("New Terminal", Box::new(NewTerminal))                .separator()                .action("Copy", Box::new(Copy))                .action("Paste", Box::new(Paste))                .action("Select All", Box::new(SelectAll))                .action("Clear", Box::new(Clear))                .when(assistant_enabled, |menu| {                    menu.separator()                        .action("Inline Assist", Box::new(InlineAssist::default()))                })                .separator()                .action(                    "Close Terminal Tab",                    Box::new(CloseActiveItem {                        save_intent: None,                        close_pinned: true,                    }),                )        });         window.focus(&context_menu.focus_handle(cx));        let subscription = cx.subscribe_in(            &context_menu,            window,            |this, _, _: &DismissEvent, window, cx| {                if this.context_menu.as_ref().is_some_and(|context_menu| {                    context_menu.0.focus_handle(cx).contains_focused(window, cx)                }) {                    cx.focus_self(window);                }                this.context_menu.take();                cx.notify();            },        );         self.context_menu = Some((context_menu, position, subscription));    }     fn settings_changed(&mut self, cx: &mut Context<Self>) {        let settings = TerminalSettings::get_global(cx);        self.show_breadcrumbs = settings.toolbar.breadcrumbs;         let new_cursor_shape = settings.cursor_shape.unwrap_or_default();        let old_cursor_shape = self.cursor_shape;        if old_cursor_shape != new_cursor_shape {            self.cursor_shape = new_cursor_shape;            self.terminal.update(cx, |term, _| {                term.set_cursor_shape(self.cursor_shape);            });        }         cx.notify();    }     fn show_character_palette(        &mut self,        _: &ShowCharacterPalette,        window: &mut Window,        cx: &mut Context<Self>,    ) {        if self            .terminal            .read(cx)            .last_content            .mode            .contains(TermMode::ALT_SCREEN)        {            self.terminal.update(cx, |term, cx| {                term.try_keystroke(                    &Keystroke::parse("ctrl-cmd-space").unwrap(),                    TerminalSettings::get_global(cx).option_as_meta,                )            });        } else {            window.show_character_palette();        }    }     fn select_all(&mut self, _: &SelectAll, _: &mut Window, cx: &mut Context<Self>) {        self.terminal.update(cx, |term, _| term.select_all());        cx.notify();    }     fn clear(&mut self, _: &Clear, _: &mut Window, cx: &mut Context<Self>) {        self.scroll_top = px(0.);        self.terminal.update(cx, |term, _| term.clear());        cx.notify();    }     fn max_scroll_top(&self, cx: &App) -> Pixels {        let terminal = self.terminal.read(cx);         let Some(block) = self.block_below_cursor.as_ref() else {            return Pixels::ZERO;        };         let line_height = terminal.last_content().terminal_bounds.line_height;        let mut terminal_lines = terminal.total_lines();        let viewport_lines = terminal.viewport_lines();        if terminal.total_lines() == terminal.viewport_lines() {            let mut last_line = None;            for cell in terminal.last_content.cells.iter().rev() {                if !is_blank(cell) {                    break;                }                 let last_line = last_line.get_or_insert(cell.point.line);                if *last_line != cell.point.line {                    terminal_lines -= 1;                }                *last_line = cell.point.line;            }        }         let max_scroll_top_in_lines =            (block.height as usize).saturating_sub(viewport_lines.saturating_sub(terminal_lines));         max_scroll_top_in_lines as f32 * line_height    }     fn scroll_wheel(&mut self, event: &ScrollWheelEvent, cx: &mut Context<Self>) {        let terminal_content = self.terminal.read(cx).last_content();         if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {            let line_height = terminal_content.terminal_bounds.line_height;            let y_delta = event.delta.pixel_delta(line_height).y;            if y_delta < Pixels::ZERO || self.scroll_top > Pixels::ZERO {                self.scroll_top = cmp::max(                    Pixels::ZERO,                    cmp::min(self.scroll_top - y_delta, self.max_scroll_top(cx)),                );                cx.notify();                return;            }        }         self.terminal.update(cx, |term, _| term.scroll_wheel(event));    }     fn scroll_line_up(&mut self, _: &ScrollLineUp, _: &mut Window, cx: &mut Context<Self>) {        let terminal_content = self.terminal.read(cx).last_content();        if self.block_below_cursor.is_some()            && terminal_content.display_offset == 0            && self.scroll_top > Pixels::ZERO        {            let line_height = terminal_content.terminal_bounds.line_height;            self.scroll_top = cmp::max(self.scroll_top - line_height, Pixels::ZERO);            return;        }         self.terminal.update(cx, |term, _| term.scroll_line_up());        cx.notify();    }     fn scroll_line_down(&mut self, _: &ScrollLineDown, _: &mut Window, cx: &mut Context<Self>) {        let terminal_content = self.terminal.read(cx).last_content();        if self.block_below_cursor.is_some() && terminal_content.display_offset == 0 {            let max_scroll_top = self.max_scroll_top(cx);            if self.scroll_top < max_scroll_top {                let line_height = terminal_content.terminal_bounds.line_height;                self.scroll_top = cmp::min(self.scroll_top + line_height, max_scroll_top);            }            return;        }         self.terminal.update(cx, |term, _| term.scroll_line_down());        cx.notify();    }     fn scroll_page_up(&mut self, _: &ScrollPageUp, _: &mut Window, cx: &mut Context<Self>) {        if self.scroll_top == Pixels::ZERO {            self.terminal.update(cx, |term, _| term.scroll_page_up());        } else {            let line_height = self                .terminal                .read(cx)                .last_content                .terminal_bounds                .line_height();            let visible_block_lines = (self.scroll_top / line_height) as usize;            let viewport_lines = self.terminal.read(cx).viewport_lines();            let visible_content_lines = viewport_lines - visible_block_lines;             if visible_block_lines >= viewport_lines {                self.scroll_top = ((visible_block_lines - viewport_lines) as f32) * line_height;            } else {                self.scroll_top = px(0.);                self.terminal                    .update(cx, |term, _| term.scroll_up_by(visible_content_lines));            }        }        cx.notify();    }     fn scroll_page_down(&mut self, _: &ScrollPageDown, _: &mut Window, cx: &mut Context<Self>) {        self.terminal.update(cx, |term, _| term.scroll_page_down());        let terminal = self.terminal.read(cx);        if terminal.last_content().display_offset < terminal.viewport_lines() {            self.scroll_top = self.max_scroll_top(cx);        }        cx.notify();    }     fn scroll_to_top(&mut self, _: &ScrollToTop, _: &mut Window, cx: &mut Context<Self>) {        self.terminal.update(cx, |term, _| term.scroll_to_top());        cx.notify();    }     fn scroll_to_bottom(&mut self, _: &ScrollToBottom, _: &mut Window, cx: &mut Context<Self>) {        self.terminal.update(cx, |term, _| term.scroll_to_bottom());        if self.block_below_cursor.is_some() {            self.scroll_top = self.max_scroll_top(cx);        }        cx.notify();    }     fn toggle_vi_mode(&mut self, _: &ToggleViMode, _: &mut Window, cx: &mut Context<Self>) {        self.terminal.update(cx, |term, _| term.toggle_vi_mode());        cx.notify();    }     pub fn should_show_cursor(&self, focused: bool, cx: &mut Context<Self>) -> bool {        //Don't blink the cursor when not focused, blinking is disabled, or paused        if !focused            || self.blinking_paused            || self                .terminal                .read(cx)                .last_content                .mode                .contains(TermMode::ALT_SCREEN)        {            return true;        }         match TerminalSettings::get_global(cx).blinking {            //If the user requested to never blink, don't blink it.            TerminalBlink::Off => true,            //If the terminal is controlling it, check terminal mode            TerminalBlink::TerminalControlled => {                !self.blinking_terminal_enabled || self.blink_state            }            TerminalBlink::On => self.blink_state,        }    }     fn blink_cursors(&mut self, epoch: usize, window: &mut Window, cx: &mut Context<Self>) {        if epoch == self.blink_epoch && !self.blinking_paused {            self.blink_state = !self.blink_state;            cx.notify();             let epoch = self.next_blink_epoch();

Lines 1–500 of 1858.