Compare commits

...

4 Commits

Author SHA1 Message Date
Daniel S Poulin 1c222a55d0
Merge e1247d6739 into 395a71bf53 2025-07-24 07:11:22 +02:00
kiara 395a71bf53
languages: nix formatter (#14046) 2025-07-23 12:51:17 -04:00
Ian Hobson 1e4bf6704a
Update Koto grammar and queries, add formatter (#14049) 2025-07-23 12:47:47 -04: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
6 changed files with 268 additions and 13 deletions

View File

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

View File

@ -1,10 +1,12 @@
use std::cmp;
use std::fmt::Display;
use ropey::RopeSlice;
use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
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::syntax;
use crate::Range;
@ -197,6 +199,92 @@ pub fn textobject_paragraph(
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(
syntax: Option<&Syntax>,
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]
fn test_textobject_surround() {
// (text, [(cursor position, textobject, final range, surround char, count), ...])

View File

@ -5967,6 +5967,14 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
'e' => textobject_treesitter("entry", range),
'x' => textobject_treesitter("xml-element", range),
'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(
doc.syntax(),
text,
@ -6002,6 +6010,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
("w", "Word"),
("W", "WORD"),
("p", "Paragraph"),
("i", "Indentation level"),
("t", "Type definition (tree-sitter)"),
("f", "Function (tree-sitter)"),
("a", "Argument/parameter (tree-sitter)"),

View File

@ -1022,6 +1022,7 @@ shebangs = []
comment-token = "#"
language-servers = [ "nil", "nixd" ]
indent = { tab-width = 2, unit = " " }
formatter = { command = "nixfmt" }
[[grammar]]
name = "nix"
@ -4243,10 +4244,11 @@ comment-token = "#"
block-comment-tokens = ["#-", "-#"]
indent = { tab-width = 2, unit = " " }
language-servers = ["koto-ls"]
formatter = {command = "koto", args = ["--format"]}
[[grammar]]
name = "koto"
source = { git = "https://github.com/koto-lang/tree-sitter-koto", rev = "b420f7922d0d74905fd0d771e5b83be9ee8a8a9a" }
source = { git = "https://github.com/koto-lang/tree-sitter-koto", rev = "2ffc77c14f0ac1674384ff629bfc207b9c57ed89" }
[[language]]
name = "gpr"

View File

@ -5,11 +5,13 @@
"*"
"/"
"%"
"^"
"+="
"-="
"*="
"/="
"%="
"^="
"=="
"!="
"<"
@ -99,12 +101,18 @@
(export
(identifier) @namespace)
(call
function: (identifier) @function.method)
(chain
start: (identifier) @function)
(chain
lookup: (identifier) @variable.other.member)
(call
function: (identifier)) @function
(call_arg
(identifier) @variable.other.member)
[
(true)
(false)
@ -139,13 +147,10 @@
(self) @variable.builtin
(variable
type: (identifier) @type)
(type
_ @type)
(arg
(_ (identifier) @variable.parameter))
(ellipsis) @variable.parameter
(function
output_type: (identifier) @type)

View File

@ -11,10 +11,6 @@
(call_args
((call_arg) @parameter.inside . ","? @parameter.around) @parameter.around)
(chain
call: (tuple
((element) @parameter.inside . ","? @parameter.around) @parameter.around))
(map
((entry_inline) @entry.inside . ","? @entry.around) @entry.around)