Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/snippets/active.rs')
-rw-r--r--helix-core/src/snippets/active.rs255
1 files changed, 255 insertions, 0 deletions
diff --git a/helix-core/src/snippets/active.rs b/helix-core/src/snippets/active.rs
new file mode 100644
index 00000000..65ac1b7b
--- /dev/null
+++ b/helix-core/src/snippets/active.rs
@@ -0,0 +1,255 @@
+use std::ops::{Index, IndexMut};
+
+use hashbrown::HashSet;
+use helix_stdx::range::{is_exact_subset, is_subset};
+use helix_stdx::Range;
+use ropey::Rope;
+
+use crate::movement::Direction;
+use crate::snippets::render::{RenderedSnippet, Tabstop};
+use crate::snippets::TabstopIdx;
+use crate::{Assoc, ChangeSet, Selection, Transaction};
+
+pub struct ActiveSnippet {
+ ranges: Vec<Range>,
+ active_tabstops: HashSet<TabstopIdx>,
+ active_tabstop: TabstopIdx,
+ tabstops: Vec<Tabstop>,
+}
+
+impl Index<TabstopIdx> for ActiveSnippet {
+ type Output = Tabstop;
+ fn index(&self, index: TabstopIdx) -> &Tabstop {
+ &self.tabstops[index.0]
+ }
+}
+
+impl IndexMut<TabstopIdx> for ActiveSnippet {
+ fn index_mut(&mut self, index: TabstopIdx) -> &mut Tabstop {
+ &mut self.tabstops[index.0]
+ }
+}
+
+impl ActiveSnippet {
+ pub fn new(snippet: RenderedSnippet) -> Option<Self> {
+ let snippet = Self {
+ ranges: snippet.ranges,
+ tabstops: snippet.tabstops,
+ active_tabstops: HashSet::new(),
+ active_tabstop: TabstopIdx(0),
+ };
+ (snippet.tabstops.len() != 1).then_some(snippet)
+ }
+
+ pub fn is_valid(&self, new_selection: &Selection) -> bool {
+ is_subset::<false>(self.ranges.iter().copied(), new_selection.range_bounds())
+ }
+
+ pub fn tabstops(&self) -> impl Iterator<Item = &Tabstop> {
+ self.tabstops.iter()
+ }
+
+ pub fn delete_placeholder(&self, doc: &Rope) -> Transaction {
+ Transaction::delete(
+ doc,
+ self[self.active_tabstop]
+ .ranges
+ .iter()
+ .map(|range| (range.start, range.end)),
+ )
+ }
+
+ /// maps the active snippets trough a `ChangeSet` updating all tabstop ranges
+ pub fn map(&mut self, changes: &ChangeSet) -> bool {
+ let positions_to_map = self.ranges.iter_mut().flat_map(|range| {
+ [
+ (&mut range.start, Assoc::After),
+ (&mut range.end, Assoc::Before),
+ ]
+ });
+ changes.update_positions(positions_to_map);
+
+ for (i, tabstop) in self.tabstops.iter_mut().enumerate() {
+ if self.active_tabstops.contains(&TabstopIdx(i)) {
+ let positions_to_map = tabstop.ranges.iter_mut().flat_map(|range| {
+ let end_assoc = if range.start == range.end {
+ Assoc::Before
+ } else {
+ Assoc::After
+ };
+ [
+ (&mut range.start, Assoc::Before),
+ (&mut range.end, end_assoc),
+ ]
+ });
+ changes.update_positions(positions_to_map);
+ } else {
+ let positions_to_map = tabstop.ranges.iter_mut().flat_map(|range| {
+ let end_assoc = if range.start == range.end {
+ Assoc::After
+ } else {
+ Assoc::Before
+ };
+ [
+ (&mut range.start, Assoc::After),
+ (&mut range.end, end_assoc),
+ ]
+ });
+ changes.update_positions(positions_to_map);
+ }
+ let mut snippet_ranges = self.ranges.iter();
+ let mut snippet_range = snippet_ranges.next().unwrap();
+ let mut tabstop_i = 0;
+ let mut prev = Range { start: 0, end: 0 };
+ let num_ranges = tabstop.ranges.len() / self.ranges.len();
+ tabstop.ranges.retain_mut(|range| {
+ if tabstop_i == num_ranges {
+ snippet_range = snippet_ranges.next().unwrap();
+ tabstop_i = 0;
+ }
+ tabstop_i += 1;
+ let retain = snippet_range.start <= snippet_range.end;
+ if retain {
+ range.start = range.start.max(snippet_range.start);
+ range.end = range.end.max(range.start).min(snippet_range.end);
+ // garunteed by assoc
+ debug_assert!(prev.start <= range.start);
+ debug_assert!(range.start <= range.end);
+ if prev.end > range.start {
+ // not really sure what to do in this case. It shouldn't
+ // really occur in practice% the below just ensures
+ // our invriants hold
+ range.start = prev.end;
+ range.end = range.end.max(range.start)
+ }
+ prev = *range;
+ }
+ retain
+ });
+ }
+ self.ranges.iter().all(|range| range.end <= range.start)
+ }
+
+ pub fn next_tabstop(&mut self, current_selection: &Selection) -> (Selection, bool) {
+ let primary_idx = self.primary_idx(current_selection);
+ while self.active_tabstop.0 + 1 < self.tabstops.len() {
+ self.active_tabstop.0 += 1;
+ if self.activate_tabstop() {
+ let selection = self.tabstop_selection(primary_idx, Direction::Forward);
+ return (selection, self.active_tabstop.0 + 1 == self.tabstops.len());
+ }
+ }
+
+ (
+ self.tabstop_selection(primary_idx, Direction::Forward),
+ true,
+ )
+ }
+
+ pub fn prev_tabstop(&mut self, current_selection: &Selection) -> Option<Selection> {
+ let primary_idx = self.primary_idx(current_selection);
+ while self.active_tabstop.0 != 0 {
+ self.active_tabstop.0 -= 1;
+ if self.activate_tabstop() {
+ return Some(self.tabstop_selection(primary_idx, Direction::Forward));
+ }
+ }
+ None
+ }
+ // computes the primary idx adjust for the number of cursors in the current tabstop
+ fn primary_idx(&self, current_selection: &Selection) -> usize {
+ let primary: Range = current_selection.primary().into();
+ let res = self
+ .ranges
+ .iter()
+ .position(|&range| range.contains(primary));
+ res.unwrap_or_else(|| {
+ unreachable!(
+ "active snippet must be valid {current_selection:?} {:?}",
+ self.ranges
+ )
+ })
+ }
+
+ fn activate_tabstop(&mut self) -> bool {
+ let tabstop = &self[self.active_tabstop];
+ if tabstop.has_placeholder() && tabstop.ranges.iter().all(|range| range.is_empty()) {
+ return false;
+ }
+ self.active_tabstops.clear();
+ self.active_tabstops.insert(self.active_tabstop);
+ let mut parent = self[self.active_tabstop].parent;
+ while let Some(tabstop) = parent {
+ self.active_tabstops.insert(tabstop);
+ parent = self[tabstop].parent;
+ }
+ true
+ // TODO: if the user removes the seleciton(s) in one snippet (but
+ // there are still other cursors in other snippets) and jumps to the
+ // next tabstop the selection in that tabstop is restored (at the
+ // next tabstop). This could be annoying since its not possible to
+ // remove a snippet cursor until the snippet is complete. On the other
+ // hand it may be useful since the user may just have meant to edit
+ // a subselection (like with s) of the tabstops and so the selection
+ // removal was just temporary. Potentially this could have some sort of
+ // seperate keymap
+ }
+
+ pub fn tabstop_selection(&self, primary_idx: usize, direction: Direction) -> Selection {
+ let tabstop = &self[self.active_tabstop];
+ tabstop.selection(direction, primary_idx, self.ranges.len())
+ }
+
+ pub fn insert_subsnippet(mut self, snippet: RenderedSnippet) -> Option<Self> {
+ if snippet.ranges.len() % self.ranges.len() != 0
+ || !is_exact_subset(self.ranges.iter().copied(), snippet.ranges.iter().copied())
+ {
+ log::warn!("number of subsnippets did not match, discarding outer snippet");
+ return ActiveSnippet::new(snippet);
+ }
+ let mut cnt = 0;
+ let parent = self[self.active_tabstop].parent;
+ let tabstops = snippet.tabstops.into_iter().map(|mut tabstop| {
+ cnt += 1;
+ if let Some(parent) = &mut tabstop.parent {
+ parent.0 += self.active_tabstop.0;
+ } else {
+ tabstop.parent = parent;
+ }
+ tabstop
+ });
+ self.tabstops
+ .splice(self.active_tabstop.0..=self.active_tabstop.0, tabstops);
+ self.activate_tabstop();
+ Some(self)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::iter::{self};
+
+ use ropey::Rope;
+
+ use crate::snippets::{ActiveSnippet, Snippet, SnippetRenderCtx};
+ use crate::{Selection, Transaction};
+
+ #[test]
+ fn fully_remove() {
+ let snippet = Snippet::parse("foo(${1:bar})$0").unwrap();
+ let mut doc = Rope::from("bar.\n");
+ let (transaction, _, snippet) = snippet.render(
+ &doc,
+ &Selection::point(4),
+ |_| (4, 4),
+ &mut SnippetRenderCtx::test_ctx(),
+ );
+ assert!(transaction.apply(&mut doc));
+ assert_eq!(doc, "bar.foo(bar)\n");
+ let mut snippet = ActiveSnippet::new(snippet).unwrap();
+ let edit = Transaction::change(&doc, iter::once((4, 12, None)));
+ assert!(edit.apply(&mut doc));
+ snippet.map(edit.changes());
+ assert!(!snippet.is_valid(&Selection::point(4)))
+ }
+}