Compare commits

...

13 Commits

Author SHA1 Message Date
Skyler Hawthorne c7ad7f4162
Merge 70650ff55a into 205e7ece70 2025-06-16 10:48:09 +02:00
Pascal Kuthe 205e7ece70
update imara-diff (#13722) 2025-06-14 16:14:27 -04:00
yuri 1315b7e2b1
Feat: inlay hint length limit (#13742) 2025-06-13 11:09:21 -05:00
Lens0021 / Leslie 52192ae29e
Add Amber language server (#13763) 2025-06-13 10:05:21 -05:00
Jonas Köhnen fba1a6188a
Picker: Detect language before rendering preview (#13761) 2025-06-13 09:29:26 -05:00
polyface b90d8960a8
added crystal formatter in languages.toml (#13759)
Co-authored-by: polyface <polyface@proton.me>
2025-06-13 09:20:07 -05:00
uncenter 6e4ec96101
Highlight Ecma escape sequences (#13762) 2025-06-13 09:15:40 -05:00
CalebLarsen 62f270e5d2
chore: updated c,c++ highlights (#13747) 2025-06-12 09:28:09 -05:00
Skyler Hawthorne 70650ff55a feat(command): expand_selection_around
Introduces a new command `expand_selection_around` that expands the
selection to the parent node, like `expand_selection`, except it splits
on the selection you start with and continues expansion around this
initial selection.
2025-05-26 19:22:20 -04:00
Skyler Hawthorne 57d7b4fb4a Generalize View::object_selections into map
This allows using multiple distinct state histories. By default, all
history is also cleared any time a view's selection is set, unless
explicitly told to save the state. This way, we can have control over
when selection history is saved. They are also cleared on any text
edit, since an edit could invalidate the previous selection, potentially
causing a panic.

Additionally, the object selections have been moved into `Document`
so that it is easier to manipulate them when changes to the document
happen. They have been put into a wrapper struct named `ViewData`, where
the intention is that any further fields that we want to add in the
future that must be associated with a view, but are more convenient to
store in a document, can be added here, instead of further polluting the
core `Document` type.
2025-05-26 19:22:20 -04:00
Skyler Hawthorne 4b3aad7830 Add Selection::{overlaps,without} and fix contains
Adds two helper functions to `Selection`:

* `overlaps`: tests whether two `Selection`s contain any ranges which
  overlap with each other
* `without`: Computes a new `Selection` that is the set difference
  of two `Selection`s, i.e. everything in the first `Selection`
  with everything that overlaps in the second `Selection` removed,
  potentially leaving holes in the original ranges.

It also fixes a bug with `Selection::contains`: it assumes that if the
second `Selection` has a greater number of ranges than the first, then
the first cannot contain the second; but this is false, since one range
from the first could contain multiple smaller ranges in the second.
2025-05-26 19:22:20 -04:00
Skyler Hawthorne 8fbb9566fb shrink_selection without history selects first contained child
This changes the behavior of `shrink_selection` to iterate through child
nodes until it finds one that is contained within the selection, with
at least one of the ends of the selection being exclusively inside the
starting selection (though not necessarily both ends). This produces
more intuitive behavior for selecting the "first logical thing" inside
the selection.
2025-05-26 19:22:20 -04:00
Skyler Hawthorne f6e1a2f599 backfill expand/shrink tests 2025-05-26 19:22:20 -04:00
25 changed files with 1016 additions and 251 deletions

16
Cargo.lock generated
View File

@ -748,7 +748,7 @@ dependencies = [
"gix-trace",
"gix-traverse",
"gix-worktree",
"imara-diff",
"imara-diff 0.1.8",
"thiserror 2.0.12",
]
@ -1409,7 +1409,7 @@ dependencies = [
"helix-loader",
"helix-parsec",
"helix-stdx",
"imara-diff",
"imara-diff 0.2.0",
"indoc",
"log",
"nucleo",
@ -1604,7 +1604,7 @@ dependencies = [
"gix",
"helix-core",
"helix-event",
"imara-diff",
"imara-diff 0.2.0",
"log",
"parking_lot",
"tempfile",
@ -1848,6 +1848,16 @@ dependencies = [
"hashbrown 0.15.4",
]
[[package]]
name = "imara-diff"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f01d462f766df78ab820dd06f5eb700233c51f0f4c2e846520eaf4ba6aa5c5c"
dependencies = [
"hashbrown 0.15.4",
"memchr",
]
[[package]]
name = "indexmap"
version = "2.9.0"

View File

@ -157,6 +157,7 @@ The following statusline elements can be configured:
| `display-progress-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-inlay-hints` | Display inlay hints[^2] | `false` |
| `inlay-hints-length-limit` | Maximum displayed length (non-zero number) of inlay hints | Unset by default |
| `display-color-swatches` | Show color swatches next to colors | `true` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` |

View File

@ -4,7 +4,7 @@
| adl | ✓ | ✓ | ✓ | |
| agda | ✓ | | | |
| alloy | ✓ | | | |
| amber | ✓ | | | |
| amber | ✓ | | | `amber-lsp` |
| astro | ✓ | | | `astro-ls` |
| awk | ✓ | ✓ | | `awk-language-server` |
| bash | ✓ | ✓ | ✓ | `bash-language-server` |

View File

@ -46,8 +46,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
imara-diff = "0.1.8"
imara-diff = "0.2.0"
encoding_rs = "0.8"
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }

View File

@ -1,51 +1,22 @@
use std::ops::Range;
use std::time::Instant;
use imara_diff::intern::InternedInput;
use imara_diff::Algorithm;
use imara_diff::{Algorithm, Diff, Hunk, IndentHeuristic, IndentLevel, InternedInput};
use ropey::RopeSlice;
use crate::{ChangeSet, Rope, Tendril, Transaction};
/// A `imara_diff::Sink` that builds a `ChangeSet` for a character diff of a hunk
struct CharChangeSetBuilder<'a> {
res: &'a mut ChangeSet,
hunk: &'a InternedInput<char>,
pos: u32,
}
impl imara_diff::Sink for CharChangeSetBuilder<'_> {
type Out = ();
fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
self.res.retain((before.start - self.pos) as usize);
self.res.delete(before.len());
self.pos = before.end;
let res = self.hunk.after[after.start as usize..after.end as usize]
.iter()
.map(|&token| self.hunk.interner[token])
.collect();
self.res.insert(res);
}
fn finish(self) -> Self::Out {
self.res.retain(self.hunk.before.len() - self.pos as usize);
}
}
struct LineChangeSetBuilder<'a> {
struct ChangeSetBuilder<'a> {
res: ChangeSet,
after: RopeSlice<'a>,
file: &'a InternedInput<RopeSlice<'a>>,
current_hunk: InternedInput<char>,
char_diff: Diff,
pos: u32,
}
impl imara_diff::Sink for LineChangeSetBuilder<'_> {
type Out = ChangeSet;
fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
impl ChangeSetBuilder<'_> {
fn process_hunk(&mut self, before: Range<u32>, after: Range<u32>) {
let len = self.file.before[self.pos as usize..before.start as usize]
.iter()
.map(|&it| self.file.interner[it].len_chars())
@ -109,25 +80,36 @@ impl imara_diff::Sink for LineChangeSetBuilder<'_> {
.flat_map(|&it| self.file.interner[it].chars());
self.current_hunk.update_before(hunk_before);
self.current_hunk.update_after(hunk_after);
// the histogram heuristic does not work as well
// for characters because the same characters often reoccur
// use myer diff instead
imara_diff::diff(
self.char_diff.compute_with(
Algorithm::Myers,
&self.current_hunk,
CharChangeSetBuilder {
res: &mut self.res,
hunk: &self.current_hunk,
pos: 0,
},
&self.current_hunk.before,
&self.current_hunk.after,
self.current_hunk.interner.num_tokens(),
);
let mut pos = 0;
for Hunk { before, after } in self.char_diff.hunks() {
self.res.retain((before.start - pos) as usize);
self.res.delete(before.len());
pos = before.end;
let res = self.current_hunk.after[after.start as usize..after.end as usize]
.iter()
.map(|&token| self.current_hunk.interner[token])
.collect();
self.res.insert(res);
}
self.res
.retain(self.current_hunk.before.len() - pos as usize);
// reuse allocations
self.current_hunk.clear();
}
}
fn finish(mut self) -> Self::Out {
fn finish(mut self) -> ChangeSet {
let len = self.file.before[self.pos as usize..]
.iter()
.map(|&it| self.file.interner[it].len_chars())
@ -140,7 +122,7 @@ impl imara_diff::Sink for LineChangeSetBuilder<'_> {
struct RopeLines<'a>(RopeSlice<'a>);
impl<'a> imara_diff::intern::TokenSource for RopeLines<'a> {
impl<'a> imara_diff::TokenSource for RopeLines<'a> {
type Token = RopeSlice<'a>;
type Tokenizer = ropey::iter::Lines<'a>;
@ -161,15 +143,23 @@ pub fn compare_ropes(before: &Rope, after: &Rope) -> Transaction {
let res = ChangeSet::with_capacity(32);
let after = after.slice(..);
let file = InternedInput::new(RopeLines(before.slice(..)), RopeLines(after));
let builder = LineChangeSetBuilder {
let mut builder = ChangeSetBuilder {
res,
file: &file,
after,
pos: 0,
current_hunk: InternedInput::default(),
char_diff: Diff::default(),
};
let res = imara_diff::diff(Algorithm::Histogram, &file, builder).into();
let mut diff = Diff::compute(Algorithm::Histogram, &file);
diff.postprocess_with_heuristic(
&file,
IndentHeuristic::new(|token| IndentLevel::for_ascii_line(file.interner[token].bytes(), 4)),
);
for hunk in diff.hunks() {
builder.process_hunk(hunk.before, hunk.after)
}
let res = builder.finish().into();
log::debug!(
"rope diff took {}s",

View File

@ -25,38 +25,59 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection)
}
pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(
syntax,
text,
selection,
|cursor| {
cursor.goto_first_child();
},
None,
)
selection.transform(move |range| {
let (from, to) = range.into_byte_range(text);
let mut cursor = syntax.walk();
cursor.reset_to_byte_range(from, to);
if let Some(node) = cursor
.into_iter()
.find(|node| node.is_named() && node.is_contained_within(from..to))
{
return Range::from_node(node, text, range.direction());
}
range
})
}
pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(
syntax,
text,
selection,
|cursor| {
while !cursor.goto_next_sibling() {
if !cursor.goto_parent() {
break;
}
selection.transform(move |range| {
let (from, to) = range.into_byte_range(text);
let mut cursor = syntax.walk();
cursor.reset_to_byte_range(from, to);
while !cursor.goto_next_sibling() {
if !cursor.goto_parent() {
return range;
}
},
Some(Direction::Forward),
)
}
Range::from_node(cursor.node(), text, Direction::Forward)
})
}
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
selection.transform(move |range| {
let (from, to) = range.into_byte_range(text);
let mut cursor = syntax.walk();
cursor.reset_to_byte_range(from, to);
while !cursor.goto_previous_sibling() {
if !cursor.goto_parent() {
return range;
}
}
Range::from_node(cursor.node(), text, Direction::Backward)
})
}
pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
let mut cursor = syntax.walk();
selection.transform_iter(move |range| {
let (from, to) = range.into_byte_range(text);
cursor.reset_to_byte_range(from as u32, to as u32);
cursor.reset_to_byte_range(from, to);
if !cursor.goto_parent_with(|parent| parent.child_count() > 1) {
return vec![range].into_iter();
@ -70,7 +91,7 @@ pub fn select_all_children(syntax: &Syntax, text: RopeSlice, selection: Selectio
let mut cursor = syntax.walk();
selection.transform_iter(move |range| {
let (from, to) = range.into_byte_range(text);
cursor.reset_to_byte_range(from as u32, to as u32);
cursor.reset_to_byte_range(from, to);
select_children(&mut cursor, text, range).into_iter()
})
}
@ -88,47 +109,3 @@ fn select_children(cursor: &mut TreeCursor, text: RopeSlice, range: Range) -> Ve
vec![range]
}
}
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(
syntax,
text,
selection,
|cursor| {
while !cursor.goto_previous_sibling() {
if !cursor.goto_parent() {
break;
}
}
},
Some(Direction::Backward),
)
}
fn select_node_impl<F>(
syntax: &Syntax,
text: RopeSlice,
selection: Selection,
motion: F,
direction: Option<Direction>,
) -> Selection
where
F: Fn(&mut TreeCursor),
{
let cursor = &mut syntax.walk();
selection.transform(|range| {
let from = text.char_to_byte(range.from()) as u32;
let to = text.char_to_byte(range.to()) as u32;
cursor.reset_to_byte_range(from, to);
motion(cursor);
let node = cursor.node();
let from = text.byte_to_char(node.start_byte() as usize);
let to = text.byte_to_char(node.end_byte() as usize);
Range::new(from, to).with_direction(direction.unwrap_or_else(|| range.direction()))
})
}

View File

@ -387,8 +387,11 @@ impl Range {
/// Converts this char range into an in order byte range, discarding
/// direction.
pub fn into_byte_range(&self, text: RopeSlice) -> (usize, usize) {
(text.char_to_byte(self.from()), text.char_to_byte(self.to()))
pub fn into_byte_range(&self, text: RopeSlice) -> (u32, u32) {
(
text.char_to_byte(self.from()) as u32,
text.char_to_byte(self.to()) as u32,
)
}
}
@ -700,22 +703,161 @@ impl Selection {
pub fn contains(&self, other: &Selection) -> bool {
is_subset::<true>(self.range_bounds(), other.range_bounds())
}
/// returns true if self has at least one range that overlaps with at least one range from other
pub fn overlaps(&self, other: &Selection) -> bool {
let (mut iter_self, mut iter_other) = (self.iter(), other.iter());
let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next());
loop {
match (ele_self, ele_other) {
(Some(ra), Some(rb)) => {
if ra.overlaps(rb) {
return true;
} else if ra.from() > rb.from() {
ele_self = iter_self.next();
} else {
ele_other = iter_other.next();
}
}
_ => return false,
}
}
}
/// Returns the given selection with the overlapping portions of `other`
/// cut out. If one range from this selection is equal to one from `other`,
/// this range is removed. If this results in an entirely empty selection,
/// `None` is returned.
pub fn without(self, other: &Selection) -> Option<Self> {
if other.contains(&self) {
return None;
}
let mut primary_index = self.primary_index;
let mut ranges = smallvec!();
let (mut iter_self, mut iter_other) = (self.into_iter(), other.iter());
let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next());
let mut cur_index = 0;
loop {
match (ele_self, ele_other) {
(Some(ra), Some(rb)) => {
if !ra.overlaps(rb) {
// there's no overlap and it's on the left of rb
if ra.to() <= rb.from() {
ranges.push(ra);
ele_self = iter_self.next();
cur_index += 1;
// otherwise it must be on the right, so move rb forward
} else {
ele_other = iter_other.next();
}
continue;
}
// otherwise there is overlap, so truncate or split
if rb.contains_range(&ra) {
ele_self = iter_self.next();
cur_index += 1;
continue;
}
// [ ra ]
// [ rb ]
if ra.from() <= rb.from() && ra.to() <= rb.to() && ra.to() >= rb.from() {
let new = if ra.direction() == Direction::Backward {
Range::new(rb.from(), ra.head)
} else {
Range::new(ra.anchor, rb.from())
};
ranges.push(new);
ele_self = iter_self.next();
cur_index += 1;
// [ ra ]
// [ rb ]
} else if ra.from() >= rb.from() && ra.to() >= rb.to() && ra.from() <= rb.to() {
let new = if ra.direction() == Direction::Backward {
Range::new(ra.anchor, rb.to() + 1)
} else {
Range::new(rb.to(), ra.head)
};
// don't settle on the new range yet because the next
// rb could chop off the other end of ra
ele_self = Some(new);
ele_other = iter_other.next();
// [ ra ]
// [ rb ]
} else if ra.from() < rb.from() && ra.to() > rb.to() {
// we must split the range into two
let left = if ra.direction() == Direction::Backward {
Range::new(rb.from(), ra.head)
} else {
Range::new(ra.anchor, rb.from())
};
let right = if ra.direction() == Direction::Backward {
Range::new(ra.anchor, rb.to())
} else {
Range::new(rb.to(), ra.head)
};
// We do NOT push right onto the results right away.
// We must put it back into the iterator and check it
// again in case a further range splits it again.
ranges.push(left);
ele_other = iter_other.next();
// offset the primary index whenever we split
if cur_index < primary_index {
primary_index += 1;
}
cur_index += 1;
ele_self = Some(right);
}
}
// the rest just get included as is
(Some(range), None) => {
ranges.push(range);
ele_self = iter_self.next();
cur_index += 1;
}
// exhausted `self`, nothing left to do
(None, _) => {
break;
}
}
}
if ranges.is_empty() {
return None;
}
Some(Selection::new(ranges, primary_index))
}
}
impl<'a> IntoIterator for &'a Selection {
type Item = &'a Range;
type IntoIter = std::slice::Iter<'a, Range>;
fn into_iter(self) -> std::slice::Iter<'a, Range> {
fn into_iter(self) -> Self::IntoIter {
self.ranges().iter()
}
}
impl IntoIterator for Selection {
type Item = Range;
type IntoIter = smallvec::IntoIter<[Range; 1]>;
type IntoIter = smallvec::IntoIter<[Self::Item; 1]>;
fn into_iter(self) -> smallvec::IntoIter<[Range; 1]> {
fn into_iter(self) -> Self::IntoIter {
self.ranges.into_iter()
}
}
@ -882,6 +1024,7 @@ pub fn split_on_matches(text: RopeSlice, selection: &Selection, regex: &rope::Re
#[cfg(test)]
mod test {
use super::*;
use crate::test;
use crate::Rope;
#[test]
@ -972,7 +1115,7 @@ mod test {
}
#[test]
fn test_overlaps() {
fn test_range_overlaps() {
fn overlaps(a: (usize, usize), b: (usize, usize)) -> bool {
Range::new(a.0, a.1).overlaps(&Range::new(b.0, b.1))
}
@ -1022,6 +1165,160 @@ mod test {
assert!(overlaps((1, 1), (1, 1)));
}
#[test]
fn test_selection_overlaps() {
fn overlaps(a: &[(usize, usize)], b: &[(usize, usize)]) -> bool {
let a = Selection::new(
a.iter()
.map(|(anchor, head)| Range::new(*anchor, *head))
.collect(),
0,
);
let b = Selection::new(
b.iter()
.map(|(anchor, head)| Range::new(*anchor, *head))
.collect(),
0,
);
a.overlaps(&b)
}
// Two non-zero-width ranges, no overlap.
assert!(!overlaps(&[(0, 3)], &[(3, 6)]));
assert!(!overlaps(&[(0, 3)], &[(6, 3)]));
assert!(!overlaps(&[(3, 0)], &[(3, 6)]));
assert!(!overlaps(&[(3, 0)], &[(6, 3)]));
assert!(!overlaps(&[(3, 6)], &[(0, 3)]));
assert!(!overlaps(&[(3, 6)], &[(3, 0)]));
assert!(!overlaps(&[(6, 3)], &[(0, 3)]));
assert!(!overlaps(&[(6, 3)], &[(3, 0)]));
// more ranges in one or the other, no overlap
assert!(!overlaps(&[(6, 3), (9, 6)], &[(3, 0)]));
assert!(!overlaps(&[(6, 3), (6, 9)], &[(3, 0)]));
assert!(!overlaps(&[(3, 6), (9, 6)], &[(3, 0)]));
assert!(!overlaps(&[(3, 6), (6, 9)], &[(3, 0)]));
assert!(!overlaps(&[(6, 3), (9, 6)], &[(0, 3)]));
assert!(!overlaps(&[(6, 3), (6, 9)], &[(0, 3)]));
assert!(!overlaps(&[(3, 6), (9, 6)], &[(0, 3)]));
assert!(!overlaps(&[(3, 6), (6, 9)], &[(0, 3)]));
assert!(!overlaps(&[(6, 3)], &[(3, 0), (9, 6)]));
assert!(!overlaps(&[(6, 3)], &[(3, 0), (6, 9)]));
assert!(!overlaps(&[(3, 6)], &[(3, 0), (9, 6)]));
assert!(!overlaps(&[(3, 6)], &[(3, 0), (6, 9)]));
assert!(!overlaps(&[(6, 3)], &[(0, 3), (9, 6)]));
assert!(!overlaps(&[(6, 3)], &[(0, 3), (6, 9)]));
assert!(!overlaps(&[(3, 6)], &[(0, 3), (9, 6)]));
assert!(!overlaps(&[(3, 6)], &[(0, 3), (6, 9)]));
// Two non-zero-width ranges, overlap.
assert!(overlaps(&[(0, 4)], &[(3, 6)]));
assert!(overlaps(&[(0, 4)], &[(6, 3)]));
assert!(overlaps(&[(4, 0)], &[(3, 6)]));
assert!(overlaps(&[(4, 0)], &[(6, 3)]));
assert!(overlaps(&[(3, 6)], &[(0, 4)]));
assert!(overlaps(&[(3, 6)], &[(4, 0)]));
assert!(overlaps(&[(6, 3)], &[(0, 4)]));
assert!(overlaps(&[(6, 3)], &[(4, 0)]));
// Two non-zero-width ranges, overlap, extra from one or the other
assert!(overlaps(&[(0, 4), (7, 10)], &[(3, 6)]));
assert!(overlaps(&[(0, 4), (7, 10)], &[(6, 3)]));
assert!(overlaps(&[(4, 0), (7, 10)], &[(3, 6)]));
assert!(overlaps(&[(4, 0), (7, 10)], &[(6, 3)]));
assert!(overlaps(&[(3, 6), (7, 10)], &[(0, 4)]));
assert!(overlaps(&[(3, 6), (7, 10)], &[(4, 0)]));
assert!(overlaps(&[(6, 3), (7, 10)], &[(0, 4)]));
assert!(overlaps(&[(6, 3), (7, 10)], &[(4, 0)]));
assert!(overlaps(&[(0, 4), (10, 7)], &[(3, 6)]));
assert!(overlaps(&[(0, 4), (10, 7)], &[(6, 3)]));
assert!(overlaps(&[(4, 0), (10, 7)], &[(3, 6)]));
assert!(overlaps(&[(4, 0), (10, 7)], &[(6, 3)]));
assert!(overlaps(&[(3, 6), (10, 7)], &[(0, 4)]));
assert!(overlaps(&[(3, 6), (10, 7)], &[(4, 0)]));
assert!(overlaps(&[(6, 3), (10, 7)], &[(0, 4)]));
assert!(overlaps(&[(6, 3), (10, 7)], &[(4, 0)]));
assert!(overlaps(&[(0, 4)], &[(3, 6), (7, 10)]));
assert!(overlaps(&[(0, 4)], &[(6, 3), (7, 10)]));
assert!(overlaps(&[(4, 0)], &[(3, 6), (7, 10)]));
assert!(overlaps(&[(4, 0)], &[(6, 3), (7, 10)]));
assert!(overlaps(&[(3, 6)], &[(0, 4), (7, 10)]));
assert!(overlaps(&[(3, 6)], &[(4, 0), (7, 10)]));
assert!(overlaps(&[(6, 3)], &[(0, 4), (7, 10)]));
assert!(overlaps(&[(6, 3)], &[(4, 0), (7, 10)]));
assert!(overlaps(&[(0, 4)], &[(3, 6), (10, 7)]));
assert!(overlaps(&[(0, 4)], &[(6, 3), (10, 7)]));
assert!(overlaps(&[(4, 0)], &[(3, 6), (10, 7)]));
assert!(overlaps(&[(4, 0)], &[(6, 3), (10, 7)]));
assert!(overlaps(&[(3, 6)], &[(0, 4), (10, 7)]));
assert!(overlaps(&[(3, 6)], &[(4, 0), (10, 7)]));
assert!(overlaps(&[(6, 3)], &[(0, 4), (10, 7)]));
assert!(overlaps(&[(6, 3)], &[(4, 0), (10, 7)]));
// Zero-width and non-zero-width range, no overlap.
assert!(!overlaps(&[(0, 3)], &[(3, 3)]));
assert!(!overlaps(&[(3, 0)], &[(3, 3)]));
assert!(!overlaps(&[(3, 3)], &[(0, 3)]));
assert!(!overlaps(&[(3, 3)], &[(3, 0)]));
assert!(!overlaps(&[(0, 3), (7, 10)], &[(3, 3)]));
assert!(!overlaps(&[(3, 0), (7, 10)], &[(3, 3)]));
assert!(!overlaps(&[(3, 3), (7, 10)], &[(0, 3)]));
assert!(!overlaps(&[(3, 3), (7, 10)], &[(3, 0)]));
assert!(!overlaps(&[(0, 3)], &[(3, 3), (7, 10)]));
assert!(!overlaps(&[(3, 0)], &[(3, 3), (7, 10)]));
assert!(!overlaps(&[(3, 3)], &[(0, 3), (7, 10)]));
assert!(!overlaps(&[(3, 3)], &[(3, 0), (7, 10)]));
// Zero-width and non-zero-width range, overlap.
assert!(overlaps(&[(1, 4)], &[(1, 1)]));
assert!(overlaps(&[(4, 1)], &[(1, 1)]));
assert!(overlaps(&[(1, 1)], &[(1, 4)]));
assert!(overlaps(&[(1, 1)], &[(4, 1)]));
assert!(overlaps(&[(1, 4)], &[(3, 3)]));
assert!(overlaps(&[(4, 1)], &[(3, 3)]));
assert!(overlaps(&[(3, 3)], &[(1, 4)]));
assert!(overlaps(&[(3, 3)], &[(4, 1)]));
assert!(overlaps(&[(1, 4), (7, 10)], &[(1, 1)]));
assert!(overlaps(&[(4, 1), (7, 10)], &[(1, 1)]));
assert!(overlaps(&[(1, 1), (7, 10)], &[(1, 4)]));
assert!(overlaps(&[(1, 1), (7, 10)], &[(4, 1)]));
assert!(overlaps(&[(1, 4), (7, 10)], &[(3, 3)]));
assert!(overlaps(&[(4, 1), (7, 10)], &[(3, 3)]));
assert!(overlaps(&[(3, 3), (7, 10)], &[(1, 4)]));
assert!(overlaps(&[(3, 3), (7, 10)], &[(4, 1)]));
assert!(overlaps(&[(1, 4)], &[(1, 1), (7, 10)]));
assert!(overlaps(&[(4, 1)], &[(1, 1), (7, 10)]));
assert!(overlaps(&[(1, 1)], &[(1, 4), (7, 10)]));
assert!(overlaps(&[(1, 1)], &[(4, 1), (7, 10)]));
assert!(overlaps(&[(1, 4)], &[(3, 3), (7, 10)]));
assert!(overlaps(&[(4, 1)], &[(3, 3), (7, 10)]));
assert!(overlaps(&[(3, 3)], &[(1, 4), (7, 10)]));
assert!(overlaps(&[(3, 3)], &[(4, 1), (7, 10)]));
// Two zero-width ranges, no overlap.
assert!(!overlaps(&[(0, 0)], &[(1, 1)]));
assert!(!overlaps(&[(1, 1)], &[(0, 0)]));
assert!(!overlaps(&[(0, 0), (2, 2)], &[(1, 1)]));
assert!(!overlaps(&[(0, 0), (2, 2)], &[(1, 1)]));
assert!(!overlaps(&[(1, 1)], &[(0, 0), (2, 2)]));
assert!(!overlaps(&[(1, 1)], &[(0, 0), (2, 2)]));
// Two zero-width ranges, overlap.
assert!(overlaps(&[(1, 1)], &[(1, 1)]));
assert!(overlaps(&[(1, 1), (2, 2)], &[(1, 1)]));
assert!(overlaps(&[(1, 1)], &[(1, 1), (2, 2)]));
}
#[test]
fn test_grapheme_aligned() {
let r = Rope::from_str("\r\nHi\r\n");
@ -1378,9 +1675,15 @@ mod test {
// multiple matches
assert!(contains(vec!((1, 1), (2, 2)), vec!((1, 1), (2, 2))));
// smaller set can't contain bigger
// extra items out of range
assert!(!contains(vec!((1, 1)), vec!((1, 1), (2, 2))));
// one big range can contain multiple smaller ranges
assert!(contains(
vec!((1, 10)),
vec!((1, 1), (2, 2), (3, 3), (3, 5), (7, 10))
));
assert!(contains(
vec!((1, 1), (2, 4), (5, 6), (7, 9), (10, 13)),
vec!((3, 4), (7, 9))
@ -1393,4 +1696,143 @@ mod test {
vec!((1, 2), (3, 4), (7, 9))
));
}
#[test]
fn test_selection_without() {
let without = |one, two, expected: Option<_>| {
println!("one: {:?}", one);
println!("two: {:?}", two);
println!("expected: {:?}", expected);
let (one_text, one_sel) = test::print(one);
let (two_text, two_sel) = test::print(two);
assert_eq!(one_text, two_text); // sanity
let actual_sel = one_sel.without(&two_sel);
let expected_sel = expected.map(|exp| {
let (expected_text, expected_sel) = test::print(exp);
assert_eq!(two_text, expected_text);
expected_sel
});
let actual = actual_sel
.as_ref()
.map(|sel| test::plain(two_text.to_string(), sel));
println!("actual: {:?}\n\n", actual);
assert_eq!(
expected_sel,
actual_sel,
"expected: {:?}, got: {:?}",
expected_sel
.as_ref()
.map(|sel| test::plain(two_text.to_string(), sel)),
actual,
);
};
without(
"#[foo bar baz|]#",
"foo #[bar|]# baz",
Some("#[foo |]#bar#( baz|)#"),
);
without("#[foo bar baz|]#", "#[foo bar baz|]#", None);
without("#[foo bar|]# baz", "#[foo bar baz|]#", None);
without("foo #[bar|]# baz", "#[foo bar baz|]#", None);
// direction is preserved
without(
"#[|foo bar baz]#",
"foo #[|bar]# baz",
Some("#[|foo ]#bar#(| baz)#"),
);
// preference for direction is given to the left
without(
"#[|foo bar baz]#",
"foo #[bar|]# baz",
Some("#[|foo ]#bar#(| baz)#"),
);
// disjoint ranges on the right are ignored
without(
"#[foo bar|]# baz",
"foo bar #[baz|]#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo bar|]# baz",
"foo bar#[ baz|]#",
Some("#[foo bar|]# baz"),
);
without(
"#(foo|)# #[bar|]# baz",
"foo#[ b|]#ar ba#(z|)#",
Some("#(foo|)# b#[ar|]# baz"),
);
// ranges contained by those one on the right are removed
without(
"#[foo bar|]# #(b|)#az",
"foo bar#[ b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo|]# bar #(baz|)#",
"foo bar#[ b|]#a#(z|)#",
Some("#[foo|]# bar b#(a|)#z"),
);
without(
"#[foo bar|]# #(b|)#az",
"foo bar #[b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo bar|]# #(b|)#a#(z|)#",
"foo bar #[b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
without(
"#[foo bar|]# #(b|)#a#(z|)#",
"foo bar #[b|]#a#(z|)#",
Some("#[foo bar|]# baz"),
);
// more than one range intersected by a single range on the right
without(
"#[foo bar|]# #(baz|)#",
"foo b#[ar ba|]#z",
Some("#[foo b|]#ar ba#(z|)#"),
);
// partial overlap
without(
"#[foo bar|]# baz",
"foo #[bar baz|]#",
Some("#[foo |]#bar baz"),
);
without(
"#[foo bar|]# baz",
"foo#[ bar baz|]#",
Some("#[foo|]# bar baz"),
);
without(
"#[foo bar|]# baz",
"foo ba#[r baz|]#",
Some("#[foo ba|]#r baz"),
);
without(
"foo ba#[r baz|]#",
"#[foo bar|]# baz",
Some("foo bar#[ baz|]#"),
);
// primary selection is moved - preference given to the left of a split
without(
"#(|foo)# #(|bar baz)# #[|quux]#",
"f#(o|)#o ba#[r b|]#az q#(uu|)#x",
Some("#(|f)#o#(|o)# #(|ba)#r b#(|az)# #[|q]#uu#(|x)#"),
);
}
}

View File

@ -520,6 +520,7 @@ impl MappableCommand {
select_prev_sibling, "Select previous sibling the in syntax tree",
select_all_siblings, "Select all siblings of the current node",
select_all_children, "Select all children of the current node",
expand_selection_around, "Expand selection to parent syntax node, but exclude the selection you started with",
jump_forward, "Jump forward on jumplist",
jump_backward, "Jump backward on jumplist",
save_selection, "Save current selection to jumplist",
@ -3926,6 +3927,7 @@ fn goto_first_diag(cx: &mut Context) {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
@ -3937,6 +3939,7 @@ fn goto_last_diag(cx: &mut Context) {
Some(diag) => Selection::single(diag.range.start, diag.range.end),
None => return,
};
doc.set_selection(view.id, selection);
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
@ -3993,6 +3996,7 @@ fn goto_prev_diag(cx: &mut Context) {
view.diagnostics_handler
.immediately_show_diagnostic(doc, view.id);
};
cx.editor.apply_motion(motion)
}
@ -5364,6 +5368,10 @@ fn reverse_selection_contents(cx: &mut Context) {
// tree sitter node selection
const EXPAND_KEY: &str = "expand";
const EXPAND_AROUND_BASE_KEY: &str = "expand_around_base";
const PARENTS_KEY: &str = "parents";
fn expand_selection(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);
@ -5371,42 +5379,154 @@ fn expand_selection(cx: &mut Context) {
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
let current_selection = doc.selection(view.id);
let current_selection = doc.selection(view.id).clone();
let selection = object::expand_selection(syntax, text, current_selection.clone());
// check if selection is different from the last one
if *current_selection != selection {
// save current selection so it can be restored using shrink_selection
view.object_selections.push(current_selection.clone());
if current_selection != selection {
let prev_selections = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default();
doc.set_selection(view.id, selection);
// save current selection so it can be restored using shrink_selection
prev_selections.push(current_selection);
doc.set_selection_clear(view.id, selection, false);
}
}
};
cx.editor.apply_motion(motion);
}
fn shrink_selection(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);
let current_selection = doc.selection(view.id);
let current_selection = doc.selection(view.id).clone();
let prev_expansions = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default();
// try to restore previous selection
if let Some(prev_selection) = view.object_selections.pop() {
if current_selection.contains(&prev_selection) {
doc.set_selection(view.id, prev_selection);
return;
} else {
// clear existing selection as they can't be shrunk to anyway
view.object_selections.clear();
if let Some(prev_selection) = prev_expansions.pop() {
// allow shrinking the selection only if current selection contains the previous object selection
doc.set_selection_clear(view.id, prev_selection, false);
// Do a corresponding pop of the parents from `expand_selection_around`
doc.view_data_mut(view.id)
.object_selections
.entry(PARENTS_KEY)
.and_modify(|parents| {
parents.pop();
});
// need to do this again because borrowing
let prev_expansions = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default();
// if we've emptied out the previous expansions, then clear out the
// base history as well so it doesn't get used again erroneously
if prev_expansions.is_empty() {
doc.view_data_mut(view.id)
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.and_modify(|base| {
base.clear();
});
}
return;
}
// if not previous selection, shrink to first child
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
let selection = object::shrink_selection(syntax, text, current_selection.clone());
doc.set_selection(view.id, selection);
let selection = object::shrink_selection(syntax, text, current_selection);
doc.set_selection_clear(view.id, selection, false);
}
};
cx.editor.apply_motion(motion);
}
fn expand_selection_around(cx: &mut Context) {
let motion = |editor: &mut Editor| {
let (view, doc) = current!(editor);
if doc.syntax().is_some() {
// [NOTE] we do this pop and push dance because if we don't take
// ownership of the objects, then we require multiple
// mutable references to the view's object selections
let mut parents_selection = doc
.view_data_mut(view.id)
.object_selections
.entry(PARENTS_KEY)
.or_default()
.pop();
let mut base_selection = doc
.view_data_mut(view.id)
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.or_default()
.pop();
let current_selection = doc.selection(view.id).clone();
if parents_selection.is_none() || base_selection.is_none() {
parents_selection = Some(current_selection.clone());
base_selection = Some(current_selection.clone());
}
let text = doc.text().slice(..);
let syntax = doc.syntax().unwrap();
let outside_selection =
object::expand_selection(syntax, text, parents_selection.clone().unwrap());
let target_selection = match outside_selection
.clone()
.without(&base_selection.clone().unwrap())
{
Some(sel) => sel,
None => outside_selection.clone(),
};
// check if selection is different from the last one
if target_selection != current_selection {
// save current selection so it can be restored using shrink_selection
doc.view_data_mut(view.id)
.object_selections
.entry(EXPAND_KEY)
.or_default()
.push(current_selection);
doc.set_selection_clear(view.id, target_selection, false);
}
let parents = doc
.view_data_mut(view.id)
.object_selections
.entry(PARENTS_KEY)
.or_default();
parents.push(parents_selection.unwrap());
parents.push(outside_selection);
doc.view_data_mut(view.id)
.object_selections
.entry(EXPAND_AROUND_BASE_KEY)
.or_default()
.push(base_selection.unwrap());
}
};
cx.editor.apply_motion(motion);
}
@ -5424,6 +5544,7 @@ where
doc.set_selection(view.id, selection);
}
};
cx.editor.apply_motion(motion);
}
@ -5525,8 +5646,6 @@ fn match_brackets(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
//
fn jump_forward(cx: &mut Context) {
let count = cx.count();
let config = cx.editor.config();

View File

@ -1357,6 +1357,7 @@ fn compute_inlay_hints_for_view(
let mut padding_after_inlay_hints = Vec::new();
let doc_text = doc.text();
let inlay_hints_length_limit = doc.config.load().lsp.inlay_hints_length_limit;
for hint in hints {
let char_idx =
@ -1367,7 +1368,7 @@ fn compute_inlay_hints_for_view(
None => continue,
};
let label = match hint.label {
let mut label = match hint.label {
lsp::InlayHintLabel::String(s) => s,
lsp::InlayHintLabel::LabelParts(parts) => parts
.into_iter()
@ -1375,6 +1376,31 @@ fn compute_inlay_hints_for_view(
.collect::<Vec<_>>()
.join(""),
};
// Truncate the hint if too long
if let Some(limit) = inlay_hints_length_limit {
// Limit on displayed width
use helix_core::unicode::{
segmentation::UnicodeSegmentation, width::UnicodeWidthStr,
};
let width = label.width();
let limit = limit.get().into();
if width > limit {
let mut floor_boundary = 0;
let mut acc = 0;
for (i, grapheme_cluster) in label.grapheme_indices(true) {
acc += grapheme_cluster.width();
if acc > limit {
floor_boundary = i;
break;
}
}
label.truncate(floor_boundary);
label.push('…');
}
}
let inlay_hints_vec = match hint.kind {
Some(lsp::InlayHintKind::TYPE) => &mut type_inlay_hints,

View File

@ -87,6 +87,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
";" => collapse_selection,
"A-;" => flip_selections,
"A-o" | "A-up" => expand_selection,
"A-O" => expand_selection_around,
"A-i" | "A-down" => shrink_selection,
"A-I" | "A-S-down" => select_all_children,
"A-p" | "A-left" => select_prev_sibling,

View File

@ -585,8 +585,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
// retrieve the `Arc<Path>` key. The `path` in scope here is a `&Path` and
// we can cheaply clone the key for the preview highlight handler.
let (path, preview) = self.preview_cache.get_key_value(path).unwrap();
if matches!(preview, CachedPreview::Document(doc) if doc.language_config().is_none())
{
if matches!(preview, CachedPreview::Document(doc) if doc.syntax().is_none()) {
helix_event::send_blocking(&self.preview_highlight_handler, path.clone());
}
return Some((Preview::Cached(preview), range));
@ -624,27 +623,27 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
if content_type.is_binary() {
return Ok(CachedPreview::Binary);
}
Document::open(
let mut doc = Document::open(
&path,
None,
false,
editor.config.clone(),
editor.syn_loader.clone(),
)
.map_or(
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Cannot open document",
)),
|doc| {
// Asynchronously highlight the new document
helix_event::send_blocking(
&self.preview_highlight_handler,
path.clone(),
);
Ok(CachedPreview::Document(Box::new(doc)))
},
)
.or(Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Cannot open document",
)))?;
let loader = editor.syn_loader.load();
if let Some(language_config) = doc.detect_language_config(&loader) {
doc.language = Some(language_config);
// Asynchronously highlight the new document
helix_event::send_blocking(
&self.preview_highlight_handler,
path.clone(),
);
}
Ok(CachedPreview::Document(Box::new(doc)))
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,

View File

@ -66,16 +66,15 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook
return;
};
if doc.language_config().is_some() {
if doc.syntax().is_some() {
return;
}
let loader = editor.syn_loader.load();
let Some(language_config) = doc.detect_language_config(&loader) else {
let Some(language) = doc.language_config().map(|config| config.language()) else {
return;
};
let language = language_config.language();
doc.language = Some(language_config);
let loader = editor.syn_loader.load();
let text = doc.text().clone();
tokio::task::spawn_blocking(move || {

View File

@ -948,3 +948,198 @@ async fn match_bracket() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn expand_shrink_selection() -> anyhow::Result<()> {
let tests = vec![
// single range
(
indoc! {r##"
Some(#[thing|]#)
"##},
"<A-o><A-o>",
indoc! {r##"
#[Some(thing)|]#
"##},
),
// multi range
(
indoc! {r##"
Some(#[thing|]#)
Some(#(other_thing|)#)
"##},
"<A-o>",
indoc! {r##"
Some#[(thing)|]#
Some#((other_thing)|)#
"##},
),
// multi range collision merges
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o>",
indoc! {r##"
#[(
Some(thing),
Some(other_thing),
)|]#
"##},
),
// multi range collision merges, then shrinks back to original
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o><A-i>",
indoc! {r##"
(
#[Some(thing)|]#,
#(Some(other_thing)|)#,
)
"##},
),
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o><A-i><A-i>",
indoc! {r##"
(
Some#[(thing)|]#,
Some#((other_thing)|)#,
)
"##},
),
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o><A-i><A-i><A-i>",
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
),
// shrink with no expansion history defaults to first child
(
indoc! {r##"
#[(
Some(thing),
Some(other_thing),
)|]#
"##},
"<A-i>",
indoc! {r##"
(
#[Some(thing)|]#,
Some(other_thing),
)
"##},
),
// any movement cancels selection history and falls back to first child
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-o><A-o><A-o>jkvkkk<A-i>",
indoc! {r##"
(
#[|Some(thing)]#,
Some(other_thing),
)
"##},
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn expand_selection_around() -> anyhow::Result<()> {
let tests = vec![
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
indoc! {r##"
Some(#[thing|]#)
"##},
"<A-O><A-O>",
indoc! {r##"
#[Some(|]#thing#()|)#
"##},
),
// shrinking restores previous selection
(
indoc! {r##"
Some(#[thing|]#)
"##},
"<A-O><A-O><A-i><A-i>",
indoc! {r##"
Some(#[thing|]#)
"##},
),
// multi range collision merges expand as normal, except with the
// original selection removed from the result
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-O><A-O><A-O>",
indoc! {r##"
#[(
Some(|]#thing#(),
Some(|)#other_thing#(),
)|)#
"##},
),
(
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
"<A-O><A-O><A-O><A-i><A-i><A-i>",
indoc! {r##"
(
Some(#[thing|]#),
Some(#(other_thing|)#),
)
"##},
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}

View File

@ -20,7 +20,7 @@ parking_lot.workspace = true
arc-swap = { version = "1.7.1" }
gix = { version = "0.72.1", features = ["attributes", "status"], default-features = false, optional = true }
imara-diff = "0.1.8"
imara-diff = "0.2.0"
anyhow = "1"
log = "0.4"

View File

@ -1,5 +1,4 @@
use std::iter::Peekable;
use std::ops::Range;
use std::sync::Arc;
use helix_core::Rope;
@ -12,6 +11,8 @@ use tokio::time::Instant;
use crate::diff::worker::DiffWorker;
pub use imara_diff::Hunk;
mod line_cache;
mod worker;
@ -52,8 +53,8 @@ impl DiffHandle {
let worker = DiffWorker {
channel: receiver,
diff: diff.clone(),
new_hunks: Vec::default(),
diff_finished_notify: Arc::default(),
diff_alloc: imara_diff::Diff::default(),
};
let handle = tokio::spawn(worker.run(diff_base, doc));
let differ = DiffHandle {
@ -118,48 +119,6 @@ const MAX_DIFF_LINES: usize = 64 * u16::MAX as usize;
// cap average line length to 128 for files with MAX_DIFF_LINES
const MAX_DIFF_BYTES: usize = MAX_DIFF_LINES * 128;
/// A single change in a file potentially spanning multiple lines
/// Hunks produced by the differs are always ordered by their position
/// in the file and non-overlapping.
/// Specifically for any two hunks `x` and `y` the following properties hold:
///
/// ``` no_compile
/// assert!(x.before.end <= y.before.start);
/// assert!(x.after.end <= y.after.start);
/// ```
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct Hunk {
pub before: Range<u32>,
pub after: Range<u32>,
}
impl Hunk {
/// Can be used instead of `Option::None` for better performance
/// because lines larger then `i32::MAX` are not supported by `imara-diff` anyways.
/// Has some nice properties where it usually is not necessary to check for `None` separately:
/// Empty ranges fail contains checks and also fails smaller then checks.
pub const NONE: Hunk = Hunk {
before: u32::MAX..u32::MAX,
after: u32::MAX..u32::MAX,
};
/// Inverts a change so that `before`
pub fn invert(&self) -> Hunk {
Hunk {
before: self.after.clone(),
after: self.before.clone(),
}
}
pub fn is_pure_insertion(&self) -> bool {
self.before.is_empty()
}
pub fn is_pure_removal(&self) -> bool {
self.after.is_empty()
}
}
/// A list of changes in a file sorted in ascending
/// non-overlapping order
#[derive(Debug)]

View File

@ -13,7 +13,7 @@
use std::mem::transmute;
use helix_core::{Rope, RopeSlice};
use imara_diff::intern::{InternedInput, Interner};
use imara_diff::{InternedInput, Interner};
use super::{MAX_DIFF_BYTES, MAX_DIFF_LINES};

View File

@ -1,9 +1,7 @@
use std::mem::swap;
use std::ops::Range;
use std::sync::Arc;
use helix_core::{Rope, RopeSlice};
use imara_diff::intern::InternedInput;
use imara_diff::{IndentHeuristic, IndentLevel, InternedInput};
use parking_lot::RwLock;
use tokio::sync::mpsc::UnboundedReceiver;
use tokio::sync::Notify;
@ -14,7 +12,6 @@ use crate::diff::{
};
use super::line_cache::InternedRopeLines;
use super::Hunk;
#[cfg(test)]
mod test;
@ -22,8 +19,8 @@ mod test;
pub(super) struct DiffWorker {
pub channel: UnboundedReceiver<Event>,
pub diff: Arc<RwLock<DiffInner>>,
pub new_hunks: Vec<Hunk>,
pub diff_finished_notify: Arc<Notify>,
pub diff_alloc: imara_diff::Diff,
}
impl DiffWorker {
@ -76,15 +73,26 @@ impl DiffWorker {
let mut diff = self.diff.write();
diff.diff_base = diff_base;
diff.doc = doc;
swap(&mut diff.hunks, &mut self.new_hunks);
diff.hunks.clear();
diff.hunks.extend(self.diff_alloc.hunks());
drop(diff);
self.diff_finished_notify.notify_waiters();
self.new_hunks.clear();
}
fn perform_diff(&mut self, input: &InternedInput<RopeSlice>) {
imara_diff::diff(ALGORITHM, input, |before: Range<u32>, after: Range<u32>| {
self.new_hunks.push(Hunk { before, after })
})
self.diff_alloc.compute_with(
ALGORITHM,
&input.before,
&input.after,
input.interner.num_tokens(),
);
self.diff_alloc.postprocess_with(
&input.before,
&input.after,
IndentHeuristic::new(|token| {
IndentLevel::for_ascii_line(input.interner[token].bytes(), 4)
}),
);
}
}
@ -94,7 +102,7 @@ struct EventAccumulator {
render_lock: Option<RenderLock>,
}
impl EventAccumulator {
impl<'a> EventAccumulator {
fn new() -> EventAccumulator {
EventAccumulator {
diff_base: None,

View File

@ -1319,15 +1319,27 @@ impl Document {
Ok(())
}
/// Select text within the [`Document`].
pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
/// Select text within the [`Document`], optionally clearing the previous selection state.
pub fn set_selection_clear(&mut self, view_id: ViewId, selection: Selection, clear_prev: bool) {
// TODO: use a transaction?
self.selections
.insert(view_id, selection.ensure_invariants(self.text().slice(..)));
helix_event::dispatch(SelectionDidChange {
doc: self,
view: view_id,
})
});
if clear_prev {
self.view_data
.entry(view_id)
.and_modify(|view_data| view_data.object_selections.clear());
}
}
/// Select text within the [`Document`].
pub fn set_selection(&mut self, view_id: ViewId, selection: Selection) {
self.set_selection_clear(view_id, selection, true);
}
/// Find the origin selection of the text in a document, i.e. where
@ -1519,6 +1531,12 @@ impl Document {
apply_inlay_hint_changes(padding_after_inlay_hints);
}
// clear out all associated view object selections, as they are no
// longer valid
self.view_data
.values_mut()
.for_each(|view_data| view_data.object_selections.clear());
helix_event::dispatch(DocumentDidChange {
doc: self,
view: view_id,
@ -1955,13 +1973,13 @@ impl Document {
&self.selections
}
fn view_data(&self, view_id: ViewId) -> &ViewData {
pub fn view_data(&self, view_id: ViewId) -> &ViewData {
self.view_data
.get(&view_id)
.expect("This should only be called after ensure_view_init")
}
fn view_data_mut(&mut self, view_id: ViewId) -> &mut ViewData {
pub fn view_data_mut(&mut self, view_id: ViewId) -> &mut ViewData {
self.view_data.entry(view_id).or_default()
}
@ -2279,9 +2297,13 @@ impl Document {
}
}
/// Stores data needed for views that are tied to this specific Document.
#[derive(Debug, Default)]
pub struct ViewData {
view_position: ViewPosition,
/// used to store previous selections of tree-sitter objects
pub object_selections: HashMap<&'static str, Vec<Selection>>,
}
#[derive(Clone, Debug)]
@ -2332,6 +2354,7 @@ mod test {
Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
);
let view = ViewId::default();
doc.ensure_view_init(view);
doc.set_selection(view, Selection::single(0, 0));
let transaction =
@ -2370,7 +2393,9 @@ mod test {
Arc::new(ArcSwap::new(Arc::new(Config::default()))),
Arc::new(ArcSwap::from_pointee(syntax::Loader::default())),
);
let view = ViewId::default();
doc.ensure_view_init(view);
doc.set_selection(view, Selection::single(5, 5));
// insert

View File

@ -29,7 +29,7 @@ use std::{
collections::{BTreeMap, HashMap, HashSet},
fs,
io::{self, stdin},
num::NonZeroUsize,
num::{NonZeroU8, NonZeroUsize},
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
@ -459,6 +459,9 @@ pub struct LspConfig {
pub display_signature_help_docs: bool,
/// Display inlay hints
pub display_inlay_hints: bool,
/// Maximum displayed length of inlay hints (excluding the added trailing `…`).
/// If it's `None`, there's no limit
pub inlay_hints_length_limit: Option<NonZeroU8>,
/// Display document color swatches
pub display_color_swatches: bool,
/// Whether to enable snippet support
@ -476,6 +479,7 @@ impl Default for LspConfig {
auto_signature_help: true,
display_signature_help_docs: true,
display_inlay_hints: false,
inlay_hints_length_limit: None,
snippets: true,
goto_reference_include_declaration: true,
display_color_swatches: true,

View File

@ -137,8 +137,6 @@ pub struct View {
// uses two docs because we want to be able to swap between the
// two last modified docs which we need to manually keep track of
pub last_modified_docs: [Option<DocumentId>; 2],
/// used to store previous selections of tree-sitter objects
pub object_selections: Vec<Selection>,
/// all gutter-related configuration settings, used primarily for gutter rendering
pub gutters: GutterConfig,
/// A mapping between documents and the last history revision the view was updated at.
@ -175,7 +173,6 @@ impl View {
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
docs_access_history: Vec::new(),
last_modified_docs: [None, None],
object_selections: Vec::new(),
gutters,
doc_revisions: HashMap::new(),
diagnostics_handler: DiagnosticsHandler::new(),

View File

@ -5,9 +5,10 @@ use-grammars = { except = [ "wren", "gemini" ] }
[language-server]
als = { command = "als" }
ada-language-server = { command = "ada_language_server" }
ada-gpr-language-server = {command = "ada_language_server", args = ["--language-gpr"]}
ada-language-server = { command = "ada_language_server" }
als = { command = "als" }
amber-lsp = { command = "amber-lsp" }
angular = {command = "ngserver", args = ["--stdio", "--tsProbeLocations", ".", "--ngProbeLocations", ".",]}
asm-lsp = { command = "asm-lsp" }
awk-language-server = { command = "awk-language-server" }
@ -648,6 +649,7 @@ comment-token = "#"
indent = { tab-width = 2, unit = " " }
grammar = "ruby"
language-servers = [ "crystalline" ]
formatter = { command = "crystal", args = ["tool", "format", "-"] }
[[language]]
name = "c-sharp"
@ -4131,6 +4133,7 @@ scope = "source.ab"
file-types = ["ab"]
comment-token = ["//", "///"]
indent = { tab-width = 4, unit = " " }
language-servers = ["amber-lsp"]
[[grammar]]
name = "amber"

View File

@ -14,8 +14,6 @@
] @keyword.storage.type
[
"extern"
"register"
(type_qualifier)
(storage_class_specifier)
] @keyword.storage.modifier
@ -55,8 +53,11 @@
(preproc_directive)
] @keyword.directive
(pointer_declarator "*" @type.builtin)
(abstract_pointer_declarator "*" @type.builtin)
"..." @punctuation
["," "." ":" "::" ";" "->"] @punctuation.delimiter
["(" ")" "[" "]" "{" "}" "[[" "]]"] @punctuation.bracket
[
"+"
@ -95,13 +96,11 @@
"?"
] @operator
(conditional_expression ":" @operator)
(conditional_expression ":" @operator) ; After punctuation
"..." @punctuation
(pointer_declarator "*" @type.builtin) ; After Operators
(abstract_pointer_declarator "*" @type.builtin)
["," "." ":" ";" "->" "::"] @punctuation.delimiter
["(" ")" "[" "]" "{" "}"] @punctuation.bracket
[(true) (false)] @constant.builtin.boolean

View File

@ -23,11 +23,6 @@
; Functions
; These casts are parsed as function calls, but are not.
((identifier) @keyword (#eq? @keyword "static_cast"))
((identifier) @keyword (#eq? @keyword "dynamic_cast"))
((identifier) @keyword (#eq? @keyword "reinterpret_cast"))
((identifier) @keyword (#eq? @keyword "const_cast"))
(call_expression
function: (qualified_identifier
@ -39,6 +34,8 @@
(template_method
name: (field_identifier) @function)
; Support up to 3 levels of nesting of qualifiers
; i.e. a::b::c::func();
(function_declarator
declarator: (qualified_identifier
name: (identifier) @function))
@ -48,6 +45,12 @@
name: (qualified_identifier
name: (identifier) @function)))
(function_declarator
declarator: (qualified_identifier
name: (qualified_identifier
name: (qualified_identifier
name: (identifier) @function))))
(function_declarator
declarator: (field_identifier) @function)
@ -72,6 +75,13 @@
"()"
] @operator
; These casts are parsed as function calls, but are not.
((identifier) @keyword (#eq? @keyword "static_cast"))
((identifier) @keyword (#eq? @keyword "dynamic_cast"))
((identifier) @keyword (#eq? @keyword "reinterpret_cast"))
((identifier) @keyword (#eq? @keyword "const_cast"))
[
"co_await"
"co_return"

View File

@ -235,6 +235,8 @@
(template_string)
] @string
(escape_sequence) @constant.character.escape
(regex) @string.regexp
(number) @constant.numeric.integer

View File

@ -131,8 +131,8 @@
"try"
"except"
"finally"
] @keyword.control.except
(raise_statement "from" @keyword.control.except)
] @keyword.control.exception
(raise_statement "from" @keyword.control.exception)
; Functions
[