Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'helix-core/src/snippets/render.rs')
-rw-r--r--helix-core/src/snippets/render.rs354
1 files changed, 354 insertions, 0 deletions
diff --git a/helix-core/src/snippets/render.rs b/helix-core/src/snippets/render.rs
new file mode 100644
index 00000000..765c5c76
--- /dev/null
+++ b/helix-core/src/snippets/render.rs
@@ -0,0 +1,354 @@
+use std::borrow::Cow;
+use std::ops::{Index, IndexMut};
+use std::sync::Arc;
+
+use helix_stdx::Range;
+use ropey::{Rope, RopeSlice};
+use smallvec::SmallVec;
+
+use crate::indent::{normalize_indentation, IndentStyle};
+use crate::movement::Direction;
+use crate::snippets::elaborate;
+use crate::snippets::TabstopIdx;
+use crate::snippets::{Snippet, SnippetElement, Transform};
+use crate::{selection, Selection, Tendril, Transaction};
+
+#[derive(Debug, Clone, PartialEq)]
+pub enum TabstopKind {
+ Choice { choices: Arc<[Tendril]> },
+ Placeholder,
+ Empty,
+ Transform(Arc<Transform>),
+}
+
+#[derive(Debug, PartialEq)]
+pub struct Tabstop {
+ pub ranges: SmallVec<[Range; 1]>,
+ pub parent: Option<TabstopIdx>,
+ pub kind: TabstopKind,
+}
+
+impl Tabstop {
+ pub fn has_placeholder(&self) -> bool {
+ matches!(
+ self.kind,
+ TabstopKind::Choice { .. } | TabstopKind::Placeholder
+ )
+ }
+
+ pub fn selection(
+ &self,
+ direction: Direction,
+ primary_idx: usize,
+ snippet_ranges: usize,
+ ) -> Selection {
+ Selection::new(
+ self.ranges
+ .iter()
+ .map(|&range| {
+ let mut range = selection::Range::new(range.start, range.end);
+ if direction == Direction::Backward {
+ range = range.flip()
+ }
+ range
+ })
+ .collect(),
+ primary_idx * (self.ranges.len() / snippet_ranges),
+ )
+ }
+}
+
+#[derive(Debug, Default, PartialEq)]
+pub struct RenderedSnippet {
+ pub tabstops: Vec<Tabstop>,
+ pub ranges: Vec<Range>,
+}
+
+impl RenderedSnippet {
+ pub fn first_selection(&self, direction: Direction, primary_idx: usize) -> Selection {
+ self.tabstops[0].selection(direction, primary_idx, self.ranges.len())
+ }
+}
+
+impl Index<TabstopIdx> for RenderedSnippet {
+ type Output = Tabstop;
+ fn index(&self, index: TabstopIdx) -> &Tabstop {
+ &self.tabstops[index.0]
+ }
+}
+
+impl IndexMut<TabstopIdx> for RenderedSnippet {
+ fn index_mut(&mut self, index: TabstopIdx) -> &mut Tabstop {
+ &mut self.tabstops[index.0]
+ }
+}
+
+impl Snippet {
+ pub fn prepare_render(&self) -> RenderedSnippet {
+ let tabstops =
+ self.tabstops()
+ .map(|tabstop| Tabstop {
+ ranges: SmallVec::new(),
+ parent: tabstop.parent,
+ kind: match &tabstop.kind {
+ elaborate::TabstopKind::Choice { choices } => TabstopKind::Choice {
+ choices: choices.clone(),
+ },
+ // start out as empty the first non-empty placeholder will change this to a aplaceholder automatically
+ elaborate::TabstopKind::Empty
+ | elaborate::TabstopKind::Placeholder { .. } => TabstopKind::Empty,
+ elaborate::TabstopKind::Transform(transform) => {
+ TabstopKind::Transform(transform.clone())
+ }
+ },
+ })
+ .collect();
+ RenderedSnippet {
+ tabstops,
+ ranges: Vec::new(),
+ }
+ }
+
+ pub fn render_at(
+ &self,
+ snippet: &mut RenderedSnippet,
+ indent: RopeSlice<'_>,
+ at_newline: bool,
+ ctx: &mut SnippetRenderCtx,
+ pos: usize,
+ ) -> (Tendril, usize) {
+ let mut ctx = SnippetRender {
+ dst: snippet,
+ src: self,
+ indent,
+ text: Tendril::new(),
+ off: pos,
+ ctx,
+ at_newline,
+ };
+ ctx.render_elements(self.elements());
+ let end = ctx.off;
+ let text = ctx.text;
+ snippet.ranges.push(Range { start: pos, end });
+ (text, end - pos)
+ }
+
+ pub fn render(
+ &self,
+ doc: &Rope,
+ selection: &Selection,
+ change_range: impl FnMut(&selection::Range) -> (usize, usize),
+ ctx: &mut SnippetRenderCtx,
+ ) -> (Transaction, Selection, RenderedSnippet) {
+ let mut snippet = self.prepare_render();
+ let mut off = 0;
+ let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping(
+ doc,
+ selection,
+ change_range,
+ |replacement_start, replacement_end| {
+ let line_idx = doc.char_to_line(replacement_start);
+ let line_start = doc.line_to_char(line_idx);
+ let prefix = doc.slice(line_start..replacement_start);
+ let indent_len = prefix.chars().take_while(|c| c.is_whitespace()).count();
+ let indent = prefix.slice(..indent_len);
+ let at_newline = indent_len == replacement_start - line_start;
+
+ let (replacement, replacement_len) = self.render_at(
+ &mut snippet,
+ indent,
+ at_newline,
+ ctx,
+ (replacement_start as i128 + off) as usize,
+ );
+ off +=
+ replacement_start as i128 - replacement_end as i128 + replacement_len as i128;
+
+ Some(replacement)
+ },
+ );
+ (transaction, selection, snippet)
+ }
+}
+
+pub type VariableResolver = dyn FnMut(&str) -> Option<Cow<str>>;
+pub struct SnippetRenderCtx {
+ pub resolve_var: Box<VariableResolver>,
+ pub tab_width: usize,
+ pub indent_style: IndentStyle,
+ pub line_ending: &'static str,
+}
+
+impl SnippetRenderCtx {
+ #[cfg(test)]
+ pub(super) fn test_ctx() -> SnippetRenderCtx {
+ SnippetRenderCtx {
+ resolve_var: Box::new(|_| None),
+ tab_width: 4,
+ indent_style: IndentStyle::Spaces(4),
+ line_ending: "\n",
+ }
+ }
+}
+
+struct SnippetRender<'a> {
+ ctx: &'a mut SnippetRenderCtx,
+ dst: &'a mut RenderedSnippet,
+ src: &'a Snippet,
+ indent: RopeSlice<'a>,
+ text: Tendril,
+ off: usize,
+ at_newline: bool,
+}
+
+impl SnippetRender<'_> {
+ fn render_elements(&mut self, elements: &[SnippetElement]) {
+ for element in elements {
+ self.render_element(element)
+ }
+ }
+
+ fn render_element(&mut self, element: &SnippetElement) {
+ match *element {
+ SnippetElement::Tabstop { idx } => self.render_tabstop(idx),
+ SnippetElement::Variable {
+ ref name,
+ ref default,
+ ref transform,
+ } => {
+ // TODO: allow resolve_var access to the doc and make it return rope slice
+ // so we can access selections and other document content without allocating
+ if let Some(val) = (self.ctx.resolve_var)(name) {
+ if let Some(transform) = transform {
+ self.push_multiline_str(&transform.apply(
+ (&*val).into(),
+ Range {
+ start: 0,
+ end: val.chars().count(),
+ },
+ ));
+ } else {
+ self.push_multiline_str(&val)
+ }
+ } else if let Some(default) = default {
+ self.render_elements(default)
+ }
+ }
+ SnippetElement::Text(ref text) => self.push_multiline_str(text),
+ }
+ }
+
+ fn push_multiline_str(&mut self, text: &str) {
+ let mut lines = text
+ .split('\n')
+ .map(|line| line.strip_suffix('\r').unwrap_or(line));
+ let first_line = lines.next().unwrap();
+ self.push_str(first_line, self.at_newline);
+ for line in lines {
+ self.push_newline();
+ self.push_str(line, true);
+ }
+ }
+
+ fn push_str(&mut self, mut text: &str, at_newline: bool) {
+ if at_newline {
+ let old_len = self.text.len();
+ let old_indent_len = normalize_indentation(
+ self.indent,
+ text.into(),
+ &mut self.text,
+ self.ctx.indent_style,
+ self.ctx.tab_width,
+ );
+ // this is ok because indentation can only be ascii chars (' ' and '\t')
+ self.off += self.text.len() - old_len;
+ text = &text[old_indent_len..];
+ if text.is_empty() {
+ self.at_newline = true;
+ return;
+ }
+ }
+ self.text.push_str(text);
+ self.off += text.chars().count();
+ }
+
+ fn push_newline(&mut self) {
+ self.off += self.ctx.line_ending.chars().count() + self.indent.len_chars();
+ self.text.push_str(self.ctx.line_ending);
+ self.text.extend(self.indent.chunks());
+ }
+
+ fn render_tabstop(&mut self, tabstop: TabstopIdx) {
+ let start = self.off;
+ let end = match &self.src[tabstop].kind {
+ elaborate::TabstopKind::Placeholder { default } if !default.is_empty() => {
+ self.render_elements(default);
+ self.dst[tabstop].kind = TabstopKind::Placeholder;
+ self.off
+ }
+ _ => start,
+ };
+ self.dst[tabstop].ranges.push(Range { start, end });
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use helix_stdx::Range;
+
+ use crate::snippets::render::Tabstop;
+ use crate::snippets::{Snippet, SnippetRenderCtx};
+
+ use super::TabstopKind;
+
+ fn assert_snippet(snippet: &str, expect: &str, tabstops: &[Tabstop]) {
+ let snippet = Snippet::parse(snippet).unwrap();
+ let mut rendered_snippet = snippet.prepare_render();
+ let rendered_text = snippet
+ .render_at(
+ &mut rendered_snippet,
+ "\t".into(),
+ false,
+ &mut SnippetRenderCtx::test_ctx(),
+ 0,
+ )
+ .0;
+ assert_eq!(rendered_text, expect);
+ assert_eq!(&rendered_snippet.tabstops, tabstops);
+ assert_eq!(
+ rendered_snippet.ranges.last().unwrap().end,
+ rendered_text.chars().count()
+ );
+ assert_eq!(rendered_snippet.ranges.last().unwrap().start, 0)
+ }
+
+ #[test]
+ fn rust_macro() {
+ assert_snippet(
+ "macro_rules! ${1:name} {\n\t($3) => {\n\t\t$2\n\t};\n}",
+ "macro_rules! name {\n\t () => {\n\t \n\t };\n\t}",
+ &[
+ Tabstop {
+ ranges: vec![Range { start: 13, end: 17 }].into(),
+ parent: None,
+ kind: TabstopKind::Placeholder,
+ },
+ Tabstop {
+ ranges: vec![Range { start: 42, end: 42 }].into(),
+ parent: None,
+ kind: TabstopKind::Empty,
+ },
+ Tabstop {
+ ranges: vec![Range { start: 26, end: 26 }].into(),
+ parent: None,
+ kind: TabstopKind::Empty,
+ },
+ Tabstop {
+ ranges: vec![Range { start: 53, end: 53 }].into(),
+ parent: None,
+ kind: TabstopKind::Empty,
+ },
+ ],
+ );
+ }
+}