mirror of https://github.com/helix-editor/helix
Merge 6576e92333
into fed3edcab7
commit
8529aaf5d3
|
@ -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
|
||||
pub fn textobject_word(
|
||||
slice: RopeSlice,
|
||||
|
@ -77,11 +87,7 @@ pub fn textobject_word(
|
|||
) -> Range {
|
||||
let pos = range.cursor(slice);
|
||||
|
||||
let word_start = find_word_boundary(slice, pos, Direction::Backward, 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),
|
||||
};
|
||||
let (word_start, word_end) = find_word_boundaries(slice, pos, long);
|
||||
|
||||
// Special case.
|
||||
if word_start == word_end {
|
||||
|
|
|
@ -19,6 +19,7 @@ use helix_core::{
|
|||
movement::Direction,
|
||||
syntax::{self, OverlayHighlights},
|
||||
text_annotations::TextAnnotations,
|
||||
textobject::find_word_boundaries,
|
||||
unicode::width::UnicodeWidthStr,
|
||||
visual_offset_from_block, Change, Position, Range, Selection, Transaction,
|
||||
};
|
||||
|
@ -27,7 +28,7 @@ use helix_view::{
|
|||
document::{Mode, SCRATCH_BUFFER_NAME},
|
||||
editor::{CompleteAction, CursorShapeConfig},
|
||||
graphics::{Color, CursorKind, Modifier, Rect, Style},
|
||||
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
|
||||
input::{KeyEvent, MouseButton, MouseClick, MouseEvent, MouseEventKind},
|
||||
keyboard::{KeyCode, KeyModifiers},
|
||||
Document, Editor, Theme, View,
|
||||
};
|
||||
|
@ -1128,10 +1129,13 @@ impl EditorView {
|
|||
if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) {
|
||||
let prev_view_id = view!(editor).id;
|
||||
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 {
|
||||
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 {
|
||||
// Discards non-primary selections for consistent UX with normal mode
|
||||
let primary = doc.selection(view_id).primary().put_cursor(
|
||||
|
@ -1140,10 +1144,27 @@ impl EditorView {
|
|||
true,
|
||||
);
|
||||
editor.mouse_down_range = Some(primary);
|
||||
doc.set_selection(view_id, Selection::single(primary.anchor, primary.head));
|
||||
|
||||
Selection::single(primary.anchor, primary.head)
|
||||
} 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 {
|
||||
self.clear_completion(editor);
|
||||
|
|
|
@ -8,7 +8,7 @@ use crate::{
|
|||
graphics::{CursorKind, Rect},
|
||||
handlers::Handlers,
|
||||
info::Info,
|
||||
input::KeyEvent,
|
||||
input::{KeyEvent, MouseClicks},
|
||||
register::Registers,
|
||||
theme::{self, Theme},
|
||||
tree::{self, Tree},
|
||||
|
@ -1133,6 +1133,8 @@ pub struct Editor {
|
|||
|
||||
pub mouse_down_range: Option<Range>,
|
||||
pub cursor_cache: CursorCache,
|
||||
|
||||
pub mouse_clicks: MouseClicks,
|
||||
}
|
||||
|
||||
pub type Motion = Box<dyn Fn(&mut Editor)>;
|
||||
|
@ -1255,6 +1257,7 @@ impl Editor {
|
|||
handlers,
|
||||
mouse_down_range: None,
|
||||
cursor_cache: CursorCache::default(),
|
||||
mouse_clicks: MouseClicks::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ use serde::de::{self, Deserialize, Deserializer};
|
|||
use std::fmt;
|
||||
|
||||
pub use crate::keyboard::{KeyCode, KeyModifiers, MediaKeyCode, ModifierKeyCode};
|
||||
use crate::ViewId;
|
||||
|
||||
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Hash)]
|
||||
pub enum Event {
|
||||
|
@ -59,6 +60,82 @@ pub enum MouseButton {
|
|||
/// Middle mouse button.
|
||||
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.
|
||||
// We use a newtype here because we want to customize Deserialize and Display.
|
||||
#[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("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);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue