Compare commits

...

7 Commits

Author SHA1 Message Date
Daniel S Poulin b48ec9339f
Merge e1247d6739 into 362e97e927 2025-06-16 18:06:45 +03:00
Michael Davis 362e97e927
Update tree-house to v0.3.0
This release contains some fixes to highlight ordering which could cause
panics in the markdown component for highlights arriving out of order.
2025-06-16 10:27:19 -04:00
Michael Davis ba54b6afe4
LSP: Short-circuit documentColors request for no servers
This fixes a deadlock when starting Helix with very many files, like
`hx runtime/queries/*/*.scm`. The tree-sitter query files don't have
an active language server on my machine and yet we were spawning a tokio
task to collect documentColors responses. We can skip that entirely.
Further debugging is needed to figure out why this lead to a deadlock
previously.
2025-06-16 09:42:48 -04: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
Daniel Poulin e1247d6739 Implement new textobject for indentation level
This implements a textobject corresponding to the current indentation
level of the selection(s). It is only implemented for "match mode"
bound to `i` and takes a count, where count extends the selection
leftwards additional indentation levels. Inside and Around versions are
distinguished by whether they tolerate empty lines.
2024-07-17 21:02:37 -04:00
11 changed files with 352 additions and 15 deletions

4
Cargo.lock generated
View File

@ -2810,9 +2810,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
[[package]] [[package]]
name = "tree-house" name = "tree-house"
version = "0.2.0" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "679e3296e503901cd9f6e116be5a43a9270222215bf6c78b4b1f4af5c3dcc62d" checksum = "d00ea55222392f171ae004dd13b62edd09d995633abf0c13406a8df3547fb999"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"hashbrown 0.15.4", "hashbrown 0.15.4",

View File

@ -37,7 +37,7 @@ package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2 package.helix-term.opt-level = 2
[workspace.dependencies] [workspace.dependencies]
tree-house = { version = "0.2.0", default-features = false } tree-house = { version = "0.3.0", default-features = false }
nucleo = "0.5.0" nucleo = "0.5.0"
slotmap = "1.0.7" slotmap = "1.0.7"
thiserror = "2.0" thiserror = "2.0"

View File

@ -16,6 +16,7 @@ function or block of code.
| `w` | Word | | `w` | Word |
| `W` | WORD | | `W` | WORD |
| `p` | Paragraph | | `p` | Paragraph |
| `i` | Indentation level |
| `(`, `[`, `'`, etc. | Specified surround pairs | | `(`, `[`, `'`, etc. | Specified surround pairs |
| `m` | The closest surround pair | | `m` | The closest surround pair |
| `f` | Function | | `f` | Function |

View File

@ -1,10 +1,12 @@
use std::cmp;
use std::fmt::Display; use std::fmt::Display;
use ropey::RopeSlice; use ropey::RopeSlice;
use crate::chars::{categorize_char, char_is_whitespace, CharCategory}; use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary}; use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
use crate::line_ending::rope_is_line_ending; use crate::indent::indent_level_for_line;
use crate::line_ending::{get_line_ending, rope_is_line_ending};
use crate::movement::Direction; use crate::movement::Direction;
use crate::syntax; use crate::syntax;
use crate::Range; use crate::Range;
@ -197,6 +199,92 @@ pub fn textobject_paragraph(
Range::new(anchor, head) Range::new(anchor, head)
} }
pub fn textobject_indentation_level(
slice: RopeSlice,
range: Range,
textobject: TextObject,
count: usize,
indent_width: usize,
tab_width: usize,
) -> Range {
let (mut line_start, mut line_end) = range.line_range(slice);
let mut min_indent: Option<usize> = None;
// Find the innermost indent represented by the current selection range.
// Range could be only on one line, so we need an inclusive range in the
// loop definition.
for i in line_start..=line_end {
let line = slice.line(i);
// Including empty lines leads to pathological behaviour, where having
// an empty line in a multi-line selection causes the entire buffer to
// be selected, which is not intuitively what we want.
if !rope_is_line_ending(line) {
let indent_level = indent_level_for_line(line, tab_width, indent_width);
min_indent = if let Some(prev_min_indent) = min_indent {
Some(cmp::min(indent_level, prev_min_indent))
} else {
Some(indent_level)
}
}
}
// It can happen that the selection consists of an empty line, so min_indent
// will be untouched, in which case we can skip the rest of the function
// and no-op.
if min_indent.is_none() {
return range;
}
let min_indent = min_indent.unwrap() + 1 - count;
// Traverse backwards until there are no more lines indented the same or
// greater, and extend the start of the range to it.
if line_start > 0 {
for line in slice.lines_at(line_start).reversed() {
let indent_level = indent_level_for_line(line, tab_width, indent_width);
let empty_line = rope_is_line_ending(line);
if (min_indent > 0 && indent_level >= min_indent)
|| (min_indent == 0 && !empty_line)
|| (textobject == TextObject::Around && empty_line)
{
line_start -= 1;
} else {
break;
}
}
}
// Traverse forwards until there are no more lines indented the same or
// greater, and extend the end of the range to it.
if line_end < slice.len_lines() {
for line in slice.lines_at(line_end + 1) {
let indent_level = indent_level_for_line(line, tab_width, indent_width);
let empty_line = rope_is_line_ending(line);
if (min_indent > 0 && indent_level >= min_indent)
|| (min_indent == 0 && !empty_line)
|| (textobject == TextObject::Around && empty_line)
{
line_end += 1;
} else {
break;
}
}
}
let new_char_start = slice.line_to_char(line_start);
let new_line_end_slice = slice.line(line_end);
let mut new_char_end = new_line_end_slice.chars().count() + slice.line_to_char(line_end);
// Unless the end of the new range is to the end of the buffer, we want to
// trim the final line ending from the selection.
if let Some(line_ending) = get_line_ending(&new_line_end_slice) {
new_char_end = new_char_end.saturating_sub(line_ending.len_chars());
}
Range::new(new_char_start, new_char_end).with_direction(range.direction())
}
pub fn textobject_pair_surround( pub fn textobject_pair_surround(
syntax: Option<&Syntax>, syntax: Option<&Syntax>,
slice: RopeSlice, slice: RopeSlice,
@ -497,6 +585,160 @@ mod test {
} }
} }
#[test]
fn test_textobject_indentation_level_inside() {
let tests = [
("#[|]#", "#[|]#", 1),
(
"unindented\n\t#[i|]#ndented once",
"unindented\n#[\tindented once|]#",
1,
),
(
"unindented\n\t#[i|]#ndented once\n",
"unindented\n#[\tindented once|]#\n",
1,
),
(
"unindented\n\t#[|in]#dented once\n",
"unindented\n#[|\tindented once]#\n",
1,
),
(
"#[u|]#nindented\n\tindented once\n",
"#[unindented\n\tindented once|]#\n",
1,
),
(
"unindented\n\n\t#[i|]#ndented once and separated\n",
"unindented\n\n#[\tindented once and separated|]#\n",
1,
),
(
"#[u|]#nindented\n\n\tindented once and separated\n",
"#[unindented|]#\n\n\tindented once and separated\n",
1,
),
(
"unindented\n\nunindented again\n\tindented #[once|]#\nunindented one more time",
"unindented\n\nunindented again\n#[\tindented once|]#\nunindented one more time",
1,
),
(
"unindented\n\nunindented #[again\n\tindented|]# once\nunindented one more time\n",
"unindented\n\n#[unindented again\n\tindented once\nunindented one more time|]#\n",
1,
),
(
"unindented\n\tindented #[once\n\n\tindented once|]# and separated\n\tindented once again\nunindented one more time\n",
"unindented\n#[\tindented once\n\n\tindented once and separated\n\tindented once again|]#\nunindented one more time\n",
1,
),
(
"unindented\n\tindented once\n#[\n|]#\tindented once and separated\n\tindented once again\nunindented one more time\n",
"unindented\n\tindented once\n#[\n|]#\tindented once and separated\n\tindented once again\nunindented one more time\n",
1,
),
(
"unindented\n\tindented once\n\t\tindented #[twice|]#\n\tindented once again\nunindented\n",
"unindented\n#[\tindented once\n\t\tindented twice\n\tindented once again|]#\nunindented\n",
2,
),
(
"unindented\n\tindented once\n\t\tindented #[twice|]#\n\tindented once again\nunindented\n",
"#[unindented\n\tindented once\n\t\tindented twice\n\tindented once again\nunindented|]#\n",
3,
),
];
for (before, expected, count) in tests {
let (s, selection) = crate::test::print(before);
let text = Rope::from(s.as_str());
let selection = selection.transform(|r| {
textobject_indentation_level(text.slice(..), r, TextObject::Inside, count, 4, 4)
});
let actual = crate::test::plain(s.as_ref(), &selection);
assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
}
}
#[test]
fn test_textobject_indentation_level_around() {
let tests = [
("#[|]#", "#[|]#", 1),
(
"unindented\n\t#[i|]#ndented once",
"unindented\n#[\tindented once|]#",
1,
),
(
"unindented\n\t#[i|]#ndented once\n",
"unindented\n#[\tindented once\n|]#",
1,
),
(
"unindented\n\t#[|in]#dented once\n",
"unindented\n#[|\tindented once\n]#",
1,
),
(
"#[u|]#nindented\n\tindented once\n",
"#[unindented\n\tindented once\n|]#",
1,
),
(
"unindented\n\n\t#[i|]#ndented once and separated\n",
"unindented\n#[\n\tindented once and separated\n|]#",
1,
),
(
"#[u|]#nindented\n\n\tindented once and separated\n",
"#[unindented\n\n\tindented once and separated\n|]#",
1,
),
(
"unindented\n\nunindented again\n\tindented #[once|]#\nunindented one more time",
"unindented\n\nunindented again\n#[\tindented once|]#\nunindented one more time",
1,
),
(
"unindented\n\nunindented #[again\n\tindented|]# once\nunindented one more time\n",
"#[unindented\n\nunindented again\n\tindented once\nunindented one more time\n|]#",
1,
),
(
"unindented\n\tindented #[once\n\n\tindented once|]# and separated\n\tindented once again\nunindented one more time\n",
"unindented\n#[\tindented once\n\n\tindented once and separated\n\tindented once again|]#\nunindented one more time\n",
1,
),
(
"unindented\n\tindented once\n#[\n|]#\tindented once and separated\n\tindented once again\nunindented one more time\n",
"unindented\n\tindented once\n#[\n|]#\tindented once and separated\n\tindented once again\nunindented one more time\n",
1,
),
(
"unindented\n\tindented once\n\t\tindented #[twice|]#\n\tindented once again\nunindented\n",
"unindented\n#[\tindented once\n\t\tindented twice\n\tindented once again|]#\nunindented\n",
2,
),
(
"unindented\n\tindented once\n\t\tindented #[twice|]#\n\tindented once again\nunindented\n",
"#[unindented\n\tindented once\n\t\tindented twice\n\tindented once again\nunindented\n|]#",
3,
),
];
for (before, expected, count) in tests {
let (s, selection) = crate::test::print(before);
let text = Rope::from(s.as_str());
let selection = selection.transform(|r| {
textobject_indentation_level(text.slice(..), r, TextObject::Around, count, 4, 4)
});
let actual = crate::test::plain(s.as_ref(), &selection);
assert_eq!(actual, expected, "\nbefore: `{:?}`", before);
}
}
#[test] #[test]
fn test_textobject_surround() { fn test_textobject_surround() {
// (text, [(cursor position, textobject, final range, surround char, count), ...]) // (text, [(cursor position, textobject, final range, surround char, count), ...])

View File

@ -5948,6 +5948,14 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
'T' => textobject_treesitter("test", range), 'T' => textobject_treesitter("test", range),
'e' => textobject_treesitter("entry", range), 'e' => textobject_treesitter("entry", range),
'p' => textobject::textobject_paragraph(text, range, objtype, count), 'p' => textobject::textobject_paragraph(text, range, objtype, count),
'i' => textobject::textobject_indentation_level(
text,
range,
objtype,
count,
doc.indent_width(),
doc.tab_width(),
),
'm' => textobject::textobject_pair_surround_closest( 'm' => textobject::textobject_pair_surround_closest(
doc.syntax(), doc.syntax(),
text, text,
@ -5983,6 +5991,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
("w", "Word"), ("w", "Word"),
("W", "WORD"), ("W", "WORD"),
("p", "Paragraph"), ("p", "Paragraph"),
("i", "Indentation level"),
("t", "Type definition (tree-sitter)"), ("t", "Type definition (tree-sitter)"),
("f", "Function (tree-sitter)"), ("f", "Function (tree-sitter)"),
("a", "Argument/parameter (tree-sitter)"), ("a", "Argument/parameter (tree-sitter)"),

View File

@ -81,6 +81,10 @@ fn request_document_colors(editor: &mut Editor, doc_id: DocumentId) {
}) })
.collect(); .collect();
if futures.is_empty() {
return;
}
tokio::spawn(async move { tokio::spawn(async move {
let mut all_colors = Vec::new(); let mut all_colors = Vec::new();
loop { loop {

View File

@ -1437,7 +1437,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

@ -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