Compare commits

...

27 Commits

Author SHA1 Message Date
Nik Revenco 2bbd823dde
Merge 6576e92333 into 837627dd8a 2025-06-16 15:19:31 +02:00
Tatesa Uradnik 837627dd8a
feat: allow moving nonexistent file (#13748) 2025-06-16 08:19:28 -05:00
CalebLarsen 1246549afd
Fix: update c++ highlights (#13772) 2025-06-16 08:04:22 -05:00
uncenter ada8004ea5
Highlight HTML entities (#13753) 2025-06-16 08:03:02 -05:00
Nikita Revenco 6576e92333 refactor: do not use unnecessary type hint 2025-01-16 17:44:31 +00:00
Nikita Revenco 26c230cea0 fix: failing tests 2025-01-16 17:28:56 +00:00
Nikita Revenco 0d21faf712 docs: better description of MouseClicks 2025-01-16 17:26:33 +00:00
Nikita Revenco 931b8d9a31 refactor: simplify internal MouseClicks code 2025-01-16 17:23:27 +00:00
Nikita Revenco c4672ed902 test: add tests for MouseClicks 2025-01-16 16:59:53 +00:00
Nikita Revenco 7694517d0f docs: make docs more informative 2025-01-16 16:12:05 +00:00
Nikita Revenco 395f4bf877 docs: document the clicks field 2025-01-16 16:10:04 +00:00
Nikita Revenco 056e2ffbb2 fix: clicking the same char indexes in different views triggered double / triple click 2025-01-16 16:06:13 +00:00
Nikita Revenco 82401d87b7 fix: double click not registering after the first one 2025-01-13 09:16:14 +00:00
Nikita Revenco 6fbe9fe0bf refactor: variable names 2025-01-13 09:01:39 +00:00
Nikita Revenco 7537b0c25b refactor: use u8 for count 2025-01-13 08:40:18 +00:00
Nikita Revenco 7add058abc chore: add documentation 2025-01-12 21:36:48 +00:00
Nikita Revenco a60dc2d085 refactor: reverse && 2025-01-12 21:34:06 +00:00
Nikita Revenco a9a1e69f09 chore: remove comment 2025-01-12 21:32:39 +00:00
Nikita Revenco c43074abdb fix: 4th click on the same spot will reset the selection 2025-01-12 21:30:07 +00:00
Nikita Revenco 64390df159 refactor: extract duplicated logic 2025-01-12 20:53:00 +00:00
Nikita Revenco 78bfdb680f refactor: rename variables 2025-01-12 20:45:18 +00:00
Nikita Revenco b3831c032e refactor: move impl to a proper mod 2025-01-12 20:35:29 +00:00
Nikita Revenco 6c19d2c1c4 feat: implement double click and single click selection 2025-01-12 20:31:49 +00:00
Nikita Revenco 30b592fed9 refactor: clean up code and remove debugging statements 2025-01-12 20:10:23 +00:00
Nikita Revenco 2b76e839ad refactor: use enum for mouse click type 2025-01-12 20:07:25 +00:00
Nikita Revenco 897569fa64 fix: correct functions for double and triple click 2025-01-12 20:01:16 +00:00
Nikita Revenco 2491522c87 feat: basic MouseClicks struct 2025-01-12 17:40:50 +00:00
8 changed files with 269 additions and 33 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,22 +1129,42 @@ 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(..);
if modifiers == KeyModifiers::ALT { let selection = match editor.mouse_clicks.register_click(pos, view_id) {
let selection = doc.selection(view_id).clone(); MouseClick::Single => {
doc.set_selection(view_id, selection.push(Range::point(pos))); if modifiers == KeyModifiers::ALT {
} else if editor.mode == Mode::Select { let selection = doc.selection(view_id).clone();
// Discards non-primary selections for consistent UX with normal mode selection.push(Range::point(pos))
let primary = doc.selection(view_id).primary().put_cursor( } else if editor.mode == Mode::Select {
doc.text().slice(..), // Discards non-primary selections for consistent UX with normal mode
pos, let primary = doc.selection(view_id).primary().put_cursor(
true, doc.text().slice(..),
); pos,
editor.mouse_down_range = Some(primary); true,
doc.set_selection(view_id, Selection::single(primary.anchor, primary.head)); );
} else { editor.mouse_down_range = Some(primary);
doc.set_selection(view_id, Selection::point(pos));
} Selection::single(primary.anchor, primary.head)
} else {
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(),
} }
} }
@ -1437,7 +1440,11 @@ impl Editor {
log::error!("failed to apply workspace edit: {err:?}") log::error!("failed to apply workspace edit: {err:?}")
} }
} }
fs::rename(old_path, &new_path)?;
if old_path.exists() {
fs::rename(old_path, &new_path)?;
}
if let Some(doc) = self.document_by_path(old_path) { if let Some(doc) = self.document_by_path(old_path) {
self.set_doc_path(doc.id(), &new_path); self.set_doc_path(doc.id(), &new_path);
} }

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);
}
} }

View File

@ -937,7 +937,7 @@ indent = { tab-width = 2, unit = " " }
[[grammar]] [[grammar]]
name = "html" name = "html"
source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "29f53d8f4f2335e61bf6418ab8958dac3282077a" } source = { git = "https://github.com/tree-sitter/tree-sitter-html", rev = "cbb91a0ff3621245e890d1c50cc811bffb77a26b" }
[[language]] [[language]]
name = "python" name = "python"

View File

@ -1,3 +1,48 @@
; inherits: html (tag_name) @tag
(erroneous_end_tag_name) @error
(doctype) @constant
(attribute_name) @attribute
(comment) @comment
((attribute
(attribute_name) @attribute
(quoted_attribute_value (attribute_value) @markup.link.url))
(#any-of? @attribute "href" "src"))
((element
(start_tag
(tag_name) @tag)
(text) @markup.link.label)
(#eq? @tag "a"))
(attribute [(attribute_value) (quoted_attribute_value)] @string)
((element
(start_tag
(tag_name) @tag)
(text) @markup.bold)
(#any-of? @tag "strong" "b"))
((element
(start_tag
(tag_name) @tag)
(text) @markup.italic)
(#any-of? @tag "em" "i"))
((element
(start_tag
(tag_name) @tag)
(text) @markup.strikethrough)
(#any-of? @tag "s" "del"))
[
"<"
">"
"</"
"/>"
"<!"
] @punctuation.bracket
"=" @punctuation.delimiter
["---"] @punctuation.delimiter ["---"] @punctuation.delimiter

View File

@ -12,8 +12,6 @@
(namespace_definition name: (namespace_identifier) @namespace) (namespace_definition name: (namespace_identifier) @namespace)
(namespace_identifier) @namespace (namespace_identifier) @namespace
(qualified_identifier name: (identifier) @type.enum.variant)
(auto) @type (auto) @type
"decltype" @type "decltype" @type
@ -21,12 +19,29 @@
(reference_declarator ["&" "&&"] @type.builtin) (reference_declarator ["&" "&&"] @type.builtin)
(abstract_reference_declarator ["&" "&&"] @type.builtin) (abstract_reference_declarator ["&" "&&"] @type.builtin)
; -------
; Functions ; Functions
; -------
; Support up to 4 levels of nesting of qualifiers
; i.e. a::b::c::d::func();
(call_expression (call_expression
function: (qualified_identifier function: (qualified_identifier
name: (identifier) @function)) name: (identifier) @function))
(call_expression
function: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function)))
(call_expression
function: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function))))
(call_expression
function: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function)))))
(template_function (template_function
name: (identifier) @function) name: (identifier) @function)
@ -34,26 +49,42 @@
(template_method (template_method
name: (field_identifier) @function) name: (field_identifier) @function)
; Support up to 3 levels of nesting of qualifiers ; Support up to 4 levels of nesting of qualifiers
; i.e. a::b::c::func(); ; i.e. a::b::c::d::func();
(function_declarator (function_declarator
declarator: (qualified_identifier declarator: (qualified_identifier
name: (identifier) @function)) name: (identifier) @function))
(function_declarator (function_declarator
declarator: (qualified_identifier declarator: (qualified_identifier
name: (qualified_identifier name: (qualified_identifier
name: (identifier) @function))) name: (identifier) @function)))
(function_declarator (function_declarator
declarator: (qualified_identifier declarator: (qualified_identifier
name: (qualified_identifier name: (qualified_identifier
name: (qualified_identifier name: (qualified_identifier
name: (identifier) @function)))) name: (identifier) @function))))
(function_declarator
declarator: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function)))))
(function_declarator (function_declarator
declarator: (field_identifier) @function) declarator: (field_identifier) @function)
; Constructors
(class_specifier
(type_identifier) @type
(field_declaration_list
(function_definition
(function_declarator
(identifier) @constructor)))
(#eq? @type @constructor))
(destructor_name "~" @constructor
(identifier) @constructor)
; Parameters ; Parameters
(parameter_declaration (parameter_declaration

View File

@ -2,6 +2,7 @@
(erroneous_end_tag_name) @error (erroneous_end_tag_name) @error
(doctype) @constant (doctype) @constant
(attribute_name) @attribute (attribute_name) @attribute
(entity) @string.special.symbol
(comment) @comment (comment) @comment
((attribute ((attribute