#![allow(rustdoc::private_intra_doc_links)] //! This is the place where everything editor-related is stored (data-wise) and displayed (ui-wise). //! The main point of interest in this crate is [`Editor`] type, which is used in every other Zed part as a user input element. //! It comes in different flavors: single line, multiline and a fixed height one. //! //! Editor contains of multiple large submodules: //! * [`element`] — the place where all rendering happens //! * [`display_map`] - chunks up text in the editor into the logical blocks, establishes coordinates and mapping between each of them. //! Contains all metadata related to text transformations (folds, fake inlay text insertions, soft wraps, tab markup, etc.). //! * [`inlay_hint_cache`] - is a storage of inlay hints out of LSP requests, responsible for querying LSP and updating `display_map`'s state accordingly. //! //! All other submodules and structs are mostly concerned with holding editor data about the way it displays current buffer region(s). //! //! If you're looking to improve Vim mode, you should check out Vim crate that wraps Editor and overrides its behavior. pub mod actions; mod blame_entry_tooltip; mod blink_manager; mod clangd_ext; mod debounced_delay; pub mod display_map; mod editor_settings; mod editor_settings_controls; mod element; mod git; mod highlight_matching_bracket; mod hover_links; mod hover_popover; mod hunk_diff; mod indent_guides; mod inlay_hint_cache; pub mod items; mod linked_editing_ranges; mod lsp_ext; mod mouse_context_menu; pub mod movement; mod persistence; mod proposed_changes_editor; mod rust_analyzer_ext; pub mod scroll; mod selections_collection; pub mod tasks; #[cfg(test)] mod editor_tests; mod signature_help; #[cfg(any(test, feature = "test-support"))] pub mod test; use ::git::diff::DiffHunkStatus; pub(crate) use actions::*; pub use actions::{OpenExcerpts, OpenExcerptsSplit}; use aho_corasick::AhoCorasick; use anyhow::{anyhow, Context as _, Result}; use blink_manager::BlinkManager; use client::{Collaborator, ParticipantIndex}; use clock::ReplicaId; use collections::{BTreeMap, Bound, HashMap, HashSet, VecDeque}; use convert_case::{Case, Casing}; use debounced_delay::DebouncedDelay; use display_map::*; pub use display_map::{DisplayPoint, FoldPlaceholder}; pub use editor_settings::{ CurrentLineHighlight, EditorSettings, ScrollBeyondLastLine, SearchSettings, ShowScrollbar, }; pub use editor_settings_controls::*; use element::LineWithInvisibles; pub use element::{ CursorLayout, EditorElement, HighlightedRange, HighlightedRangeLine, PointForPosition, }; use futures::{future, FutureExt}; use fuzzy::{StringMatch, StringMatchCandidate}; use git::blame::GitBlame; use gpui::{ div, impl_actions, point, prelude::*, px, relative, size, uniform_list, Action, AnyElement, AppContext, AsyncWindowContext, AvailableSpace, BackgroundExecutor, Bounds, ClipboardEntry, ClipboardItem, Context, DispatchPhase, ElementId, EventEmitter, FocusHandle, FocusOutEvent, FocusableView, FontId, FontWeight, Global, HighlightStyle, Hsla, InteractiveText, KeyContext, ListSizingBehavior, Model, ModelContext, MouseButton, PaintQuad, ParentElement, Pixels, Render, ScrollStrategy, SharedString, Size, StrikethroughStyle, Styled, StyledText, Subscription, Task, TextStyle, TextStyleRefinement, UTF16Selection, UnderlineStyle, UniformListScrollHandle, View, ViewContext, ViewInputHandler, VisualContext, WeakFocusHandle, WeakView, WindowContext, }; use highlight_matching_bracket::refresh_matching_bracket_highlights; use hover_popover::{hide_hover, HoverState}; pub(crate) use hunk_diff::HoveredHunk; use hunk_diff::{diff_hunk_to_display, ExpandedHunks}; use indent_guides::ActiveIndentGuidesState; use inlay_hint_cache::{InlayHintCache, InlaySplice, InvalidationStrategy}; pub use inline_completion::Direction; use inline_completion::{InlayProposal, InlineCompletionProvider, InlineCompletionProviderHandle}; pub use items::MAX_TAB_TITLE_LEN; use itertools::Itertools; use language::{ language_settings::{self, all_language_settings, language_settings, InlayHintSettings}, markdown, point_from_lsp, AutoindentMode, BracketPair, Buffer, Capability, CharKind, CodeLabel, CursorShape, Diagnostic, Documentation, IndentKind, IndentSize, Language, OffsetRangeExt, Point, Selection, SelectionGoal, TransactionId, }; use language::{point_to_lsp, BufferRow, CharClassifier, Runnable, RunnableRange}; use linked_editing_ranges::refresh_linked_ranges; pub use proposed_changes_editor::{ ProposedChangeLocation, ProposedChangesEditor, ProposedChangesEditorToolbar, }; use similar::{ChangeTag, TextDiff}; use std::iter::Peekable; use task::{ResolvedTask, TaskTemplate, TaskVariables}; use hover_links::{find_file, HoverLink, HoveredLinkState, InlayHighlight}; pub use lsp::CompletionContext; use lsp::{ CompletionItemKind, CompletionTriggerKind, DiagnosticSeverity, InsertTextFormat, LanguageServerId, LanguageServerName, }; use mouse_context_menu::MouseContextMenu; use movement::TextLayoutDetails; pub use multi_buffer::{ Anchor, AnchorRangeExt, ExcerptId, ExcerptRange, MultiBuffer, MultiBufferSnapshot, ToOffset, ToPoint, }; use multi_buffer::{ ExpandExcerptDirection, MultiBufferDiffHunk, MultiBufferPoint, MultiBufferRow, ToOffsetUtf16, }; use ordered_float::OrderedFloat; use parking_lot::{Mutex, RwLock}; use project::{ lsp_store::{FormatTarget, FormatTrigger}, project_settings::{GitGutterSetting, ProjectSettings}, CodeAction, Completion, CompletionIntent, DocumentHighlight, InlayHint, Item, Location, LocationLink, Project, ProjectTransaction, TaskSourceKind, }; use rand::prelude::*; use rpc::{proto::*, ErrorExt}; use scroll::{Autoscroll, OngoingScroll, ScrollAnchor, ScrollManager, ScrollbarAutoHide}; use selections_collection::{ resolve_selections, MutableSelectionsCollection, SelectionsCollection, }; use serde::{Deserialize, Serialize}; use settings::{update_settings_file, Settings, SettingsLocation, SettingsStore}; use smallvec::SmallVec; use snippet::Snippet; use std::{ any::TypeId, borrow::Cow, cell::RefCell, cmp::{self, Ordering, Reverse}, mem, num::NonZeroU32, ops::{ControlFlow, Deref, DerefMut, Not as _, Range, RangeInclusive}, path::{Path, PathBuf}, rc::Rc, sync::Arc, time::{Duration, Instant}, }; pub use sum_tree::Bias; use sum_tree::TreeMap; use text::{BufferId, OffsetUtf16, Rope}; use theme::{ observe_buffer_font_size_adjustment, ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, ThemeColors, ThemeSettings, }; use ui::{ h_flex, prelude::*, ButtonSize, ButtonStyle, Disclosure, IconButton, IconName, IconSize, ListItem, Popover, PopoverMenuHandle, Tooltip, }; use util::{defer, maybe, post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::item::{ItemHandle, PreviewTabsSettings}; use workspace::notifications::{DetachAndPromptErr, NotificationId, NotifyTaskExt}; use workspace::{ searchable::SearchEvent, ItemNavHistory, SplitDirection, ViewId, Workspace, WorkspaceId, }; use workspace::{Item as WorkspaceItem, OpenInTerminal, OpenTerminal, TabBarSettings, Toast}; use crate::hover_links::find_url; use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState}; pub const FILE_HEADER_HEIGHT: u32 = 2; pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u32 = 1; pub const MULTI_BUFFER_EXCERPT_FOOTER_HEIGHT: u32 = 1; pub const DEFAULT_MULTIBUFFER_CONTEXT: u32 = 2; const CURSOR_BLINK_INTERVAL: Duration = Duration::from_millis(500); const MAX_LINE_LEN: usize = 1024; const MIN_NAVIGATION_HISTORY_ROW_DELTA: i64 = 10; const MAX_SELECTION_HISTORY_LEN: usize = 1024; pub(crate) const CURSORS_VISIBLE_FOR: Duration = Duration::from_millis(2000); #[doc(hidden)] pub const CODE_ACTIONS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(250); #[doc(hidden)] pub const DOCUMENT_HIGHLIGHTS_DEBOUNCE_TIMEOUT: Duration = Duration::from_millis(75); pub(crate) const FORMAT_TIMEOUT: Duration = Duration::from_secs(2); pub(crate) const SCROLL_CENTER_TOP_BOTTOM_DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1); pub fn render_parsed_markdown( element_id: impl Into, parsed: &language::ParsedMarkdown, editor_style: &EditorStyle, workspace: Option>, cx: &mut WindowContext, ) -> InteractiveText { let code_span_background_color = cx .theme() .colors() .editor_document_highlight_read_background; let highlights = gpui::combine_highlights( parsed.highlights.iter().filter_map(|(range, highlight)| { let highlight = highlight.to_highlight_style(&editor_style.syntax)?; Some((range.clone(), highlight)) }), parsed .regions .iter() .zip(&parsed.region_ranges) .filter_map(|(region, range)| { if region.code { Some(( range.clone(), HighlightStyle { background_color: Some(code_span_background_color), ..Default::default() }, )) } else { None } }), ); let mut links = Vec::new(); let mut link_ranges = Vec::new(); for (range, region) in parsed.region_ranges.iter().zip(&parsed.regions) { if let Some(link) = region.link.clone() { links.push(link); link_ranges.push(range.clone()); } } InteractiveText::new( element_id, StyledText::new(parsed.text.clone()).with_highlights(&editor_style.text, highlights), ) .on_click(link_ranges, move |clicked_range_ix, cx| { match &links[clicked_range_ix] { markdown::Link::Web { url } => cx.open_url(url), markdown::Link::Path { path } => { if let Some(workspace) = &workspace { _ = workspace.update(cx, |workspace, cx| { workspace.open_abs_path(path.clone(), false, cx).detach(); }); } } } }) } #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] pub(crate) enum InlayId { Suggestion(usize), Hint(usize), } impl InlayId { fn id(&self) -> usize { match self { Self::Suggestion(id) => *id, Self::Hint(id) => *id, } } } enum DiffRowHighlight {} enum DocumentHighlightRead {} enum DocumentHighlightWrite {} enum InputComposition {} #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum Navigated { Yes, No, } impl Navigated { pub fn from_bool(yes: bool) -> Navigated { if yes { Navigated::Yes } else { Navigated::No } } } pub fn init_settings(cx: &mut AppContext) { EditorSettings::register(cx); } pub fn init(cx: &mut AppContext) { init_settings(cx); workspace::register_project_item::(cx); workspace::FollowableViewRegistry::register::(cx); workspace::register_serializable_item::(cx); cx.observe_new_views( |workspace: &mut Workspace, _cx: &mut ViewContext| { workspace.register_action(Editor::new_file); workspace.register_action(Editor::new_file_vertical); workspace.register_action(Editor::new_file_horizontal); }, ) .detach(); cx.on_action(move |_: &workspace::NewFile, cx| { let app_state = workspace::AppState::global(cx); if let Some(app_state) = app_state.upgrade() { workspace::open_new(Default::default(), app_state, cx, |workspace, cx| { Editor::new_file(workspace, &Default::default(), cx) }) .detach(); } }); cx.on_action(move |_: &workspace::NewWindow, cx| { let app_state = workspace::AppState::global(cx); if let Some(app_state) = app_state.upgrade() { workspace::open_new(Default::default(), app_state, cx, |workspace, cx| { Editor::new_file(workspace, &Default::default(), cx) }) .detach(); } }); } pub struct SearchWithinRange; trait InvalidationRegion { fn ranges(&self) -> &[Range]; } #[derive(Clone, Debug, PartialEq)] pub enum SelectPhase { Begin { position: DisplayPoint, add: bool, click_count: usize, }, BeginColumnar { position: DisplayPoint, reset: bool, goal_column: u32, }, Extend { position: DisplayPoint, click_count: usize, }, Update { position: DisplayPoint, goal_column: u32, scroll_delta: gpui::Point, }, End, } #[derive(Clone, Debug)] pub enum SelectMode { Character, Word(Range), Line(Range), All, } #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum EditorMode { SingleLine { auto_width: bool }, AutoHeight { max_lines: usize }, Full, } #[derive(Copy, Clone, Debug)] pub enum SoftWrap { /// Prefer not to wrap at all. /// /// Note: this is currently internal, as actually limited by [`crate::MAX_LINE_LEN`] until it wraps. /// The mode is used inside git diff hunks, where it's seems currently more useful to not wrap as much as possible. GitDiff, /// Prefer a single line generally, unless an overly long line is encountered. None, /// Soft wrap lines that exceed the editor width. EditorWidth, /// Soft wrap lines at the preferred line length. Column(u32), /// Soft wrap line at the preferred line length or the editor width (whichever is smaller). Bounded(u32), } #[derive(Clone)] pub struct EditorStyle { pub background: Hsla, pub local_player: PlayerColor, pub text: TextStyle, pub scrollbar_width: Pixels, pub syntax: Arc, pub status: StatusColors, pub inlay_hints_style: HighlightStyle, pub suggestions_style: HighlightStyle, pub unnecessary_code_fade: f32, } impl Default for EditorStyle { fn default() -> Self { Self { background: Hsla::default(), local_player: PlayerColor::default(), text: TextStyle::default(), scrollbar_width: Pixels::default(), syntax: Default::default(), // HACK: Status colors don't have a real default. // We should look into removing the status colors from the editor // style and retrieve them directly from the theme. status: StatusColors::dark(), inlay_hints_style: HighlightStyle::default(), suggestions_style: HighlightStyle::default(), unnecessary_code_fade: Default::default(), } } } pub fn make_inlay_hints_style(cx: &WindowContext) -> HighlightStyle { let show_background = language_settings::language_settings(None, None, cx) .inlay_hints .show_background; HighlightStyle { color: Some(cx.theme().status().hint), background_color: show_background.then(|| cx.theme().status().hint_background), ..HighlightStyle::default() } } type CompletionId = usize; #[derive(Clone, Debug)] struct CompletionState { // render_inlay_ids represents the inlay hints that are inserted // for rendering the inline completions. They may be discontinuous // in the event that the completion provider returns some intersection // with the existing content. render_inlay_ids: Vec, // text is the resulting rope that is inserted when the user accepts a completion. text: Rope, // position is the position of the cursor when the completion was triggered. position: multi_buffer::Anchor, // delete_range is the range of text that this completion state covers. // if the completion is accepted, this range should be deleted. delete_range: Option>, } #[derive(Copy, Clone, Eq, PartialEq, PartialOrd, Ord, Debug, Default)] struct EditorActionId(usize); impl EditorActionId { pub fn post_inc(&mut self) -> Self { let answer = self.0; *self = Self(answer + 1); Self(answer) } } // type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor; // type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option; type BackgroundHighlight = (fn(&ThemeColors) -> Hsla, Arc<[Range]>); type GutterHighlight = (fn(&AppContext) -> Hsla, Arc<[Range]>); #[derive(Default)] struct ScrollbarMarkerState { scrollbar_size: Size, dirty: bool, markers: Arc<[PaintQuad]>, pending_refresh: Option>>, } impl ScrollbarMarkerState { fn should_refresh(&self, scrollbar_size: Size) -> bool { self.pending_refresh.is_none() && (self.scrollbar_size != scrollbar_size || self.dirty) } } #[derive(Clone, Debug)] struct RunnableTasks { templates: Vec<(TaskSourceKind, TaskTemplate)>, offset: MultiBufferOffset, // We need the column at which the task context evaluation should take place (when we're spawning it via gutter). column: u32, // Values of all named captures, including those starting with '_' extra_variables: HashMap, // Full range of the tagged region. We use it to determine which `extra_variables` to grab for context resolution in e.g. a modal. context_range: Range, } impl RunnableTasks { fn resolve<'a>( &'a self, cx: &'a task::TaskContext, ) -> impl Iterator + 'a { self.templates.iter().filter_map(|(kind, template)| { template .resolve_task(&kind.to_id_base(), cx) .map(|task| (kind.clone(), task)) }) } } #[derive(Clone)] struct ResolvedTasks { templates: SmallVec<[(TaskSourceKind, ResolvedTask); 1]>, position: Anchor, } #[derive(Copy, Clone, Debug)] struct MultiBufferOffset(usize); #[derive(Copy, Clone, Debug, PartialEq, PartialOrd)] struct BufferOffset(usize); // Addons allow storing per-editor state in other crates (e.g. Vim) pub trait Addon: 'static { fn extend_key_context(&self, _: &mut KeyContext, _: &AppContext) {} fn to_any(&self) -> &dyn std::any::Any; } #[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum IsVimMode { Yes, No, } pub trait ActiveLineTrailerProvider { fn render_active_line_trailer( &mut self, style: &EditorStyle, focus_handle: &FocusHandle, cx: &mut WindowContext, ) -> Option; } /// Zed's primary text input `View`, allowing users to edit a [`MultiBuffer`] /// /// See the [module level documentation](self) for more information. pub struct Editor { focus_handle: FocusHandle, last_focused_descendant: Option, /// The text buffer being edited buffer: Model, /// Map of how text in the buffer should be displayed. /// Handles soft wraps, folds, fake inlay text insertions, etc. pub display_map: Model, pub selections: SelectionsCollection, pub scroll_manager: ScrollManager, /// When inline assist editors are linked, they all render cursors because /// typing enters text into each of them, even the ones that aren't focused. pub(crate) show_cursor_when_unfocused: bool, columnar_selection_tail: Option, add_selections_state: Option, select_next_state: Option, select_prev_state: Option, selection_history: SelectionHistory, autoclose_regions: Vec, snippet_stack: InvalidationStack, select_larger_syntax_node_stack: Vec]>>, ime_transaction: Option, active_diagnostics: Option, soft_wrap_mode_override: Option, project: Option>, semantics_provider: Option>, completion_provider: Option>, collaboration_hub: Option>, blink_manager: Model, show_cursor_names: bool, hovered_cursors: HashMap>, pub show_local_selections: bool, mode: EditorMode, show_breadcrumbs: bool, show_gutter: bool, show_line_numbers: Option, use_relative_line_numbers: Option, show_git_diff_gutter: Option, show_code_actions: Option, show_runnables: Option, show_wrap_guides: Option, show_indent_guides: Option, placeholder_text: Option>, highlight_order: usize, highlighted_rows: HashMap>, background_highlights: TreeMap, gutter_highlights: TreeMap, scrollbar_marker_state: ScrollbarMarkerState, active_indent_guides_state: ActiveIndentGuidesState, nav_history: Option, context_menu: RwLock>, mouse_context_menu: Option, hunk_controls_menu_handle: PopoverMenuHandle, completion_tasks: Vec<(CompletionId, Task>)>, signature_help_state: SignatureHelpState, auto_signature_help: Option, find_all_references_task_sources: Vec, next_completion_id: CompletionId, completion_documentation_pre_resolve_debounce: DebouncedDelay, available_code_actions: Option<(Location, Arc<[AvailableCodeAction]>)>, code_actions_task: Option>>, document_highlights_task: Option>, linked_editing_range_task: Option>>, linked_edit_ranges: linked_editing_ranges::LinkedEditingRanges, pending_rename: Option, searchable: bool, cursor_shape: CursorShape, current_line_highlight: Option, collapse_matches: bool, autoindent_mode: Option, workspace: Option<(WeakView, Option)>, input_enabled: bool, use_modal_editing: bool, read_only: bool, leader_peer_id: Option, remote_id: Option, hover_state: HoverState, gutter_hovered: bool, hovered_link_state: Option, inline_completion_provider: Option, code_action_providers: Vec>, active_inline_completion: Option, // enable_inline_completions is a switch that Vim can use to disable // inline completions based on its mode. enable_inline_completions: bool, show_inline_completions_override: Option, inlay_hint_cache: InlayHintCache, expanded_hunks: ExpandedHunks, next_inlay_id: usize, _subscriptions: Vec, pixel_position_of_newest_cursor: Option>, gutter_dimensions: GutterDimensions, style: Option, text_style_refinement: Option, next_editor_action_id: EditorActionId, editor_actions: Rc)>>>>, use_autoclose: bool, use_auto_surround: bool, auto_replace_emoji_shortcode: bool, show_git_blame_gutter: bool, show_git_blame_inline: bool, show_git_blame_inline_delay_task: Option>, git_blame_inline_enabled: bool, serialize_dirty_buffers: bool, show_selection_menu: Option, blame: Option>, blame_subscription: Option, custom_context_menu: Option< Box< dyn 'static + Fn(&mut Self, DisplayPoint, &mut ViewContext) -> Option>, >, >, last_bounds: Option>, expect_bounds_change: Option>, tasks: BTreeMap<(BufferId, BufferRow), RunnableTasks>, tasks_update_task: Option>, previous_search_ranges: Option]>>, breadcrumb_header: Option, focused_block: Option, next_scroll_position: NextScrollCursorCenterTopBottom, addons: HashMap>, _scroll_cursor_center_top_bottom_task: Task<()>, active_line_trailer_provider: Option>, } #[derive(Copy, Clone, Debug, PartialEq, Eq, Default)] enum NextScrollCursorCenterTopBottom { #[default] Center, Top, Bottom, } impl NextScrollCursorCenterTopBottom { fn next(&self) -> Self { match self { Self::Center => Self::Top, Self::Top => Self::Bottom, Self::Bottom => Self::Center, } } } #[derive(Clone)] pub struct EditorSnapshot { pub mode: EditorMode, show_gutter: bool, show_line_numbers: Option, show_git_diff_gutter: Option, show_code_actions: Option, show_runnables: Option, git_blame_gutter_max_author_length: Option, pub display_snapshot: DisplaySnapshot, pub placeholder_text: Option>, is_focused: bool, scroll_anchor: ScrollAnchor, ongoing_scroll: OngoingScroll, current_line_highlight: CurrentLineHighlight, gutter_hovered: bool, } const GIT_BLAME_MAX_AUTHOR_CHARS_DISPLAYED: usize = 20; #[derive(Default, Debug, Clone, Copy)] pub struct GutterDimensions { pub left_padding: Pixels, pub right_padding: Pixels, pub width: Pixels, pub margin: Pixels, pub git_blame_entries_width: Option, } impl GutterDimensions { /// The full width of the space taken up by the gutter. pub fn full_width(&self) -> Pixels { self.margin + self.width } /// The width of the space reserved for the fold indicators, /// use alongside 'justify_end' and `gutter_width` to /// right align content with the line numbers pub fn fold_area_width(&self) -> Pixels { self.margin + self.right_padding } } #[derive(Debug)] pub struct RemoteSelection { pub replica_id: ReplicaId, pub selection: Selection, pub cursor_shape: CursorShape, pub peer_id: PeerId, pub line_mode: bool, pub participant_index: Option, pub user_name: Option, } #[derive(Clone, Debug)] struct SelectionHistoryEntry { selections: Arc<[Selection]>, select_next_state: Option, select_prev_state: Option, add_selections_state: Option, } enum SelectionHistoryMode { Normal, Undoing, Redoing, } #[derive(Clone, PartialEq, Eq, Hash)] struct HoveredCursor { replica_id: u16, selection_id: usize, } impl Default for SelectionHistoryMode { fn default() -> Self { Self::Normal } } #[derive(Default)] struct SelectionHistory { #[allow(clippy::type_complexity)] selections_by_transaction: HashMap]>, Option]>>)>, mode: SelectionHistoryMode, undo_stack: VecDeque, redo_stack: VecDeque, } impl SelectionHistory { fn insert_transaction( &mut self, transaction_id: TransactionId, selections: Arc<[Selection]>, ) { self.selections_by_transaction .insert(transaction_id, (selections, None)); } #[allow(clippy::type_complexity)] fn transaction( &self, transaction_id: TransactionId, ) -> Option<&(Arc<[Selection]>, Option]>>)> { self.selections_by_transaction.get(&transaction_id) } #[allow(clippy::type_complexity)] fn transaction_mut( &mut self, transaction_id: TransactionId, ) -> Option<&mut (Arc<[Selection]>, Option]>>)> { self.selections_by_transaction.get_mut(&transaction_id) } fn push(&mut self, entry: SelectionHistoryEntry) { if !entry.selections.is_empty() { match self.mode { SelectionHistoryMode::Normal => { self.push_undo(entry); self.redo_stack.clear(); } SelectionHistoryMode::Undoing => self.push_redo(entry), SelectionHistoryMode::Redoing => self.push_undo(entry), } } } fn push_undo(&mut self, entry: SelectionHistoryEntry) { if self .undo_stack .back() .map_or(true, |e| e.selections != entry.selections) { self.undo_stack.push_back(entry); if self.undo_stack.len() > MAX_SELECTION_HISTORY_LEN { self.undo_stack.pop_front(); } } } fn push_redo(&mut self, entry: SelectionHistoryEntry) { if self .redo_stack .back() .map_or(true, |e| e.selections != entry.selections) { self.redo_stack.push_back(entry); if self.redo_stack.len() > MAX_SELECTION_HISTORY_LEN { self.redo_stack.pop_front(); } } } } struct RowHighlight { index: usize, range: Range, color: Hsla, should_autoscroll: bool, } #[derive(Clone, Debug)] struct AddSelectionsState { above: bool, stack: Vec, } #[derive(Clone)] struct SelectNextState { query: AhoCorasick, wordwise: bool, done: bool, } impl std::fmt::Debug for SelectNextState { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct(std::any::type_name::()) .field("wordwise", &self.wordwise) .field("done", &self.done) .finish() } } #[derive(Debug)] struct AutocloseRegion { selection_id: usize, range: Range, pair: BracketPair, } #[derive(Debug)] struct SnippetState { ranges: Vec>>, active_index: usize, choices: Vec>>, } #[doc(hidden)] pub struct RenameState { pub range: Range, pub old_name: Arc, pub editor: View, block_id: CustomBlockId, } struct InvalidationStack(Vec); struct RegisteredInlineCompletionProvider { provider: Arc, _subscription: Subscription, } enum ContextMenu { Completions(CompletionsMenu), CodeActions(CodeActionsMenu), } impl ContextMenu { fn select_first( &mut self, provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { ContextMenu::Completions(menu) => menu.select_first(provider, cx), ContextMenu::CodeActions(menu) => menu.select_first(cx), } true } else { false } } fn select_prev( &mut self, provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { ContextMenu::Completions(menu) => menu.select_prev(provider, cx), ContextMenu::CodeActions(menu) => menu.select_prev(cx), } true } else { false } } fn select_next( &mut self, provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { ContextMenu::Completions(menu) => menu.select_next(provider, cx), ContextMenu::CodeActions(menu) => menu.select_next(cx), } true } else { false } } fn select_last( &mut self, provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) -> bool { if self.visible() { match self { ContextMenu::Completions(menu) => menu.select_last(provider, cx), ContextMenu::CodeActions(menu) => menu.select_last(cx), } true } else { false } } fn visible(&self) -> bool { match self { ContextMenu::Completions(menu) => menu.visible(), ContextMenu::CodeActions(menu) => menu.visible(), } } fn render( &self, cursor_position: DisplayPoint, style: &EditorStyle, max_height: Pixels, workspace: Option>, cx: &mut ViewContext, ) -> (ContextMenuOrigin, AnyElement) { match self { ContextMenu::Completions(menu) => ( ContextMenuOrigin::EditorPoint(cursor_position), menu.render(style, max_height, workspace, cx), ), ContextMenu::CodeActions(menu) => menu.render(cursor_position, style, max_height, cx), } } } enum ContextMenuOrigin { EditorPoint(DisplayPoint), GutterIndicator(DisplayRow), } #[derive(Clone, Debug)] struct CompletionsMenu { id: CompletionId, sort_completions: bool, initial_position: Anchor, buffer: Model, completions: Arc>>, match_candidates: Arc<[StringMatchCandidate]>, matches: Arc<[StringMatch]>, selected_item: usize, scroll_handle: UniformListScrollHandle, selected_completion_documentation_resolve_debounce: Option>>, } impl CompletionsMenu { fn new( id: CompletionId, sort_completions: bool, initial_position: Anchor, buffer: Model, completions: Box<[Completion]>, ) -> Self { let match_candidates = completions .iter() .enumerate() .map(|(id, completion)| { StringMatchCandidate::new( id, completion.label.text[completion.label.filter_range.clone()].into(), ) }) .collect(); Self { id, sort_completions, initial_position, buffer, completions: Arc::new(RwLock::new(completions)), match_candidates, matches: Vec::new().into(), selected_item: 0, scroll_handle: UniformListScrollHandle::new(), selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new( DebouncedDelay::new(), ))), } } fn new_snippet_choices( id: CompletionId, sort_completions: bool, choices: &Vec, selection: Range, buffer: Model, ) -> Self { let completions = choices .iter() .map(|choice| Completion { old_range: selection.start.text_anchor..selection.end.text_anchor, new_text: choice.to_string(), label: CodeLabel { text: choice.to_string(), runs: Default::default(), filter_range: Default::default(), }, server_id: LanguageServerId(usize::MAX), documentation: None, lsp_completion: Default::default(), confirm: None, }) .collect(); let match_candidates = choices .iter() .enumerate() .map(|(id, completion)| StringMatchCandidate::new(id, completion.to_string())) .collect(); let matches = choices .iter() .enumerate() .map(|(id, completion)| StringMatch { candidate_id: id, score: 1., positions: vec![], string: completion.clone(), }) .collect(); Self { id, sort_completions, initial_position: selection.start, buffer, completions: Arc::new(RwLock::new(completions)), match_candidates, matches, selected_item: 0, scroll_handle: UniformListScrollHandle::new(), selected_completion_documentation_resolve_debounce: Some(Arc::new(Mutex::new( DebouncedDelay::new(), ))), } } fn suppress_documentation_resolution(mut self) -> Self { self.selected_completion_documentation_resolve_debounce .take(); self } fn select_first( &mut self, provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) { self.selected_item = 0; self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); self.attempt_resolve_selected_completion_documentation(provider, cx); cx.notify(); } fn select_prev( &mut self, provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) { if self.selected_item > 0 { self.selected_item -= 1; } else { self.selected_item = self.matches.len() - 1; } self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); self.attempt_resolve_selected_completion_documentation(provider, cx); cx.notify(); } fn select_next( &mut self, provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) { if self.selected_item + 1 < self.matches.len() { self.selected_item += 1; } else { self.selected_item = 0; } self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); self.attempt_resolve_selected_completion_documentation(provider, cx); cx.notify(); } fn select_last( &mut self, provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) { self.selected_item = self.matches.len() - 1; self.scroll_handle .scroll_to_item(self.selected_item, ScrollStrategy::Top); self.attempt_resolve_selected_completion_documentation(provider, cx); cx.notify(); } fn pre_resolve_completion_documentation( buffer: Model, completions: Arc>>, matches: Arc<[StringMatch]>, editor: &Editor, cx: &mut ViewContext, ) -> Task<()> { let settings = EditorSettings::get_global(cx); if !settings.show_completion_documentation { return Task::ready(()); } let Some(provider) = editor.completion_provider.as_ref() else { return Task::ready(()); }; let resolve_task = provider.resolve_completions( buffer, matches.iter().map(|m| m.candidate_id).collect(), completions.clone(), cx, ); cx.spawn(move |this, mut cx| async move { if let Some(true) = resolve_task.await.log_err() { this.update(&mut cx, |_, cx| cx.notify()).ok(); } }) } fn attempt_resolve_selected_completion_documentation( &mut self, provider: Option<&dyn CompletionProvider>, cx: &mut ViewContext, ) { let settings = EditorSettings::get_global(cx); if !settings.show_completion_documentation { return; } let completion_index = self.matches[self.selected_item].candidate_id; let Some(provider) = provider else { return; }; let Some(documentation_resolve) = self .selected_completion_documentation_resolve_debounce .as_ref() else { return; }; let resolve_task = provider.resolve_completions( self.buffer.clone(), vec![completion_index], self.completions.clone(), cx, ); let delay_ms = EditorSettings::get_global(cx).completion_documentation_secondary_query_debounce; let delay = Duration::from_millis(delay_ms); documentation_resolve.lock().fire_new(delay, cx, |_, cx| { cx.spawn(move |this, mut cx| async move { if let Some(true) = resolve_task.await.log_err() { this.update(&mut cx, |_, cx| cx.notify()).ok(); } }) }); } fn visible(&self) -> bool { !self.matches.is_empty() } fn render( &self, style: &EditorStyle, max_height: Pixels, workspace: Option>, cx: &mut ViewContext, ) -> AnyElement { let settings = EditorSettings::get_global(cx); let show_completion_documentation = settings.show_completion_documentation; let widest_completion_ix = self .matches .iter() .enumerate() .max_by_key(|(_, mat)| { let completions = self.completions.read(); let completion = &completions[mat.candidate_id]; let documentation = &completion.documentation; let mut len = completion.label.text.chars().count(); if let Some(Documentation::SingleLine(text)) = documentation { if show_completion_documentation { len += text.chars().count(); } } len }) .map(|(ix, _)| ix); let completions = self.completions.clone(); let matches = self.matches.clone(); let selected_item = self.selected_item; let style = style.clone(); let multiline_docs = if show_completion_documentation { let mat = &self.matches[selected_item]; let multiline_docs = match &self.completions.read()[mat.candidate_id].documentation { Some(Documentation::MultiLinePlainText(text)) => { Some(div().child(SharedString::from(text.clone()))) } Some(Documentation::MultiLineMarkdown(parsed)) if !parsed.text.is_empty() => { Some(div().child(render_parsed_markdown( "completions_markdown", parsed, &style, workspace, cx, ))) } _ => None, }; multiline_docs.map(|div| { div.id("multiline_docs") .max_h(max_height) .flex_1() .px_1p5() .py_1() .min_w(px(260.)) .max_w(px(640.)) .w(px(500.)) .overflow_y_scroll() .occlude() }) } else { None }; let list = uniform_list( cx.view().clone(), "completions", matches.len(), move |_editor, range, cx| { let start_ix = range.start; let completions_guard = completions.read(); matches[range] .iter() .enumerate() .map(|(ix, mat)| { let item_ix = start_ix + ix; let candidate_id = mat.candidate_id; let completion = &completions_guard[candidate_id]; let documentation = if show_completion_documentation { &completion.documentation } else { &None }; let highlights = gpui::combine_highlights( mat.ranges().map(|range| (range, FontWeight::BOLD.into())), styled_runs_for_code_label(&completion.label, &style.syntax).map( |(range, mut highlight)| { // Ignore font weight for syntax highlighting, as we'll use it // for fuzzy matches. highlight.font_weight = None; if completion.lsp_completion.deprecated.unwrap_or(false) { highlight.strikethrough = Some(StrikethroughStyle { thickness: 1.0.into(), ..Default::default() }); highlight.color = Some(cx.theme().colors().text_muted); } (range, highlight) }, ), ); let completion_label = StyledText::new(completion.label.text.clone()) .with_highlights(&style.text, highlights); let documentation_label = if let Some(Documentation::SingleLine(text)) = documentation { if text.trim().is_empty() { None } else { Some( Label::new(text.clone()) .ml_4() .size(LabelSize::Small) .color(Color::Muted), ) } } else { None }; let color_swatch = completion .color() .map(|color| div().size_4().bg(color).rounded_sm()); div().min_w(px(220.)).max_w(px(540.)).child( ListItem::new(mat.candidate_id) .inset(true) .selected(item_ix == selected_item) .on_click(cx.listener(move |editor, _event, cx| { cx.stop_propagation(); if let Some(task) = editor.confirm_completion( &ConfirmCompletion { item_ix: Some(item_ix), }, cx, ) { task.detach_and_log_err(cx) } })) .start_slot::
(color_swatch) .child(h_flex().overflow_hidden().child(completion_label)) .end_slot::