Unnamed repository; edit this file 'description' to name the repository.
Diffstat (limited to 'crates/ide-db/src/source_change.rs')
| -rw-r--r-- | crates/ide-db/src/source_change.rs | 204 |
1 files changed, 132 insertions, 72 deletions
diff --git a/crates/ide-db/src/source_change.rs b/crates/ide-db/src/source_change.rs index fad0ca51a0..39763479c6 100644 --- a/crates/ide-db/src/source_change.rs +++ b/crates/ide-db/src/source_change.rs @@ -7,17 +7,17 @@ use std::{collections::hash_map::Entry, iter, mem}; use crate::SnippetCap; use base_db::{AnchoredPathBuf, FileId}; +use itertools::Itertools; use nohash_hasher::IntMap; use stdx::never; use syntax::{ - algo, ast, ted, AstNode, SyntaxElement, SyntaxNode, SyntaxNodePtr, SyntaxToken, TextRange, - TextSize, + algo, AstNode, SyntaxElement, SyntaxNode, SyntaxNodePtr, SyntaxToken, TextRange, TextSize, }; use text_edit::{TextEdit, TextEditBuilder}; #[derive(Default, Debug, Clone)] pub struct SourceChange { - pub source_file_edits: IntMap<FileId, TextEdit>, + pub source_file_edits: IntMap<FileId, (TextEdit, Option<SnippetEdit>)>, pub file_system_edits: Vec<FileSystemEdit>, pub is_snippet: bool, } @@ -26,7 +26,7 @@ impl SourceChange { /// Creates a new SourceChange with the given label /// from the edits. pub fn from_edits( - source_file_edits: IntMap<FileId, TextEdit>, + source_file_edits: IntMap<FileId, (TextEdit, Option<SnippetEdit>)>, file_system_edits: Vec<FileSystemEdit>, ) -> Self { SourceChange { source_file_edits, file_system_edits, is_snippet: false } @@ -34,7 +34,7 @@ impl SourceChange { pub fn from_text_edit(file_id: FileId, edit: TextEdit) -> Self { SourceChange { - source_file_edits: iter::once((file_id, edit)).collect(), + source_file_edits: iter::once((file_id, (edit, None))).collect(), ..Default::default() } } @@ -42,12 +42,31 @@ impl SourceChange { /// Inserts a [`TextEdit`] for the given [`FileId`]. This properly handles merging existing /// edits for a file if some already exist. pub fn insert_source_edit(&mut self, file_id: FileId, edit: TextEdit) { + self.insert_source_and_snippet_edit(file_id, edit, None) + } + + /// Inserts a [`TextEdit`] and potentially a [`SnippetEdit`] for the given [`FileId`]. + /// This properly handles merging existing edits for a file if some already exist. + pub fn insert_source_and_snippet_edit( + &mut self, + file_id: FileId, + edit: TextEdit, + snippet_edit: Option<SnippetEdit>, + ) { match self.source_file_edits.entry(file_id) { Entry::Occupied(mut entry) => { - never!(entry.get_mut().union(edit).is_err(), "overlapping edits for same file"); + let value = entry.get_mut(); + never!(value.0.union(edit).is_err(), "overlapping edits for same file"); + never!( + value.1.is_some() && snippet_edit.is_some(), + "overlapping snippet edits for same file" + ); + if value.1.is_none() { + value.1 = snippet_edit; + } } Entry::Vacant(entry) => { - entry.insert(edit); + entry.insert((edit, snippet_edit)); } } } @@ -56,7 +75,10 @@ impl SourceChange { self.file_system_edits.push(edit); } - pub fn get_source_edit(&self, file_id: FileId) -> Option<&TextEdit> { + pub fn get_source_and_snippet_edit( + &self, + file_id: FileId, + ) -> Option<&(TextEdit, Option<SnippetEdit>)> { self.source_file_edits.get(&file_id) } @@ -70,7 +92,18 @@ impl SourceChange { impl Extend<(FileId, TextEdit)> for SourceChange { fn extend<T: IntoIterator<Item = (FileId, TextEdit)>>(&mut self, iter: T) { - iter.into_iter().for_each(|(file_id, edit)| self.insert_source_edit(file_id, edit)); + self.extend(iter.into_iter().map(|(file_id, edit)| (file_id, (edit, None)))) + } +} + +impl Extend<(FileId, (TextEdit, Option<SnippetEdit>))> for SourceChange { + fn extend<T: IntoIterator<Item = (FileId, (TextEdit, Option<SnippetEdit>))>>( + &mut self, + iter: T, + ) { + iter.into_iter().for_each(|(file_id, (edit, snippet_edit))| { + self.insert_source_and_snippet_edit(file_id, edit, snippet_edit) + }); } } @@ -82,6 +115,8 @@ impl Extend<FileSystemEdit> for SourceChange { impl From<IntMap<FileId, TextEdit>> for SourceChange { fn from(source_file_edits: IntMap<FileId, TextEdit>) -> SourceChange { + let source_file_edits = + source_file_edits.into_iter().map(|(file_id, edit)| (file_id, (edit, None))).collect(); SourceChange { source_file_edits, file_system_edits: Vec::new(), is_snippet: false } } } @@ -94,6 +129,65 @@ impl FromIterator<(FileId, TextEdit)> for SourceChange { } } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SnippetEdit(Vec<(u32, TextRange)>); + +impl SnippetEdit { + pub fn new(snippets: Vec<Snippet>) -> Self { + let mut snippet_ranges = snippets + .into_iter() + .zip(1..) + .with_position() + .map(|pos| { + let (snippet, index) = match pos { + itertools::Position::First(it) | itertools::Position::Middle(it) => it, + // last/only snippet gets index 0 + itertools::Position::Last((snippet, _)) + | itertools::Position::Only((snippet, _)) => (snippet, 0), + }; + + let range = match snippet { + Snippet::Tabstop(pos) => TextRange::empty(pos), + Snippet::Placeholder(range) => range, + }; + (index, range) + }) + .collect_vec(); + + snippet_ranges.sort_by_key(|(_, range)| range.start()); + + // Ensure that none of the ranges overlap + let disjoint_ranges = snippet_ranges + .iter() + .zip(snippet_ranges.iter().skip(1)) + .all(|((_, left), (_, right))| left.end() <= right.start() || left == right); + stdx::always!(disjoint_ranges); + + SnippetEdit(snippet_ranges) + } + + /// Inserts all of the snippets into the given text. + pub fn apply(&self, text: &mut String) { + // Start from the back so that we don't have to adjust ranges + for (index, range) in self.0.iter().rev() { + if range.is_empty() { + // is a tabstop + text.insert_str(range.start().into(), &format!("${index}")); + } else { + // is a placeholder + text.insert(range.end().into(), '}'); + text.insert_str(range.start().into(), &format!("${{{index}:")); + } + } + } + + /// Gets the underlying snippet index + text range + /// Tabstops are represented by an empty range, and placeholders use the range that they were given + pub fn into_edit_ranges(self) -> Vec<(u32, TextRange)> { + self.0 + } +} + pub struct SourceChangeBuilder { pub edit: TextEditBuilder, pub file_id: FileId, @@ -152,24 +246,19 @@ impl SourceChangeBuilder { } fn commit(&mut self) { - // Render snippets first so that they get bundled into the tree diff - if let Some(mut snippets) = self.snippet_builder.take() { - // Last snippet always has stop index 0 - let last_stop = snippets.places.pop().unwrap(); - last_stop.place(0); - - for (index, stop) in snippets.places.into_iter().enumerate() { - stop.place(index + 1) - } - } + let snippet_edit = self.snippet_builder.take().map(|builder| { + SnippetEdit::new( + builder.places.into_iter().map(PlaceSnippet::finalize_position).collect_vec(), + ) + }); if let Some(tm) = self.mutated_tree.take() { - algo::diff(&tm.immutable, &tm.mutable_clone).into_text_edit(&mut self.edit) + algo::diff(&tm.immutable, &tm.mutable_clone).into_text_edit(&mut self.edit); } let edit = mem::take(&mut self.edit).finish(); - if !edit.is_empty() { - self.source_change.insert_source_edit(self.file_id, edit); + if !edit.is_empty() || snippet_edit.is_some() { + self.source_change.insert_source_and_snippet_edit(self.file_id, edit, snippet_edit); } } @@ -275,6 +364,16 @@ impl SourceChangeBuilder { pub fn finish(mut self) -> SourceChange { self.commit(); + + // Only one file can have snippet edits + stdx::never!(self + .source_change + .source_file_edits + .iter() + .filter(|(_, (_, snippet_edit))| snippet_edit.is_some()) + .at_most_one() + .is_err()); + mem::take(&mut self.source_change) } } @@ -296,6 +395,13 @@ impl From<FileSystemEdit> for SourceChange { } } +pub enum Snippet { + /// A tabstop snippet (e.g. `$0`). + Tabstop(TextSize), + /// A placeholder snippet (e.g. `${0:placeholder}`). + Placeholder(TextRange), +} + enum PlaceSnippet { /// Place a tabstop before an element Before(SyntaxElement), @@ -306,57 +412,11 @@ enum PlaceSnippet { } impl PlaceSnippet { - /// Places the snippet before or over an element with the given tab stop index - fn place(self, order: usize) { - // ensure the target element is still attached - match &self { - PlaceSnippet::Before(element) - | PlaceSnippet::After(element) - | PlaceSnippet::Over(element) => { - // element should still be in the tree, but if it isn't - // then it's okay to just ignore this place - if stdx::never!(element.parent().is_none()) { - return; - } - } - } - + fn finalize_position(self) -> Snippet { match self { - PlaceSnippet::Before(element) => { - ted::insert_raw(ted::Position::before(&element), Self::make_tab_stop(order)); - } - PlaceSnippet::After(element) => { - ted::insert_raw(ted::Position::after(&element), Self::make_tab_stop(order)); - } - PlaceSnippet::Over(element) => { - let position = ted::Position::before(&element); - element.detach(); - - let snippet = ast::SourceFile::parse(&format!("${{{order}:_}}")) - .syntax_node() - .clone_for_update(); - - let placeholder = - snippet.descendants().find_map(ast::UnderscoreExpr::cast).unwrap(); - ted::replace(placeholder.syntax(), element); - - ted::insert_raw(position, snippet); - } + PlaceSnippet::Before(it) => Snippet::Tabstop(it.text_range().start()), + PlaceSnippet::After(it) => Snippet::Tabstop(it.text_range().end()), + PlaceSnippet::Over(it) => Snippet::Placeholder(it.text_range()), } } - - fn make_tab_stop(order: usize) -> SyntaxNode { - let stop = ast::SourceFile::parse(&format!("stop!(${order})")) - .syntax_node() - .descendants() - .find_map(ast::TokenTree::cast) - .unwrap() - .syntax() - .clone_for_update(); - - stop.first_token().unwrap().detach(); - stop.last_token().unwrap().detach(); - - stop - } } |