pub mod parser;mod path_range; use base64::Engine as _;use futures::FutureExt as _;use gpui::HitboxBehavior;use language::LanguageName;use log::Level;pub use path_range::{LineCol, PathWithRange};use ui::Checkbox;use ui::CopyButton; use std::borrow::Cow;use std::iter;use std::mem;use std::ops::Range;use std::path::Path;use std::rc::Rc;use std::sync::Arc;use std::time::Duration; use collections::{HashMap, HashSet};use gpui::{ AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image, ImageFormat, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad,};use language::{Language, LanguageRegistry, Rope};use parser::CodeBlockMetadata;use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown};use pulldown_cmark::Alignment;use sum_tree::TreeMap;use theme::SyntaxTheme;use ui::{ScrollAxes, Scrollbars, WithScrollbar, prelude::*};use util::ResultExt; use crate::parser::CodeBlockKind; /// A callback function that can be used to customize the style of links based on the destination URL./// If the callback returns `None`, the default link style will be used.type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>; /// Defines custom style refinements for each heading level (H1-H6)#[derive(Clone, Default)]pub struct HeadingLevelStyles { pub h1: Option<TextStyleRefinement>, pub h2: Option<TextStyleRefinement>, pub h3: Option<TextStyleRefinement>, pub h4: Option<TextStyleRefinement>, pub h5: Option<TextStyleRefinement>, pub h6: Option<TextStyleRefinement>,} #[derive(Clone)]pub struct MarkdownStyle { pub base_text_style: TextStyle, pub container_style: StyleRefinement, pub code_block: StyleRefinement, pub code_block_overflow_x_scroll: bool, pub inline_code: TextStyleRefinement, pub block_quote: TextStyleRefinement, pub link: TextStyleRefinement, pub link_callback: Option<LinkStyleCallback>, pub rule_color: Hsla, pub block_quote_border_color: Hsla, pub syntax: Arc<SyntaxTheme>, pub selection_background_color: Hsla, pub heading: StyleRefinement, pub heading_level_styles: Option<HeadingLevelStyles>, pub height_is_multiple_of_line_height: bool, pub prevent_mouse_interaction: bool, pub table_columns_min_size: bool,} impl Default for MarkdownStyle { fn default() -> Self { Self { base_text_style: Default::default(), container_style: Default::default(), code_block: Default::default(), code_block_overflow_x_scroll: false, inline_code: Default::default(), block_quote: Default::default(), link: Default::default(), link_callback: None, rule_color: Default::default(), block_quote_border_color: Default::default(), syntax: Arc::new(SyntaxTheme::default()), selection_background_color: Default::default(), heading: Default::default(), heading_level_styles: None, height_is_multiple_of_line_height: false, prevent_mouse_interaction: false, table_columns_min_size: false, } }} pub struct Markdown { source: SharedString, selection: Selection, pressed_link: Option<RenderedLink>, autoscroll_request: Option<usize>, parsed_markdown: ParsedMarkdown, images_by_source_offset: HashMap<usize, Arc<Image>>, should_reparse: bool, pending_parse: Option<Task<()>>, focus_handle: FocusHandle, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<LanguageName>, options: Options, copied_code_blocks: HashSet<ElementId>, code_block_scroll_handles: HashMap<usize, ScrollHandle>, context_menu_selected_text: Option<String>,} struct Options { parse_links_only: bool,} pub enum CodeBlockRenderer { Default { copy_button: bool, copy_button_on_hover: bool, border: bool, }, Custom { render: CodeBlockRenderFn, /// A function that can modify the parent container after the code block /// content has been appended as a child element. transform: Option<CodeBlockTransformFn>, },} pub type CodeBlockRenderFn = Arc< dyn Fn( &CodeBlockKind, &ParsedMarkdown, Range<usize>, CodeBlockMetadata, &mut Window, &App, ) -> Div,>; pub type CodeBlockTransformFn = Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>; actions!( markdown, [ /// Copies the selected text to the clipboard. Copy, /// Copies the selected text as markdown to the clipboard. CopyAsMarkdown ]); impl Markdown { pub fn new( source: SharedString, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<LanguageName>, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, should_reparse: false, images_by_source_offset: Default::default(), parsed_markdown: ParsedMarkdown::default(), pending_parse: None, focus_handle, language_registry, fallback_code_block_language, options: Options { parse_links_only: false, }, copied_code_blocks: HashSet::default(), code_block_scroll_handles: HashMap::default(), context_menu_selected_text: None, }; this.parse(cx); this } pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, should_reparse: false, parsed_markdown: ParsedMarkdown::default(), images_by_source_offset: Default::default(), pending_parse: None, focus_handle, language_registry: None, fallback_code_block_language: None, options: Options { parse_links_only: true, }, copied_code_blocks: HashSet::default(), code_block_scroll_handles: HashMap::default(), context_menu_selected_text: None, }; this.parse(cx); this } fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle { self.code_block_scroll_handles .entry(id) .or_insert_with(ScrollHandle::new) .clone() } fn retain_code_block_scroll_handles(&mut self, ids: &HashSet<usize>) { self.code_block_scroll_handles .retain(|id, _| ids.contains(id)); } fn clear_code_block_scroll_handles(&mut self) { self.code_block_scroll_handles.clear(); } pub fn is_parsing(&self) -> bool { self.pending_parse.is_some() } pub fn source(&self) -> &str { &self.source } pub fn append(&mut self, text: &str, cx: &mut Context<Self>) { self.source = SharedString::new(self.source.to_string() + text); self.parse(cx); } pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) { self.source = source.into(); self.parse(cx); } pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) { if source == self.source() { return; } self.source = source; self.selection = Selection::default(); self.autoscroll_request = None; self.pending_parse = None; self.should_reparse = false; // Don't clear parsed_markdown here - keep existing content visible until new parse completes self.parse(cx); } #[cfg(any(test, feature = "test-support"))] pub fn parsed_markdown(&self) -> &ParsedMarkdown { &self.parsed_markdown } pub fn escape(s: &str) -> Cow<'_, str> { // Valid to use bytes since multi-byte UTF-8 doesn't use ASCII chars. let count = s .bytes() .filter(|c| *c == b'\n' || c.is_ascii_punctuation()) .count(); if count > 0 { let mut output = String::with_capacity(s.len() + count); let mut is_newline = false; for c in s.chars() { if is_newline && c == ' ' { continue; } is_newline = c == '\n'; if c == '\n' { output.push('\n') } else if c.is_ascii_punctuation() { output.push('\\') } output.push(c) } output.into() } else { s.into() } } pub fn selected_text(&self) -> Option<String> { if self.selection.end <= self.selection.start { None } else { Some(self.source[self.selection.start..self.selection.end].to_string()) } } fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) { if self.selection.end <= self.selection.start { return; } let text = text.text_for_range(self.selection.start..self.selection.end); cx.write_to_clipboard(ClipboardItem::new_string(text)); } fn copy_as_markdown(&mut self, _: &mut Window, cx: &mut Context<Self>) { if let Some(text) = self.context_menu_selected_text.take() { cx.write_to_clipboard(ClipboardItem::new_string(text)); return; } if self.selection.end <= self.selection.start { return; } let text = self.source[self.selection.start..self.selection.end].to_string(); cx.write_to_clipboard(ClipboardItem::new_string(text)); } fn capture_selection_for_context_menu(&mut self) { self.context_menu_selected_text = self.selected_text(); } fn parse(&mut self, cx: &mut Context<Self>) { if self.source.is_empty() { return; } if self.pending_parse.is_some() { self.should_reparse = true; return; } self.should_reparse = false; self.pending_parse = Some(self.start_background_parse(cx)); } fn start_background_parse(&self, cx: &Context<Self>) -> Task<()> { let source = self.source.clone(); let should_parse_links_only = self.options.parse_links_only; let language_registry = self.language_registry.clone(); let fallback = self.fallback_code_block_language.clone(); let parsed = cx.background_spawn(async move { if should_parse_links_only { return ( ParsedMarkdown { events: Arc::from(parse_links_only(source.as_ref())), source, languages_by_name: TreeMap::default(), languages_by_path: TreeMap::default(), }, Default::default(), ); } let (events, language_names, paths) = parse_markdown(&source); let mut images_by_source_offset = HashMap::default(); let mut languages_by_name = TreeMap::default(); let mut languages_by_path = TreeMap::default(); if let Some(registry) = language_registry.as_ref() { for name in language_names { let language = if !name.is_empty() { registry.language_for_name_or_extension(&name).left_future() } else if let Some(fallback) = &fallback { registry.language_for_name(fallback.as_ref()).right_future() } else { continue; }; if let Ok(language) = language.await { languages_by_name.insert(name, language); } } for path in paths { if let Ok(language) = registry .load_language_for_file_path(Path::new(path.as_ref())) .await { languages_by_path.insert(path, language); } } } for (range, event) in &events { if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event && let Some(data_url) = dest_url.strip_prefix("data:") { let Some((mime_info, data)) = data_url.split_once(',') else { continue; }; let Some((mime_type, encoding)) = mime_info.split_once(';') else { continue; }; let Some(format) = ImageFormat::from_mime_type(mime_type) else { continue; }; let is_base64 = encoding == "base64"; if is_base64 && let Some(bytes) = base64::prelude::BASE64_STANDARD .decode(data) .log_with_level(Level::Debug) { let image = Arc::new(Image::from_bytes(format, bytes)); images_by_source_offset.insert(range.start, image); } } } ( ParsedMarkdown { source, events: Arc::from(events), languages_by_name, languages_by_path, }, images_by_source_offset, ) }); cx.spawn(async move |this, cx| { let (parsed, images_by_source_offset) = parsed.await; this.update(cx, |this, cx| { this.parsed_markdown = parsed; this.images_by_source_offset = images_by_source_offset; this.pending_parse.take(); if this.should_reparse { this.parse(cx); } cx.refresh_windows(); }) .ok(); }) }} impl Focusable for Markdown { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() }} #[derive(Debug, Default, Clone)]enum SelectMode { #[default] Character, Word(Range<usize>), Line(Range<usize>), All,} #[derive(Clone, Default)]struct Selection { start: usize, end: usize, reversed: bool, pending: bool, mode: SelectMode,} impl Selection { fn set_head(&mut self, head: usize, rendered_text: &RenderedText) { match &self.mode { SelectMode::Character => { if head < self.tail() { if !self.reversed { self.end = self.start; self.reversed = true; } self.start = head; } else { if self.reversed { self.start = self.end; self.reversed = false; } self.end = head; } } SelectMode::Word(original_range) | SelectMode::Line(original_range) => { let head_range = if matches!(self.mode, SelectMode::Word(_)) { rendered_text.surrounding_word_range(head) } else { rendered_text.surrounding_line_range(head) }; if head < original_range.start { self.start = head_range.start; self.end = original_range.end; self.reversed = true; } else if head >= original_range.end { self.start = original_range.start; self.end = head_range.end; self.reversed = false; } else { self.start = original_range.start; self.end = original_range.end;Lines 1–500 of 2290. Load more
pub mod parser; mod path_range; use base64::Engine as _; use futures::FutureExt as _; use gpui::HitboxBehavior; use language::LanguageName; use log::Level; pub use path_range::{LineCol, PathWithRange}; use ui::Checkbox; use ui::CopyButton; use std::borrow::Cow; use std::iter; use std::mem; use std::ops::Range; use std::path::Path; use std::rc::Rc; use std::sync::Arc; use std::time::Duration; use collections::{HashMap, HashSet}; use gpui::{ AnyElement, App, BorderStyle, Bounds, ClipboardItem, CursorStyle, DispatchPhase, Edges, Entity, FocusHandle, Focusable, FontStyle, FontWeight, GlobalElementId, Hitbox, Hsla, Image, ImageFormat, KeyContext, Length, MouseButton, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point, ScrollHandle, Stateful, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun, TextStyle, TextStyleRefinement, actions, img, point, quad, }; use language::{Language, LanguageRegistry, Rope}; use parser::CodeBlockMetadata; use parser::{MarkdownEvent, MarkdownTag, MarkdownTagEnd, parse_links_only, parse_markdown}; use pulldown_cmark::Alignment; use sum_tree::TreeMap; use theme::SyntaxTheme; use ui::{ScrollAxes, Scrollbars, WithScrollbar, prelude::*}; use util::ResultExt; use crate::parser::CodeBlockKind; /// A callback function that can be used to customize the style of links based on the destination URL. /// If the callback returns `None`, the default link style will be used. type LinkStyleCallback = Rc<dyn Fn(&str, &App) -> Option<TextStyleRefinement>>; /// Defines custom style refinements for each heading level (H1-H6) #[derive(Clone, Default)] pub struct HeadingLevelStyles { pub h1: Option<TextStyleRefinement>, pub h2: Option<TextStyleRefinement>, pub h3: Option<TextStyleRefinement>, pub h4: Option<TextStyleRefinement>, pub h5: Option<TextStyleRefinement>, pub h6: Option<TextStyleRefinement>, } #[derive(Clone)] pub struct MarkdownStyle { pub base_text_style: TextStyle, pub container_style: StyleRefinement, pub code_block: StyleRefinement, pub code_block_overflow_x_scroll: bool, pub inline_code: TextStyleRefinement, pub block_quote: TextStyleRefinement, pub link: TextStyleRefinement, pub link_callback: Option<LinkStyleCallback>, pub rule_color: Hsla, pub block_quote_border_color: Hsla, pub syntax: Arc<SyntaxTheme>, pub selection_background_color: Hsla, pub heading: StyleRefinement, pub heading_level_styles: Option<HeadingLevelStyles>, pub height_is_multiple_of_line_height: bool, pub prevent_mouse_interaction: bool, pub table_columns_min_size: bool, } impl Default for MarkdownStyle { fn default() -> Self { Self { base_text_style: Default::default(), container_style: Default::default(), code_block: Default::default(), code_block_overflow_x_scroll: false, inline_code: Default::default(), block_quote: Default::default(), link: Default::default(), link_callback: None, rule_color: Default::default(), block_quote_border_color: Default::default(), syntax: Arc::new(SyntaxTheme::default()), selection_background_color: Default::default(), heading: Default::default(), heading_level_styles: None, height_is_multiple_of_line_height: false, prevent_mouse_interaction: false, table_columns_min_size: false, } } } pub struct Markdown { source: SharedString, selection: Selection, pressed_link: Option<RenderedLink>, autoscroll_request: Option<usize>, parsed_markdown: ParsedMarkdown, images_by_source_offset: HashMap<usize, Arc<Image>>, should_reparse: bool, pending_parse: Option<Task<()>>, focus_handle: FocusHandle, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<LanguageName>, options: Options, copied_code_blocks: HashSet<ElementId>, code_block_scroll_handles: HashMap<usize, ScrollHandle>, context_menu_selected_text: Option<String>, } struct Options { parse_links_only: bool, } pub enum CodeBlockRenderer { Default { copy_button: bool, copy_button_on_hover: bool, border: bool, }, Custom { render: CodeBlockRenderFn, /// A function that can modify the parent container after the code block /// content has been appended as a child element. transform: Option<CodeBlockTransformFn>, }, } pub type CodeBlockRenderFn = Arc< dyn Fn( &CodeBlockKind, &ParsedMarkdown, Range<usize>, CodeBlockMetadata, &mut Window, &App, ) -> Div, >; pub type CodeBlockTransformFn = Arc<dyn Fn(AnyDiv, Range<usize>, CodeBlockMetadata, &mut Window, &App) -> AnyDiv>; actions!( markdown, [ /// Copies the selected text to the clipboard. Copy, /// Copies the selected text as markdown to the clipboard. CopyAsMarkdown ] ); impl Markdown { pub fn new( source: SharedString, language_registry: Option<Arc<LanguageRegistry>>, fallback_code_block_language: Option<LanguageName>, cx: &mut Context<Self>, ) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, should_reparse: false, images_by_source_offset: Default::default(), parsed_markdown: ParsedMarkdown::default(), pending_parse: None, focus_handle, language_registry, fallback_code_block_language, options: Options { parse_links_only: false, }, copied_code_blocks: HashSet::default(), code_block_scroll_handles: HashMap::default(), context_menu_selected_text: None, }; this.parse(cx); this } pub fn new_text(source: SharedString, cx: &mut Context<Self>) -> Self { let focus_handle = cx.focus_handle(); let mut this = Self { source, selection: Selection::default(), pressed_link: None, autoscroll_request: None, should_reparse: false, parsed_markdown: ParsedMarkdown::default(), images_by_source_offset: Default::default(), pending_parse: None, focus_handle, language_registry: None, fallback_code_block_language: None, options: Options { parse_links_only: true, }, copied_code_blocks: HashSet::default(), code_block_scroll_handles: HashMap::default(), context_menu_selected_text: None, }; this.parse(cx); this } fn code_block_scroll_handle(&mut self, id: usize) -> ScrollHandle { self.code_block_scroll_handles .entry(id) .or_insert_with(ScrollHandle::new) .clone() } fn retain_code_block_scroll_handles(&mut self, ids: &HashSet<usize>) { self.code_block_scroll_handles .retain(|id, _| ids.contains(id)); } fn clear_code_block_scroll_handles(&mut self) { self.code_block_scroll_handles.clear(); } pub fn is_parsing(&self) -> bool { self.pending_parse.is_some() } pub fn source(&self) -> &str { &self.source } pub fn append(&mut self, text: &str, cx: &mut Context<Self>) { self.source = SharedString::new(self.source.to_string() + text); self.parse(cx); } pub fn replace(&mut self, source: impl Into<SharedString>, cx: &mut Context<Self>) { self.source = source.into(); self.parse(cx); } pub fn reset(&mut self, source: SharedString, cx: &mut Context<Self>) { if source == self.source() { return; } self.source = source; self.selection = Selection::default(); self.autoscroll_request = None; self.pending_parse = None; self.should_reparse = false; // Don't clear parsed_markdown here - keep existing content visible until new parse completes self.parse(cx); } #[cfg(any(test, feature = "test-support"))] pub fn parsed_markdown(&self) -> &ParsedMarkdown { &self.parsed_markdown } pub fn escape(s: &str) -> Cow<'_, str> { // Valid to use bytes since multi-byte UTF-8 doesn't use ASCII chars. let count = s .bytes() .filter(|c| *c == b'\n' || c.is_ascii_punctuation()) .count(); if count > 0 { let mut output = String::with_capacity(s.len() + count); let mut is_newline = false; for c in s.chars() { if is_newline && c == ' ' { continue; } is_newline = c == '\n'; if c == '\n' { output.push('\n') } else if c.is_ascii_punctuation() { output.push('\\') } output.push(c) } output.into() } else { s.into() } } pub fn selected_text(&self) -> Option<String> { if self.selection.end <= self.selection.start { None } else { Some(self.source[self.selection.start..self.selection.end].to_string()) } } fn copy(&self, text: &RenderedText, _: &mut Window, cx: &mut Context<Self>) { if self.selection.end <= self.selection.start { return; } let text = text.text_for_range(self.selection.start..self.selection.end); cx.write_to_clipboard(ClipboardItem::new_string(text)); } fn copy_as_markdown(&mut self, _: &mut Window, cx: &mut Context<Self>) { if let Some(text) = self.context_menu_selected_text.take() { cx.write_to_clipboard(ClipboardItem::new_string(text)); return; } if self.selection.end <= self.selection.start { return; } let text = self.source[self.selection.start..self.selection.end].to_string(); cx.write_to_clipboard(ClipboardItem::new_string(text)); } fn capture_selection_for_context_menu(&mut self) { self.context_menu_selected_text = self.selected_text(); } fn parse(&mut self, cx: &mut Context<Self>) { if self.source.is_empty() { return; } if self.pending_parse.is_some() { self.should_reparse = true; return; } self.should_reparse = false; self.pending_parse = Some(self.start_background_parse(cx)); } fn start_background_parse(&self, cx: &Context<Self>) -> Task<()> { let source = self.source.clone(); let should_parse_links_only = self.options.parse_links_only; let language_registry = self.language_registry.clone(); let fallback = self.fallback_code_block_language.clone(); let parsed = cx.background_spawn(async move { if should_parse_links_only { return ( ParsedMarkdown { events: Arc::from(parse_links_only(source.as_ref())), source, languages_by_name: TreeMap::default(), languages_by_path: TreeMap::default(), }, Default::default(), ); } let (events, language_names, paths) = parse_markdown(&source); let mut images_by_source_offset = HashMap::default(); let mut languages_by_name = TreeMap::default(); let mut languages_by_path = TreeMap::default(); if let Some(registry) = language_registry.as_ref() { for name in language_names { let language = if !name.is_empty() { registry.language_for_name_or_extension(&name).left_future() } else if let Some(fallback) = &fallback { registry.language_for_name(fallback.as_ref()).right_future() } else { continue; }; if let Ok(language) = language.await { languages_by_name.insert(name, language); } } for path in paths { if let Ok(language) = registry .load_language_for_file_path(Path::new(path.as_ref())) .await { languages_by_path.insert(path, language); } } } for (range, event) in &events { if let MarkdownEvent::Start(MarkdownTag::Image { dest_url, .. }) = event && let Some(data_url) = dest_url.strip_prefix("data:") { let Some((mime_info, data)) = data_url.split_once(',') else { continue; }; let Some((mime_type, encoding)) = mime_info.split_once(';') else { continue; }; let Some(format) = ImageFormat::from_mime_type(mime_type) else { continue; }; let is_base64 = encoding == "base64"; if is_base64 && let Some(bytes) = base64::prelude::BASE64_STANDARD .decode(data) .log_with_level(Level::Debug) { let image = Arc::new(Image::from_bytes(format, bytes)); images_by_source_offset.insert(range.start, image); } } } ( ParsedMarkdown { source, events: Arc::from(events), languages_by_name, languages_by_path, }, images_by_source_offset, ) }); cx.spawn(async move |this, cx| { let (parsed, images_by_source_offset) = parsed.await; this.update(cx, |this, cx| { this.parsed_markdown = parsed; this.images_by_source_offset = images_by_source_offset; this.pending_parse.take(); if this.should_reparse { this.parse(cx); } cx.refresh_windows(); }) .ok(); }) } } impl Focusable for Markdown { fn focus_handle(&self, _cx: &App) -> FocusHandle { self.focus_handle.clone() } } #[derive(Debug, Default, Clone)] enum SelectMode { #[default] Character, Word(Range<usize>), Line(Range<usize>), All, } #[derive(Clone, Default)] struct Selection { start: usize, end: usize, reversed: bool, pending: bool, mode: SelectMode, } impl Selection { fn set_head(&mut self, head: usize, rendered_text: &RenderedText) { match &self.mode { SelectMode::Character => { if head < self.tail() { if !self.reversed { self.end = self.start; self.reversed = true; } self.start = head; } else { if self.reversed { self.start = self.end; self.reversed = false; } self.end = head; } } SelectMode::Word(original_range) | SelectMode::Line(original_range) => { let head_range = if matches!(self.mode, SelectMode::Word(_)) { rendered_text.surrounding_word_range(head) } else { rendered_text.surrounding_line_range(head) }; if head < original_range.start { self.start = head_range.start; self.end = original_range.end; self.reversed = true; } else if head >= original_range.end { self.start = original_range.start; self.end = head_range.end; self.reversed = false; } else { self.start = original_range.start; self.end = original_range.end;