Auto merge of #15269 - DropDemBits:structured-snippets-deferred-rendering, r=Veykril
internal: Defer structured snippet rendering to allow escaping snippet bits Since we know exactly where snippets are, we can transparently escape snippet bits to the exact text edits that need it, and not have to do it for anything other text edits. Also will eventually fix #11006 once all assists are migrated. This comes as a side-effect of text edits that don't have snippets get marked as having no insert formatting at all.
This commit is contained in:
commit
c71e1368fd
8 changed files with 956 additions and 233 deletions
|
@ -132,8 +132,13 @@ fn check_doc_test(assist_id: &str, before: &str, after: &str) {
|
|||
.filter(|it| !it.source_file_edits.is_empty() || !it.file_system_edits.is_empty())
|
||||
.expect("Assist did not contain any source changes");
|
||||
let mut actual = before;
|
||||
if let Some(source_file_edit) = source_change.get_source_edit(file_id) {
|
||||
if let Some((source_file_edit, snippet_edit)) =
|
||||
source_change.get_source_and_snippet_edit(file_id)
|
||||
{
|
||||
source_file_edit.apply(&mut actual);
|
||||
if let Some(snippet_edit) = snippet_edit {
|
||||
snippet_edit.apply(&mut actual);
|
||||
}
|
||||
}
|
||||
actual
|
||||
};
|
||||
|
@ -191,9 +196,12 @@ fn check_with_config(
|
|||
&& source_change.file_system_edits.len() == 0;
|
||||
|
||||
let mut buf = String::new();
|
||||
for (file_id, edit) in source_change.source_file_edits {
|
||||
for (file_id, (edit, snippet_edit)) in source_change.source_file_edits {
|
||||
let mut text = db.file_text(file_id).as_ref().to_owned();
|
||||
edit.apply(&mut text);
|
||||
if let Some(snippet_edit) = snippet_edit {
|
||||
snippet_edit.apply(&mut text);
|
||||
}
|
||||
if !skip_header {
|
||||
let sr = db.file_source_root(file_id);
|
||||
let sr = db.source_root(sr);
|
||||
|
@ -485,18 +493,21 @@ pub fn test_some_range(a: int) -> bool {
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "let $0var_name = 5;\n ",
|
||||
delete: 45..45,
|
||||
},
|
||||
Indel {
|
||||
insert: "var_name",
|
||||
delete: 59..60,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "let $0var_name = 5;\n ",
|
||||
delete: 45..45,
|
||||
},
|
||||
Indel {
|
||||
insert: "var_name",
|
||||
delete: 59..60,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [],
|
||||
is_snippet: true,
|
||||
|
@ -544,18 +555,21 @@ pub fn test_some_range(a: int) -> bool {
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "let $0var_name = 5;\n ",
|
||||
delete: 45..45,
|
||||
},
|
||||
Indel {
|
||||
insert: "var_name",
|
||||
delete: 59..60,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "let $0var_name = 5;\n ",
|
||||
delete: 45..45,
|
||||
},
|
||||
Indel {
|
||||
insert: "var_name",
|
||||
delete: 59..60,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [],
|
||||
is_snippet: true,
|
||||
|
@ -581,18 +595,21 @@ pub fn test_some_range(a: int) -> bool {
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "fun_name()",
|
||||
delete: 59..60,
|
||||
},
|
||||
Indel {
|
||||
insert: "\n\nfn $0fun_name() -> i32 {\n 5\n}",
|
||||
delete: 110..110,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "fun_name()",
|
||||
delete: 59..60,
|
||||
},
|
||||
Indel {
|
||||
insert: "\n\nfn $0fun_name() -> i32 {\n 5\n}",
|
||||
delete: 110..110,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [],
|
||||
is_snippet: true,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,8 +49,11 @@ fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
|
|||
let file_id = *source_change.source_file_edits.keys().next().unwrap();
|
||||
let mut actual = db.file_text(file_id).to_string();
|
||||
|
||||
for edit in source_change.source_file_edits.values() {
|
||||
for (edit, snippet_edit) in source_change.source_file_edits.values() {
|
||||
edit.apply(&mut actual);
|
||||
if let Some(snippet_edit) = snippet_edit {
|
||||
snippet_edit.apply(&mut actual);
|
||||
}
|
||||
}
|
||||
actual
|
||||
};
|
||||
|
|
|
@ -127,7 +127,7 @@ pub use ide_db::{
|
|||
label::Label,
|
||||
line_index::{LineCol, LineIndex},
|
||||
search::{ReferenceCategory, SearchScope},
|
||||
source_change::{FileSystemEdit, SourceChange},
|
||||
source_change::{FileSystemEdit, SnippetEdit, SourceChange},
|
||||
symbol_index::Query,
|
||||
RootDatabase, SymbolKind,
|
||||
};
|
||||
|
|
|
@ -367,7 +367,7 @@ mod tests {
|
|||
let mut file_id: Option<FileId> = None;
|
||||
for edit in source_change.source_file_edits {
|
||||
file_id = Some(edit.0);
|
||||
for indel in edit.1.into_iter() {
|
||||
for indel in edit.1 .0.into_iter() {
|
||||
text_edit_builder.replace(indel.delete, indel.insert);
|
||||
}
|
||||
}
|
||||
|
@ -895,14 +895,17 @@ mod foo$0;
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
1,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo2",
|
||||
delete: 4..7,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo2",
|
||||
delete: 4..7,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [
|
||||
MoveFile {
|
||||
|
@ -944,24 +947,30 @@ use crate::foo$0::FooContent;
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "quux",
|
||||
delete: 8..11,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "quux",
|
||||
delete: 8..11,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
FileId(
|
||||
2,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "quux",
|
||||
delete: 11..14,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "quux",
|
||||
delete: 11..14,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [
|
||||
MoveFile {
|
||||
|
@ -997,14 +1006,17 @@ mod fo$0o;
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo2",
|
||||
delete: 4..7,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo2",
|
||||
delete: 4..7,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [
|
||||
MoveDir {
|
||||
|
@ -1047,14 +1059,17 @@ mod outer { mod fo$0o; }
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "bar",
|
||||
delete: 16..19,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "bar",
|
||||
delete: 16..19,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [
|
||||
MoveFile {
|
||||
|
@ -1120,24 +1135,30 @@ pub mod foo$0;
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo2",
|
||||
delete: 27..30,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo2",
|
||||
delete: 27..30,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
FileId(
|
||||
1,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo2",
|
||||
delete: 8..11,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo2",
|
||||
delete: 8..11,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [
|
||||
MoveFile {
|
||||
|
@ -1187,14 +1208,17 @@ mod quux;
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo2",
|
||||
delete: 4..7,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo2",
|
||||
delete: 4..7,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [
|
||||
MoveFile {
|
||||
|
@ -1325,18 +1349,21 @@ pub fn baz() {}
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "r#fn",
|
||||
delete: 4..7,
|
||||
},
|
||||
Indel {
|
||||
insert: "r#fn",
|
||||
delete: 22..25,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "r#fn",
|
||||
delete: 4..7,
|
||||
},
|
||||
Indel {
|
||||
insert: "r#fn",
|
||||
delete: 22..25,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [
|
||||
MoveFile {
|
||||
|
@ -1395,18 +1422,21 @@ pub fn baz() {}
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo",
|
||||
delete: 4..8,
|
||||
},
|
||||
Indel {
|
||||
insert: "foo",
|
||||
delete: 23..27,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "foo",
|
||||
delete: 4..8,
|
||||
},
|
||||
Indel {
|
||||
insert: "foo",
|
||||
delete: 23..27,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [
|
||||
MoveFile {
|
||||
|
|
|
@ -126,14 +126,17 @@ mod tests {
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "3",
|
||||
delete: 33..34,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "3",
|
||||
delete: 33..34,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [],
|
||||
is_snippet: false,
|
||||
|
@ -163,24 +166,30 @@ mod tests {
|
|||
source_file_edits: {
|
||||
FileId(
|
||||
0,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "3",
|
||||
delete: 33..34,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "3",
|
||||
delete: 33..34,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
FileId(
|
||||
1,
|
||||
): TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "3",
|
||||
delete: 11..12,
|
||||
},
|
||||
],
|
||||
},
|
||||
): (
|
||||
TextEdit {
|
||||
indels: [
|
||||
Indel {
|
||||
insert: "3",
|
||||
delete: 11..12,
|
||||
},
|
||||
],
|
||||
},
|
||||
None,
|
||||
),
|
||||
},
|
||||
file_system_edits: [],
|
||||
is_snippet: false,
|
||||
|
|
|
@ -353,7 +353,8 @@ pub(crate) fn handle_on_type_formatting(
|
|||
};
|
||||
|
||||
// This should be a single-file edit
|
||||
let (_, text_edit) = edit.source_file_edits.into_iter().next().unwrap();
|
||||
let (_, (text_edit, snippet_edit)) = edit.source_file_edits.into_iter().next().unwrap();
|
||||
stdx::never!(snippet_edit.is_none(), "on type formatting shouldn't use structured snippets");
|
||||
|
||||
let change = to_proto::snippet_text_edit_vec(&line_index, edit.is_snippet, text_edit);
|
||||
Ok(Some(change))
|
||||
|
|
|
@ -10,8 +10,8 @@ use ide::{
|
|||
CompletionItemKind, CompletionRelevance, Documentation, FileId, FileRange, FileSystemEdit,
|
||||
Fold, FoldKind, Highlight, HlMod, HlOperator, HlPunct, HlRange, HlTag, Indel, InlayHint,
|
||||
InlayHintLabel, InlayHintLabelPart, InlayKind, Markup, NavigationTarget, ReferenceCategory,
|
||||
RenameError, Runnable, Severity, SignatureHelp, SourceChange, StructureNodeKind, SymbolKind,
|
||||
TextEdit, TextRange, TextSize,
|
||||
RenameError, Runnable, Severity, SignatureHelp, SnippetEdit, SourceChange, StructureNodeKind,
|
||||
SymbolKind, TextEdit, TextRange, TextSize,
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use serde_json::to_value;
|
||||
|
@ -22,7 +22,7 @@ use crate::{
|
|||
config::{CallInfoConfig, Config},
|
||||
global_state::GlobalStateSnapshot,
|
||||
line_index::{LineEndings, LineIndex, PositionEncoding},
|
||||
lsp_ext,
|
||||
lsp_ext::{self, SnippetTextEdit},
|
||||
lsp_utils::invalid_params_error,
|
||||
semantic_tokens::{self, standard_fallback_type},
|
||||
};
|
||||
|
@ -885,16 +885,136 @@ fn outside_workspace_annotation_id() -> String {
|
|||
String::from("OutsideWorkspace")
|
||||
}
|
||||
|
||||
fn merge_text_and_snippet_edits(
|
||||
line_index: &LineIndex,
|
||||
edit: TextEdit,
|
||||
snippet_edit: SnippetEdit,
|
||||
) -> Vec<SnippetTextEdit> {
|
||||
let mut edits: Vec<SnippetTextEdit> = vec![];
|
||||
let mut snippets = snippet_edit.into_edit_ranges().into_iter().peekable();
|
||||
let mut text_edits = edit.into_iter();
|
||||
|
||||
while let Some(current_indel) = text_edits.next() {
|
||||
let new_range = {
|
||||
let insert_len =
|
||||
TextSize::try_from(current_indel.insert.len()).unwrap_or(TextSize::from(u32::MAX));
|
||||
TextRange::at(current_indel.delete.start(), insert_len)
|
||||
};
|
||||
|
||||
// insert any snippets before the text edit
|
||||
for (snippet_index, snippet_range) in
|
||||
snippets.take_while_ref(|(_, range)| range.end() < new_range.start())
|
||||
{
|
||||
let snippet_range = if !stdx::always!(
|
||||
snippet_range.is_empty(),
|
||||
"placeholder range {:?} is before current text edit range {:?}",
|
||||
snippet_range,
|
||||
new_range
|
||||
) {
|
||||
// only possible for tabstops, so make sure it's an empty/insert range
|
||||
TextRange::empty(snippet_range.start())
|
||||
} else {
|
||||
snippet_range
|
||||
};
|
||||
|
||||
let range = range(&line_index, snippet_range);
|
||||
let new_text = format!("${snippet_index}");
|
||||
|
||||
edits.push(SnippetTextEdit {
|
||||
range,
|
||||
new_text,
|
||||
insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET),
|
||||
annotation_id: None,
|
||||
})
|
||||
}
|
||||
|
||||
if snippets.peek().is_some_and(|(_, range)| new_range.intersect(*range).is_some()) {
|
||||
// at least one snippet edit intersects this text edit,
|
||||
// so gather all of the edits that intersect this text edit
|
||||
let mut all_snippets = snippets
|
||||
.take_while_ref(|(_, range)| new_range.intersect(*range).is_some())
|
||||
.collect_vec();
|
||||
|
||||
// ensure all of the ranges are wholly contained inside of the new range
|
||||
all_snippets.retain(|(_, range)| {
|
||||
stdx::always!(
|
||||
new_range.contains_range(*range),
|
||||
"found placeholder range {:?} which wasn't fully inside of text edit's new range {:?}", range, new_range
|
||||
)
|
||||
});
|
||||
|
||||
let mut text_edit = text_edit(line_index, current_indel);
|
||||
|
||||
// escape out snippet text
|
||||
stdx::replace(&mut text_edit.new_text, '\\', r"\\");
|
||||
stdx::replace(&mut text_edit.new_text, '$', r"\$");
|
||||
|
||||
// ...and apply!
|
||||
for (index, range) in all_snippets.iter().rev() {
|
||||
let start = (range.start() - new_range.start()).into();
|
||||
let end = (range.end() - new_range.start()).into();
|
||||
|
||||
if range.is_empty() {
|
||||
text_edit.new_text.insert_str(start, &format!("${index}"));
|
||||
} else {
|
||||
text_edit.new_text.insert(end, '}');
|
||||
text_edit.new_text.insert_str(start, &format!("${{{index}:"));
|
||||
}
|
||||
}
|
||||
|
||||
edits.push(SnippetTextEdit {
|
||||
range: text_edit.range,
|
||||
new_text: text_edit.new_text,
|
||||
insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET),
|
||||
annotation_id: None,
|
||||
})
|
||||
} else {
|
||||
// snippet edit was beyond the current one
|
||||
// since it wasn't consumed, it's available for the next pass
|
||||
edits.push(snippet_text_edit(line_index, false, current_indel));
|
||||
}
|
||||
}
|
||||
|
||||
// insert any remaining tabstops
|
||||
edits.extend(snippets.map(|(snippet_index, snippet_range)| {
|
||||
let snippet_range = if !stdx::always!(
|
||||
snippet_range.is_empty(),
|
||||
"found placeholder snippet {:?} without a text edit",
|
||||
snippet_range
|
||||
) {
|
||||
TextRange::empty(snippet_range.start())
|
||||
} else {
|
||||
snippet_range
|
||||
};
|
||||
|
||||
let range = range(&line_index, snippet_range);
|
||||
let new_text = format!("${snippet_index}");
|
||||
|
||||
SnippetTextEdit {
|
||||
range,
|
||||
new_text,
|
||||
insert_text_format: Some(lsp_types::InsertTextFormat::SNIPPET),
|
||||
annotation_id: None,
|
||||
}
|
||||
}));
|
||||
|
||||
edits
|
||||
}
|
||||
|
||||
pub(crate) fn snippet_text_document_edit(
|
||||
snap: &GlobalStateSnapshot,
|
||||
is_snippet: bool,
|
||||
file_id: FileId,
|
||||
edit: TextEdit,
|
||||
snippet_edit: Option<SnippetEdit>,
|
||||
) -> Cancellable<lsp_ext::SnippetTextDocumentEdit> {
|
||||
let text_document = optional_versioned_text_document_identifier(snap, file_id);
|
||||
let line_index = snap.file_line_index(file_id)?;
|
||||
let mut edits: Vec<_> =
|
||||
edit.into_iter().map(|it| snippet_text_edit(&line_index, is_snippet, it)).collect();
|
||||
let mut edits = if let Some(snippet_edit) = snippet_edit {
|
||||
merge_text_and_snippet_edits(&line_index, edit, snippet_edit)
|
||||
} else {
|
||||
edit.into_iter().map(|it| snippet_text_edit(&line_index, is_snippet, it)).collect()
|
||||
};
|
||||
|
||||
if snap.analysis.is_library_file(file_id)? && snap.config.change_annotation_support() {
|
||||
for edit in &mut edits {
|
||||
|
@ -974,8 +1094,14 @@ pub(crate) fn snippet_workspace_edit(
|
|||
let ops = snippet_text_document_ops(snap, op)?;
|
||||
document_changes.extend_from_slice(&ops);
|
||||
}
|
||||
for (file_id, edit) in source_change.source_file_edits {
|
||||
let edit = snippet_text_document_edit(snap, source_change.is_snippet, file_id, edit)?;
|
||||
for (file_id, (edit, snippet_edit)) in source_change.source_file_edits {
|
||||
let edit = snippet_text_document_edit(
|
||||
snap,
|
||||
source_change.is_snippet,
|
||||
file_id,
|
||||
edit,
|
||||
snippet_edit,
|
||||
)?;
|
||||
document_changes.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit));
|
||||
}
|
||||
let mut workspace_edit = lsp_ext::SnippetWorkspaceEdit {
|
||||
|
@ -1414,7 +1540,9 @@ pub(crate) fn rename_error(err: RenameError) -> crate::LspError {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use expect_test::{expect, Expect};
|
||||
use ide::{Analysis, FilePosition};
|
||||
use ide_db::source_change::Snippet;
|
||||
use test_utils::extract_offset;
|
||||
use triomphe::Arc;
|
||||
|
||||
|
@ -1484,6 +1612,481 @@ fn bar(_: usize) {}
|
|||
assert!(!docs.contains("use crate::bar"));
|
||||
}
|
||||
|
||||
fn check_rendered_snippets(edit: TextEdit, snippets: SnippetEdit, expect: Expect) {
|
||||
let text = r#"/* place to put all ranges in */"#;
|
||||
let line_index = LineIndex {
|
||||
index: Arc::new(ide::LineIndex::new(text)),
|
||||
endings: LineEndings::Unix,
|
||||
encoding: PositionEncoding::Utf8,
|
||||
};
|
||||
|
||||
let res = merge_text_and_snippet_edits(&line_index, edit, snippets);
|
||||
expect.assert_debug_eq(&res);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snippet_rendering_only_tabstops() {
|
||||
let edit = TextEdit::builder().finish();
|
||||
let snippets = SnippetEdit::new(vec![
|
||||
Snippet::Tabstop(0.into()),
|
||||
Snippet::Tabstop(0.into()),
|
||||
Snippet::Tabstop(1.into()),
|
||||
Snippet::Tabstop(1.into()),
|
||||
]);
|
||||
|
||||
check_rendered_snippets(
|
||||
edit,
|
||||
snippets,
|
||||
expect![[r#"
|
||||
[
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
},
|
||||
new_text: "$1",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
},
|
||||
new_text: "$2",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 1,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 1,
|
||||
},
|
||||
},
|
||||
new_text: "$3",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 1,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 1,
|
||||
},
|
||||
},
|
||||
new_text: "$0",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
]
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snippet_rendering_only_text_edits() {
|
||||
let mut edit = TextEdit::builder();
|
||||
edit.insert(0.into(), "abc".to_owned());
|
||||
edit.insert(3.into(), "def".to_owned());
|
||||
let edit = edit.finish();
|
||||
let snippets = SnippetEdit::new(vec![]);
|
||||
|
||||
check_rendered_snippets(
|
||||
edit,
|
||||
snippets,
|
||||
expect![[r#"
|
||||
[
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
},
|
||||
new_text: "abc",
|
||||
insert_text_format: None,
|
||||
annotation_id: None,
|
||||
},
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 3,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 3,
|
||||
},
|
||||
},
|
||||
new_text: "def",
|
||||
insert_text_format: None,
|
||||
annotation_id: None,
|
||||
},
|
||||
]
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snippet_rendering_tabstop_after_text_edit() {
|
||||
let mut edit = TextEdit::builder();
|
||||
edit.insert(0.into(), "abc".to_owned());
|
||||
let edit = edit.finish();
|
||||
let snippets = SnippetEdit::new(vec![Snippet::Tabstop(7.into())]);
|
||||
|
||||
check_rendered_snippets(
|
||||
edit,
|
||||
snippets,
|
||||
expect![[r#"
|
||||
[
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
},
|
||||
new_text: "abc",
|
||||
insert_text_format: None,
|
||||
annotation_id: None,
|
||||
},
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 7,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 7,
|
||||
},
|
||||
},
|
||||
new_text: "$0",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
]
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snippet_rendering_tabstops_before_text_edit() {
|
||||
let mut edit = TextEdit::builder();
|
||||
edit.insert(2.into(), "abc".to_owned());
|
||||
let edit = edit.finish();
|
||||
let snippets =
|
||||
SnippetEdit::new(vec![Snippet::Tabstop(0.into()), Snippet::Tabstop(0.into())]);
|
||||
|
||||
check_rendered_snippets(
|
||||
edit,
|
||||
snippets,
|
||||
expect![[r#"
|
||||
[
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
},
|
||||
new_text: "$1",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
},
|
||||
new_text: "$0",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 2,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 2,
|
||||
},
|
||||
},
|
||||
new_text: "abc",
|
||||
insert_text_format: None,
|
||||
annotation_id: None,
|
||||
},
|
||||
]
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snippet_rendering_tabstops_between_text_edits() {
|
||||
let mut edit = TextEdit::builder();
|
||||
edit.insert(0.into(), "abc".to_owned());
|
||||
edit.insert(7.into(), "abc".to_owned());
|
||||
let edit = edit.finish();
|
||||
let snippets =
|
||||
SnippetEdit::new(vec![Snippet::Tabstop(4.into()), Snippet::Tabstop(4.into())]);
|
||||
|
||||
check_rendered_snippets(
|
||||
edit,
|
||||
snippets,
|
||||
expect![[r#"
|
||||
[
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
},
|
||||
new_text: "abc",
|
||||
insert_text_format: None,
|
||||
annotation_id: None,
|
||||
},
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 4,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 4,
|
||||
},
|
||||
},
|
||||
new_text: "$1",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 4,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 4,
|
||||
},
|
||||
},
|
||||
new_text: "$0",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 7,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 7,
|
||||
},
|
||||
},
|
||||
new_text: "abc",
|
||||
insert_text_format: None,
|
||||
annotation_id: None,
|
||||
},
|
||||
]
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snippet_rendering_multiple_tabstops_in_text_edit() {
|
||||
let mut edit = TextEdit::builder();
|
||||
edit.insert(0.into(), "abcdefghijkl".to_owned());
|
||||
let edit = edit.finish();
|
||||
let snippets = SnippetEdit::new(vec![
|
||||
Snippet::Tabstop(0.into()),
|
||||
Snippet::Tabstop(5.into()),
|
||||
Snippet::Tabstop(12.into()),
|
||||
]);
|
||||
|
||||
check_rendered_snippets(
|
||||
edit,
|
||||
snippets,
|
||||
expect![[r#"
|
||||
[
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
},
|
||||
new_text: "$1abcde$2fghijkl$0",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
]
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snippet_rendering_multiple_placeholders_in_text_edit() {
|
||||
let mut edit = TextEdit::builder();
|
||||
edit.insert(0.into(), "abcdefghijkl".to_owned());
|
||||
let edit = edit.finish();
|
||||
let snippets = SnippetEdit::new(vec![
|
||||
Snippet::Placeholder(TextRange::new(0.into(), 3.into())),
|
||||
Snippet::Placeholder(TextRange::new(5.into(), 7.into())),
|
||||
Snippet::Placeholder(TextRange::new(10.into(), 12.into())),
|
||||
]);
|
||||
|
||||
check_rendered_snippets(
|
||||
edit,
|
||||
snippets,
|
||||
expect![[r#"
|
||||
[
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
},
|
||||
new_text: "${1:abc}de${2:fg}hij${0:kl}",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
]
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snippet_rendering_escape_snippet_bits() {
|
||||
// only needed for snippet formats
|
||||
let mut edit = TextEdit::builder();
|
||||
edit.insert(0.into(), r"abc\def$".to_owned());
|
||||
edit.insert(8.into(), r"ghi\jkl$".to_owned());
|
||||
let edit = edit.finish();
|
||||
let snippets =
|
||||
SnippetEdit::new(vec![Snippet::Placeholder(TextRange::new(0.into(), 3.into()))]);
|
||||
|
||||
check_rendered_snippets(
|
||||
edit,
|
||||
snippets,
|
||||
expect![[r#"
|
||||
[
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 0,
|
||||
},
|
||||
},
|
||||
new_text: "${0:abc}\\\\def\\$",
|
||||
insert_text_format: Some(
|
||||
Snippet,
|
||||
),
|
||||
annotation_id: None,
|
||||
},
|
||||
SnippetTextEdit {
|
||||
range: Range {
|
||||
start: Position {
|
||||
line: 0,
|
||||
character: 8,
|
||||
},
|
||||
end: Position {
|
||||
line: 0,
|
||||
character: 8,
|
||||
},
|
||||
},
|
||||
new_text: "ghi\\jkl$",
|
||||
insert_text_format: None,
|
||||
annotation_id: None,
|
||||
},
|
||||
]
|
||||
"#]],
|
||||
);
|
||||
}
|
||||
|
||||
// `Url` is not able to parse windows paths on unix machines.
|
||||
#[test]
|
||||
#[cfg(target_os = "windows")]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue