mirror of https://github.com/helix-editor/helix
Compare commits
7 Commits
c61d5a4b74
...
55a24e7ac2
Author | SHA1 | Date |
---|---|---|
|
55a24e7ac2 | |
|
362e97e927 | |
|
ba54b6afe4 | |
|
837627dd8a | |
|
1246549afd | |
|
ada8004ea5 | |
|
c312f0baff |
|
@ -2810,9 +2810,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
|
|||
|
||||
[[package]]
|
||||
name = "tree-house"
|
||||
version = "0.2.0"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "679e3296e503901cd9f6e116be5a43a9270222215bf6c78b4b1f4af5c3dcc62d"
|
||||
checksum = "d00ea55222392f171ae004dd13b62edd09d995633abf0c13406a8df3547fb999"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"hashbrown 0.15.4",
|
||||
|
|
|
@ -37,7 +37,7 @@ package.helix-tui.opt-level = 2
|
|||
package.helix-term.opt-level = 2
|
||||
|
||||
[workspace.dependencies]
|
||||
tree-house = { version = "0.2.0", default-features = false }
|
||||
tree-house = { version = "0.3.0", default-features = false }
|
||||
nucleo = "0.5.0"
|
||||
slotmap = "1.0.7"
|
||||
thiserror = "2.0"
|
||||
|
|
|
@ -171,6 +171,15 @@ impl Range {
|
|||
self.from() <= pos && pos < self.to()
|
||||
}
|
||||
|
||||
/// Returns equal if anchor and head are the same, and disregards old_visual_position. When
|
||||
/// a single character is selected, the orientation (i.e. which is the head and which is the
|
||||
/// anchor) is indistinguishable and should be disregarded.
|
||||
pub fn visual_eq(&self, other: Range) -> bool {
|
||||
self.anchor == other.anchor && self.head == other.head
|
||||
// TODO: this does not work for graphemes like \r\n
|
||||
|| self.len() == 1 && self.from() == other.from() && self.to() == other.to()
|
||||
}
|
||||
|
||||
/// Map a range through a set of changes. Returns a new range representing
|
||||
/// the same position after the changes are applied. Note that this
|
||||
/// function runs in O(N) (N is number of changes) and can therefore
|
||||
|
@ -700,6 +709,14 @@ impl Selection {
|
|||
pub fn contains(&self, other: &Selection) -> bool {
|
||||
is_subset::<true>(self.range_bounds(), other.range_bounds())
|
||||
}
|
||||
|
||||
/// Returns true if two selections are identical as perceived by the user
|
||||
pub fn visual_eq(&self, other: Selection) -> bool {
|
||||
self.ranges()
|
||||
.iter()
|
||||
.zip(other.ranges().iter())
|
||||
.all(|(&r1, &r2)| r1.visual_eq(r2))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoIterator for &'a Selection {
|
||||
|
|
|
@ -555,6 +555,10 @@ impl MappableCommand {
|
|||
surround_delete, "Surround delete",
|
||||
select_textobject_around, "Select around object",
|
||||
select_textobject_inner, "Select inside object",
|
||||
select_next_word, "Select next whole word",
|
||||
select_next_long_word, "Select next whole long word",
|
||||
select_prev_word, "Select previous whole word",
|
||||
select_prev_long_word, "Select previous whole long word",
|
||||
goto_next_function, "Goto next function",
|
||||
goto_prev_function, "Goto previous function",
|
||||
goto_next_class, "Goto next type definition",
|
||||
|
@ -5902,75 +5906,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
|
|||
cx.on_next_key(move |cx, event| {
|
||||
cx.editor.autoinfo = None;
|
||||
if let Some(ch) = event.char() {
|
||||
let textobject = move |editor: &mut Editor| {
|
||||
let (view, doc) = current!(editor);
|
||||
let loader = editor.syn_loader.load();
|
||||
let text = doc.text().slice(..);
|
||||
|
||||
let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
|
||||
let Some(syntax) = doc.syntax() else {
|
||||
return range;
|
||||
};
|
||||
textobject::textobject_treesitter(
|
||||
text, range, objtype, obj_name, syntax, &loader, count,
|
||||
)
|
||||
};
|
||||
|
||||
if ch == 'g' && doc.diff_handle().is_none() {
|
||||
editor.set_status("Diff is not available in current buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
let textobject_change = |range: Range| -> Range {
|
||||
let diff_handle = doc.diff_handle().unwrap();
|
||||
let diff = diff_handle.load();
|
||||
let line = range.cursor_line(text);
|
||||
let hunk_idx = if let Some(hunk_idx) = diff.hunk_at(line as u32, false) {
|
||||
hunk_idx
|
||||
} else {
|
||||
return range;
|
||||
};
|
||||
let hunk = diff.nth_hunk(hunk_idx).after;
|
||||
|
||||
let start = text.line_to_char(hunk.start as usize);
|
||||
let end = text.line_to_char(hunk.end as usize);
|
||||
Range::new(start, end).with_direction(range.direction())
|
||||
};
|
||||
|
||||
let selection = doc.selection(view.id).clone().transform(|range| {
|
||||
match ch {
|
||||
'w' => textobject::textobject_word(text, range, objtype, count, false),
|
||||
'W' => textobject::textobject_word(text, range, objtype, count, true),
|
||||
't' => textobject_treesitter("class", range),
|
||||
'f' => textobject_treesitter("function", range),
|
||||
'a' => textobject_treesitter("parameter", range),
|
||||
'c' => textobject_treesitter("comment", range),
|
||||
'T' => textobject_treesitter("test", range),
|
||||
'e' => textobject_treesitter("entry", range),
|
||||
'p' => textobject::textobject_paragraph(text, range, objtype, count),
|
||||
'm' => textobject::textobject_pair_surround_closest(
|
||||
doc.syntax(),
|
||||
text,
|
||||
range,
|
||||
objtype,
|
||||
count,
|
||||
),
|
||||
'g' => textobject_change(range),
|
||||
// TODO: cancel new ranges if inconsistent surround matches across lines
|
||||
ch if !ch.is_ascii_alphanumeric() => textobject::textobject_pair_surround(
|
||||
doc.syntax(),
|
||||
text,
|
||||
range,
|
||||
objtype,
|
||||
ch,
|
||||
count,
|
||||
),
|
||||
_ => range,
|
||||
}
|
||||
});
|
||||
doc.set_selection(view.id, selection);
|
||||
};
|
||||
cx.editor.apply_motion(textobject);
|
||||
select_textobject_for_char(cx, ch, objtype, count);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -5997,6 +5933,132 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
|
|||
cx.editor.autoinfo = Some(Info::new(title, &help_text));
|
||||
}
|
||||
|
||||
fn select_textobject_for_char(
|
||||
cx: &mut Context,
|
||||
ch: char,
|
||||
objtype: textobject::TextObject,
|
||||
count: usize,
|
||||
) {
|
||||
let textobject = move |editor: &mut Editor| {
|
||||
let (view, doc) = current!(editor);
|
||||
let loader = editor.syn_loader.load();
|
||||
let text = doc.text().slice(..);
|
||||
|
||||
let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
|
||||
let Some(syntax) = doc.syntax() else {
|
||||
return range;
|
||||
};
|
||||
textobject::textobject_treesitter(
|
||||
text, range, objtype, obj_name, syntax, &loader, count,
|
||||
)
|
||||
};
|
||||
|
||||
if ch == 'g' && doc.diff_handle().is_none() {
|
||||
editor.set_status("Diff is not available in current buffer");
|
||||
return;
|
||||
}
|
||||
|
||||
let textobject_change = |range: Range| -> Range {
|
||||
let diff_handle = doc.diff_handle().unwrap();
|
||||
let diff = diff_handle.load();
|
||||
let line = range.cursor_line(text);
|
||||
let hunk_idx = if let Some(hunk_idx) = diff.hunk_at(line as u32, false) {
|
||||
hunk_idx
|
||||
} else {
|
||||
return range;
|
||||
};
|
||||
let hunk = diff.nth_hunk(hunk_idx).after;
|
||||
|
||||
let start = text.line_to_char(hunk.start as usize);
|
||||
let end = text.line_to_char(hunk.end as usize);
|
||||
Range::new(start, end).with_direction(range.direction())
|
||||
};
|
||||
|
||||
let selection = doc.selection(view.id).clone().transform(|range| {
|
||||
match ch {
|
||||
'w' => textobject::textobject_word(text, range, objtype, count, false),
|
||||
'W' => textobject::textobject_word(text, range, objtype, count, true),
|
||||
't' => textobject_treesitter("class", range),
|
||||
'f' => textobject_treesitter("function", range),
|
||||
'a' => textobject_treesitter("parameter", range),
|
||||
'c' => textobject_treesitter("comment", range),
|
||||
'T' => textobject_treesitter("test", range),
|
||||
'e' => textobject_treesitter("entry", range),
|
||||
'p' => textobject::textobject_paragraph(text, range, objtype, count),
|
||||
'm' => textobject::textobject_pair_surround_closest(
|
||||
doc.syntax(),
|
||||
text,
|
||||
range,
|
||||
objtype,
|
||||
count,
|
||||
),
|
||||
'g' => textobject_change(range),
|
||||
// TODO: cancel new ranges if inconsistent surround matches across lines
|
||||
ch if !ch.is_ascii_alphanumeric() => textobject::textobject_pair_surround(
|
||||
doc.syntax(),
|
||||
text,
|
||||
range,
|
||||
objtype,
|
||||
ch,
|
||||
count,
|
||||
),
|
||||
_ => range,
|
||||
}
|
||||
});
|
||||
doc.set_selection(view.id, selection);
|
||||
};
|
||||
cx.editor.apply_motion(textobject);
|
||||
}
|
||||
|
||||
fn select_next_word(cx: &mut Context) {
|
||||
let current_selection = get_current_selection(cx);
|
||||
select_textobject_for_char(cx, 'w', textobject::TextObject::Inside, cx.count());
|
||||
let new_selection = get_current_selection(cx);
|
||||
if current_selection.visual_eq(new_selection) {
|
||||
move_next_word_end(cx);
|
||||
select_textobject_for_char(cx, 'w', textobject::TextObject::Inside, cx.count());
|
||||
}
|
||||
}
|
||||
|
||||
fn select_next_long_word(cx: &mut Context) {
|
||||
let current_selection = get_current_selection(cx);
|
||||
select_textobject_for_char(cx, 'W', textobject::TextObject::Inside, cx.count());
|
||||
let new_selection = get_current_selection(cx);
|
||||
if current_selection.visual_eq(new_selection) {
|
||||
move_next_long_word_end(cx);
|
||||
select_textobject_for_char(cx, 'W', textobject::TextObject::Inside, cx.count());
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev_word(cx: &mut Context) {
|
||||
let current_selection = get_current_selection(cx);
|
||||
select_textobject_for_char(cx, 'w', textobject::TextObject::Inside, cx.count());
|
||||
flip_selections(cx);
|
||||
let new_selection = get_current_selection(cx);
|
||||
if current_selection.visual_eq(new_selection) {
|
||||
move_prev_word_start(cx);
|
||||
select_textobject_for_char(cx, 'w', textobject::TextObject::Inside, cx.count());
|
||||
flip_selections(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn select_prev_long_word(cx: &mut Context) {
|
||||
let current_selection = get_current_selection(cx);
|
||||
select_textobject_for_char(cx, 'W', textobject::TextObject::Inside, cx.count());
|
||||
flip_selections(cx);
|
||||
let new_selection = get_current_selection(cx);
|
||||
if current_selection.visual_eq(new_selection) {
|
||||
move_prev_long_word_start(cx);
|
||||
select_textobject_for_char(cx, 'W', textobject::TextObject::Inside, cx.count());
|
||||
flip_selections(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_selection(cx: &mut Context) -> Selection {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
doc.selection(view.id).clone()
|
||||
}
|
||||
|
||||
static SURROUND_HELP_TEXT: [(&str, &str); 6] = [
|
||||
("m", "Nearest matching pair"),
|
||||
("( or )", "Parentheses"),
|
||||
|
|
|
@ -81,6 +81,10 @@ fn request_document_colors(editor: &mut Editor, doc_id: DocumentId) {
|
|||
})
|
||||
.collect();
|
||||
|
||||
if futures.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut all_colors = Vec::new();
|
||||
loop {
|
||||
|
|
|
@ -1437,7 +1437,11 @@ impl Editor {
|
|||
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) {
|
||||
self.set_doc_path(doc.id(), &new_path);
|
||||
}
|
||||
|
|
|
@ -937,7 +937,7 @@ indent = { tab-width = 2, unit = " " }
|
|||
|
||||
[[grammar]]
|
||||
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]]
|
||||
name = "python"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -12,8 +12,6 @@
|
|||
(namespace_definition name: (namespace_identifier) @namespace)
|
||||
(namespace_identifier) @namespace
|
||||
|
||||
(qualified_identifier name: (identifier) @type.enum.variant)
|
||||
|
||||
(auto) @type
|
||||
"decltype" @type
|
||||
|
||||
|
@ -21,12 +19,29 @@
|
|||
(reference_declarator ["&" "&&"] @type.builtin)
|
||||
(abstract_reference_declarator ["&" "&&"] @type.builtin)
|
||||
|
||||
; -------
|
||||
; Functions
|
||||
|
||||
|
||||
; -------
|
||||
; Support up to 4 levels of nesting of qualifiers
|
||||
; i.e. a::b::c::d::func();
|
||||
(call_expression
|
||||
function: (qualified_identifier
|
||||
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
|
||||
name: (identifier) @function)
|
||||
|
@ -34,26 +49,42 @@
|
|||
(template_method
|
||||
name: (field_identifier) @function)
|
||||
|
||||
; Support up to 3 levels of nesting of qualifiers
|
||||
; i.e. a::b::c::func();
|
||||
; Support up to 4 levels of nesting of qualifiers
|
||||
; i.e. a::b::c::d::func();
|
||||
(function_declarator
|
||||
declarator: (qualified_identifier
|
||||
name: (identifier) @function))
|
||||
|
||||
(function_declarator
|
||||
declarator: (qualified_identifier
|
||||
name: (qualified_identifier
|
||||
name: (identifier) @function)))
|
||||
|
||||
(function_declarator
|
||||
declarator: (qualified_identifier
|
||||
name: (qualified_identifier
|
||||
name: (qualified_identifier
|
||||
name: (identifier) @function))))
|
||||
(function_declarator
|
||||
declarator: (qualified_identifier
|
||||
name: (qualified_identifier
|
||||
name: (qualified_identifier
|
||||
name: (qualified_identifier
|
||||
name: (identifier) @function)))))
|
||||
|
||||
(function_declarator
|
||||
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
|
||||
|
||||
(parameter_declaration
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
(erroneous_end_tag_name) @error
|
||||
(doctype) @constant
|
||||
(attribute_name) @attribute
|
||||
(entity) @string.special.symbol
|
||||
(comment) @comment
|
||||
|
||||
((attribute
|
||||
|
|
Loading…
Reference in New Issue