Nik Revenco 2025-06-16 15:19:31 +02:00 committed by GitHub
commit 2bbd823dde
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 177 additions and 22 deletions

View File

@ -67,6 +67,16 @@ impl Display for TextObject {
} }
} }
pub fn find_word_boundaries(slice: RopeSlice, pos: usize, is_long: bool) -> (usize, usize) {
let word_start = find_word_boundary(slice, pos, Direction::Backward, is_long);
let word_end = match slice.get_char(pos).map(categorize_char) {
None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
_ => find_word_boundary(slice, pos + 1, Direction::Forward, is_long),
};
(word_start, word_end)
}
// count doesn't do anything yet // count doesn't do anything yet
pub fn textobject_word( pub fn textobject_word(
slice: RopeSlice, slice: RopeSlice,
@ -77,11 +87,7 @@ pub fn textobject_word(
) -> Range { ) -> Range {
let pos = range.cursor(slice); let pos = range.cursor(slice);
let word_start = find_word_boundary(slice, pos, Direction::Backward, long); let (word_start, word_end) = find_word_boundaries(slice, pos, long);
let word_end = match slice.get_char(pos).map(categorize_char) {
None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
_ => find_word_boundary(slice, pos + 1, Direction::Forward, long),
};
// Special case. // Special case.
if word_start == word_end { if word_start == word_end {

View File

@ -19,6 +19,7 @@ use helix_core::{
movement::Direction, movement::Direction,
syntax::{self, OverlayHighlights}, syntax::{self, OverlayHighlights},
text_annotations::TextAnnotations, text_annotations::TextAnnotations,
textobject::find_word_boundaries,
unicode::width::UnicodeWidthStr, unicode::width::UnicodeWidthStr,
visual_offset_from_block, Change, Position, Range, Selection, Transaction, visual_offset_from_block, Change, Position, Range, Selection, Transaction,
}; };
@ -27,7 +28,7 @@ use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME}, document::{Mode, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig}, editor::{CompleteAction, CursorShapeConfig},
graphics::{Color, CursorKind, Modifier, Rect, Style}, graphics::{Color, CursorKind, Modifier, Rect, Style},
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}, input::{KeyEvent, MouseButton, MouseClick, MouseEvent, MouseEventKind},
keyboard::{KeyCode, KeyModifiers}, keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View, Document, Editor, Theme, View,
}; };
@ -1128,10 +1129,13 @@ impl EditorView {
if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) { if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) {
let prev_view_id = view!(editor).id; let prev_view_id = view!(editor).id;
let doc = doc_mut!(editor, &view!(editor, view_id).doc); let doc = doc_mut!(editor, &view!(editor, view_id).doc);
let text = doc.text().slice(..);
let selection = match editor.mouse_clicks.register_click(pos, view_id) {
MouseClick::Single => {
if modifiers == KeyModifiers::ALT { if modifiers == KeyModifiers::ALT {
let selection = doc.selection(view_id).clone(); let selection = doc.selection(view_id).clone();
doc.set_selection(view_id, selection.push(Range::point(pos))); selection.push(Range::point(pos))
} else if editor.mode == Mode::Select { } else if editor.mode == Mode::Select {
// Discards non-primary selections for consistent UX with normal mode // Discards non-primary selections for consistent UX with normal mode
let primary = doc.selection(view_id).primary().put_cursor( let primary = doc.selection(view_id).primary().put_cursor(
@ -1140,10 +1144,27 @@ impl EditorView {
true, true,
); );
editor.mouse_down_range = Some(primary); editor.mouse_down_range = Some(primary);
doc.set_selection(view_id, Selection::single(primary.anchor, primary.head));
Selection::single(primary.anchor, primary.head)
} else { } else {
doc.set_selection(view_id, Selection::point(pos)); Selection::point(pos)
} }
}
MouseClick::Double => {
let (word_start, word_end) = find_word_boundaries(text, pos, false);
Selection::single(word_start, word_end)
}
MouseClick::Triple => {
let current_line = text.char_to_line(pos);
let line_start = text.line_to_char(current_line);
let line_end = text.line_to_char(current_line + 1);
Selection::single(line_start, line_end)
}
};
doc.set_selection(view_id, selection);
if view_id != prev_view_id { if view_id != prev_view_id {
self.clear_completion(editor); self.clear_completion(editor);

View File

@ -8,7 +8,7 @@ use crate::{
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
handlers::Handlers, handlers::Handlers,
info::Info, info::Info,
input::KeyEvent, input::{KeyEvent, MouseClicks},
register::Registers, register::Registers,
theme::{self, Theme}, theme::{self, Theme},
tree::{self, Tree}, tree::{self, Tree},
@ -1133,6 +1133,8 @@ pub struct Editor {
pub mouse_down_range: Option<Range>, pub mouse_down_range: Option<Range>,
pub cursor_cache: CursorCache, pub cursor_cache: CursorCache,
pub mouse_clicks: MouseClicks,
} }
pub type Motion = Box<dyn Fn(&mut Editor)>; pub type Motion = Box<dyn Fn(&mut Editor)>;
@ -1255,6 +1257,7 @@ impl Editor {
handlers, handlers,
mouse_down_range: None, mouse_down_range: None,
cursor_cache: CursorCache::default(), cursor_cache: CursorCache::default(),
mouse_clicks: MouseClicks::new(),
} }
} }

View File

@ -5,6 +5,7 @@ use serde::de::{self, Deserialize, Deserializer};
use std::fmt; use std::fmt;
pub use crate::keyboard::{KeyCode, KeyModifiers, MediaKeyCode, ModifierKeyCode}; pub use crate::keyboard::{KeyCode, KeyModifiers, MediaKeyCode, ModifierKeyCode};
use crate::ViewId;
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)] #[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)]
pub enum Event { pub enum Event {
@ -59,6 +60,82 @@ pub enum MouseButton {
/// Middle mouse button. /// Middle mouse button.
Middle, Middle,
} }
/// Tracks the character positions and views where we last saw a mouse click
#[derive(Debug)]
pub struct MouseClicks {
/// The last 2 clicks on specific characters in the editor:
/// (character index clicked, view id)
// We store the view id to ensure that if we click on
// the 3rd character in view #1 and 3rd character in view #2,
// it won't register as a double click.
clicks: [Option<(usize, ViewId)>; 2],
}
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)]
pub enum MouseClick {
/// A click where the pressed character is different to the character previously pressed
Single,
/// A click where the same character was pressed 2 times in a row
Double,
/// A click where the same character pressed 3 times in a row
Triple,
}
/// A fixed-size queue of length 2, storing the most recently clicked characters
/// as well as the views for which they were clicked.
impl MouseClicks {
pub fn new() -> Self {
Self {
clicks: [None, None],
}
}
/// Add a click to the beginning of the queue, discarding the last click
fn insert(&mut self, click: usize, view_id: ViewId) {
self.clicks[1] = self.clicks[0];
self.clicks[0] = Some((click, view_id));
}
/// Registers a click for a certain character index, and returns the type of this click
pub fn register_click(&mut self, click: usize, view_id: ViewId) -> MouseClick {
let click_type = if self.is_triple_click(click, view_id) {
// Clicking 4th time on the same character should be the same as clicking for the 1st time
// So we reset the state
self.clicks = [None, None];
return MouseClick::Triple;
} else if self.is_double_click(click, view_id) {
MouseClick::Double
} else {
MouseClick::Single
};
self.insert(click, view_id);
click_type
}
/// If we click this character, would that be a triple click?
fn is_triple_click(&mut self, click: usize, view_id: ViewId) -> bool {
Some((click, view_id)) == self.clicks[0] && Some((click, view_id)) == self.clicks[1]
}
/// If we click this character, would that be a double click?
fn is_double_click(&mut self, click: usize, view_id: ViewId) -> bool {
Some((click, view_id)) == self.clicks[0]
&& self.clicks[1].map_or(true, |(prev_click, prev_view_id)| {
!(click == prev_click && prev_view_id == view_id)
})
}
}
impl Default for MouseClicks {
fn default() -> Self {
Self::new()
}
}
/// Represents a key event. /// Represents a key event.
// We use a newtype here because we want to customize Deserialize and Display. // We use a newtype here because we want to customize Deserialize and Display.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
@ -961,4 +1038,52 @@ mod test {
assert!(parse_macro("abc>123").is_err()); assert!(parse_macro("abc>123").is_err());
assert!(parse_macro("wd<foo>").is_err()); assert!(parse_macro("wd<foo>").is_err());
} }
#[test]
fn clicking_4th_time_resets_mouse_clicks() {
let mut mouse_clicks = MouseClicks::new();
let view = ViewId::default();
assert_eq!(mouse_clicks.register_click(4, view), MouseClick::Single);
assert_eq!(mouse_clicks.register_click(4, view), MouseClick::Double);
assert_eq!(mouse_clicks.register_click(4, view), MouseClick::Triple);
assert_eq!(mouse_clicks.register_click(4, view), MouseClick::Single);
}
#[test]
fn clicking_different_characters_resets_mouse_clicks() {
let mut mouse_clicks = MouseClicks::new();
let view = ViewId::default();
assert_eq!(mouse_clicks.register_click(4, view), MouseClick::Single);
assert_eq!(mouse_clicks.register_click(4, view), MouseClick::Double);
assert_eq!(mouse_clicks.register_click(8, view), MouseClick::Single);
assert_eq!(mouse_clicks.register_click(1, view), MouseClick::Single);
assert_eq!(mouse_clicks.register_click(1, view), MouseClick::Double);
assert_eq!(mouse_clicks.register_click(1, view), MouseClick::Triple);
}
#[test]
fn switching_views_resets_mouse_clicks() {
let mut mouse_clicks = MouseClicks::new();
let mut view_ids = slotmap::HopSlotMap::with_key();
let view1 = view_ids.insert(());
let view2 = view_ids.insert(());
assert_eq!(mouse_clicks.register_click(4, view1), MouseClick::Single);
assert_eq!(mouse_clicks.register_click(4, view2), MouseClick::Single);
assert_eq!(mouse_clicks.register_click(4, view1), MouseClick::Single);
assert_eq!(mouse_clicks.register_click(4, view2), MouseClick::Single);
assert_eq!(mouse_clicks.register_click(4, view2), MouseClick::Double);
assert_eq!(mouse_clicks.register_click(4, view1), MouseClick::Single);
assert_eq!(mouse_clicks.register_click(4, view1), MouseClick::Double);
assert_eq!(mouse_clicks.register_click(4, view1), MouseClick::Triple);
}
} }