mirror of https://github.com/helix-editor/helix
Compare commits
13 Commits
c9a1c0c656
...
c7ad7f4162
Author | SHA1 | Date |
---|---|---|
|
c7ad7f4162 | |
|
205e7ece70 | |
|
1315b7e2b1 | |
|
52192ae29e | |
|
fba1a6188a | |
|
b90d8960a8 | |
|
6e4ec96101 | |
|
62f270e5d2 | |
|
70650ff55a | |
|
57d7b4fb4a | |
|
4b3aad7830 | |
|
8fbb9566fb | |
|
f6e1a2f599 |
|
@ -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"
|
||||
|
|
|
@ -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` |
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
| adl | ✓ | ✓ | ✓ | |
|
||||
| agda | ✓ | | | |
|
||||
| alloy | ✓ | | | |
|
||||
| amber | ✓ | | | |
|
||||
| amber | ✓ | | | `amber-lsp` |
|
||||
| astro | ✓ | | | `astro-ls` |
|
||||
| awk | ✓ | ✓ | | `awk-language-server` |
|
||||
| bash | ✓ | ✓ | ✓ | `bash-language-server` |
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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()))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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)#"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 || {
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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)]
|
||||
|
|
|
@ -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};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -235,6 +235,8 @@
|
|||
(template_string)
|
||||
] @string
|
||||
|
||||
(escape_sequence) @constant.character.escape
|
||||
|
||||
(regex) @string.regexp
|
||||
(number) @constant.numeric.integer
|
||||
|
||||
|
|
|
@ -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
|
||||
[
|
||||
|
|
Loading…
Reference in New Issue