Skip to content

Commit 89f7bf7

Browse files
committed
Add SnippetEdit to be alongside source changes
Rendering of snippet edits is deferred to places using source change
1 parent 75ac37f commit 89f7bf7

File tree

7 files changed

+335
-160
lines changed

7 files changed

+335
-160
lines changed

crates/ide-assists/src/tests.rs

Lines changed: 46 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ fn check_with_config(
191191
&& source_change.file_system_edits.len() == 0;
192192

193193
let mut buf = String::new();
194-
for (file_id, edit) in source_change.source_file_edits {
194+
for (file_id, (edit, _snippet_edit)) in source_change.source_file_edits {
195195
let mut text = db.file_text(file_id).as_ref().to_owned();
196196
edit.apply(&mut text);
197197
if !skip_header {
@@ -485,18 +485,21 @@ pub fn test_some_range(a: int) -> bool {
485485
source_file_edits: {
486486
FileId(
487487
0,
488-
): TextEdit {
489-
indels: [
490-
Indel {
491-
insert: "let $0var_name = 5;\n ",
492-
delete: 45..45,
493-
},
494-
Indel {
495-
insert: "var_name",
496-
delete: 59..60,
497-
},
498-
],
499-
},
488+
): (
489+
TextEdit {
490+
indels: [
491+
Indel {
492+
insert: "let $0var_name = 5;\n ",
493+
delete: 45..45,
494+
},
495+
Indel {
496+
insert: "var_name",
497+
delete: 59..60,
498+
},
499+
],
500+
},
501+
None,
502+
),
500503
},
501504
file_system_edits: [],
502505
is_snippet: true,
@@ -544,18 +547,21 @@ pub fn test_some_range(a: int) -> bool {
544547
source_file_edits: {
545548
FileId(
546549
0,
547-
): TextEdit {
548-
indels: [
549-
Indel {
550-
insert: "let $0var_name = 5;\n ",
551-
delete: 45..45,
552-
},
553-
Indel {
554-
insert: "var_name",
555-
delete: 59..60,
556-
},
557-
],
558-
},
550+
): (
551+
TextEdit {
552+
indels: [
553+
Indel {
554+
insert: "let $0var_name = 5;\n ",
555+
delete: 45..45,
556+
},
557+
Indel {
558+
insert: "var_name",
559+
delete: 59..60,
560+
},
561+
],
562+
},
563+
None,
564+
),
559565
},
560566
file_system_edits: [],
561567
is_snippet: true,
@@ -581,18 +587,21 @@ pub fn test_some_range(a: int) -> bool {
581587
source_file_edits: {
582588
FileId(
583589
0,
584-
): TextEdit {
585-
indels: [
586-
Indel {
587-
insert: "fun_name()",
588-
delete: 59..60,
589-
},
590-
Indel {
591-
insert: "\n\nfn $0fun_name() -> i32 {\n 5\n}",
592-
delete: 110..110,
593-
},
594-
],
595-
},
590+
): (
591+
TextEdit {
592+
indels: [
593+
Indel {
594+
insert: "fun_name()",
595+
delete: 59..60,
596+
},
597+
Indel {
598+
insert: "\n\nfn $0fun_name() -> i32 {\n 5\n}",
599+
delete: 110..110,
600+
},
601+
],
602+
},
603+
None,
604+
),
596605
},
597606
file_system_edits: [],
598607
is_snippet: true,

crates/ide-db/src/source_change.rs

Lines changed: 130 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::{collections::hash_map::Entry, iter, mem};
77

88
use crate::SnippetCap;
99
use base_db::{AnchoredPathBuf, FileId};
10+
use itertools::Itertools;
1011
use nohash_hasher::IntMap;
1112
use stdx::never;
1213
use syntax::{
@@ -17,7 +18,7 @@ use text_edit::{TextEdit, TextEditBuilder};
1718

1819
#[derive(Default, Debug, Clone)]
1920
pub struct SourceChange {
20-
pub source_file_edits: IntMap<FileId, TextEdit>,
21+
pub source_file_edits: IntMap<FileId, (TextEdit, Option<SnippetEdit>)>,
2122
pub file_system_edits: Vec<FileSystemEdit>,
2223
pub is_snippet: bool,
2324
}
@@ -26,28 +27,47 @@ impl SourceChange {
2627
/// Creates a new SourceChange with the given label
2728
/// from the edits.
2829
pub fn from_edits(
29-
source_file_edits: IntMap<FileId, TextEdit>,
30+
source_file_edits: IntMap<FileId, (TextEdit, Option<SnippetEdit>)>,
3031
file_system_edits: Vec<FileSystemEdit>,
3132
) -> Self {
3233
SourceChange { source_file_edits, file_system_edits, is_snippet: false }
3334
}
3435

3536
pub fn from_text_edit(file_id: FileId, edit: TextEdit) -> Self {
3637
SourceChange {
37-
source_file_edits: iter::once((file_id, edit)).collect(),
38+
source_file_edits: iter::once((file_id, (edit, None))).collect(),
3839
..Default::default()
3940
}
4041
}
4142

4243
/// Inserts a [`TextEdit`] for the given [`FileId`]. This properly handles merging existing
4344
/// edits for a file if some already exist.
4445
pub fn insert_source_edit(&mut self, file_id: FileId, edit: TextEdit) {
46+
self.insert_source_and_snippet_edit(file_id, edit, None)
47+
}
48+
49+
/// Inserts a [`TextEdit`] and potentially a [`SnippetEdit`] for the given [`FileId`].
50+
/// This properly handles merging existing edits for a file if some already exist.
51+
pub fn insert_source_and_snippet_edit(
52+
&mut self,
53+
file_id: FileId,
54+
edit: TextEdit,
55+
snippet_edit: Option<SnippetEdit>,
56+
) {
4557
match self.source_file_edits.entry(file_id) {
4658
Entry::Occupied(mut entry) => {
47-
never!(entry.get_mut().union(edit).is_err(), "overlapping edits for same file");
59+
let value = entry.get_mut();
60+
never!(value.0.union(edit).is_err(), "overlapping edits for same file");
61+
never!(
62+
value.1.is_some() && snippet_edit.is_some(),
63+
"overlapping snippet edits for same file"
64+
);
65+
if value.1.is_none() {
66+
value.1 = snippet_edit;
67+
}
4868
}
4969
Entry::Vacant(entry) => {
50-
entry.insert(edit);
70+
entry.insert((edit, snippet_edit));
5171
}
5272
}
5373
}
@@ -57,7 +77,7 @@ impl SourceChange {
5777
}
5878

5979
pub fn get_source_edit(&self, file_id: FileId) -> Option<&TextEdit> {
60-
self.source_file_edits.get(&file_id)
80+
self.source_file_edits.get(&file_id).map(|(edit, _)| edit)
6181
}
6282

6383
pub fn merge(mut self, other: SourceChange) -> SourceChange {
@@ -70,7 +90,18 @@ impl SourceChange {
7090

7191
impl Extend<(FileId, TextEdit)> for SourceChange {
7292
fn extend<T: IntoIterator<Item = (FileId, TextEdit)>>(&mut self, iter: T) {
73-
iter.into_iter().for_each(|(file_id, edit)| self.insert_source_edit(file_id, edit));
93+
self.extend(iter.into_iter().map(|(file_id, edit)| (file_id, (edit, None))))
94+
}
95+
}
96+
97+
impl Extend<(FileId, (TextEdit, Option<SnippetEdit>))> for SourceChange {
98+
fn extend<T: IntoIterator<Item = (FileId, (TextEdit, Option<SnippetEdit>))>>(
99+
&mut self,
100+
iter: T,
101+
) {
102+
iter.into_iter().for_each(|(file_id, (edit, snippet_edit))| {
103+
self.insert_source_and_snippet_edit(file_id, edit, snippet_edit)
104+
});
74105
}
75106
}
76107

@@ -82,6 +113,14 @@ impl Extend<FileSystemEdit> for SourceChange {
82113

83114
impl From<IntMap<FileId, TextEdit>> for SourceChange {
84115
fn from(source_file_edits: IntMap<FileId, TextEdit>) -> SourceChange {
116+
let source_file_edits =
117+
source_file_edits.into_iter().map(|(file_id, edit)| (file_id, (edit, None))).collect();
118+
SourceChange { source_file_edits, file_system_edits: Vec::new(), is_snippet: false }
119+
}
120+
}
121+
122+
impl From<IntMap<FileId, (TextEdit, Option<SnippetEdit>)>> for SourceChange {
123+
fn from(source_file_edits: IntMap<FileId, (TextEdit, Option<SnippetEdit>)>) -> SourceChange {
85124
SourceChange { source_file_edits, file_system_edits: Vec::new(), is_snippet: false }
86125
}
87126
}
@@ -94,6 +133,73 @@ impl FromIterator<(FileId, TextEdit)> for SourceChange {
94133
}
95134
}
96135

136+
impl FromIterator<(FileId, (TextEdit, Option<SnippetEdit>))> for SourceChange {
137+
fn from_iter<T: IntoIterator<Item = (FileId, (TextEdit, Option<SnippetEdit>))>>(
138+
iter: T,
139+
) -> Self {
140+
let mut this = SourceChange::default();
141+
this.extend(iter);
142+
this
143+
}
144+
}
145+
146+
#[derive(Debug, Clone, PartialEq, Eq)]
147+
pub struct SnippetEdit(Vec<(u32, TextRange)>);
148+
149+
impl SnippetEdit {
150+
fn new(snippets: Vec<Snippet>) -> Self {
151+
let mut snippet_ranges = snippets
152+
.into_iter()
153+
.zip(1..)
154+
.with_position()
155+
.map(|pos| {
156+
let (snippet, index) = match pos {
157+
itertools::Position::First(it) | itertools::Position::Middle(it) => it,
158+
// last/only snippet gets index 0
159+
itertools::Position::Last((snippet, _))
160+
| itertools::Position::Only((snippet, _)) => (snippet, 0),
161+
};
162+
163+
let range = match snippet {
164+
Snippet::Tabstop(pos) => TextRange::empty(pos),
165+
Snippet::Placeholder(range) => range,
166+
};
167+
(index, range)
168+
})
169+
.collect_vec();
170+
171+
snippet_ranges.sort_by_key(|(_, range)| range.start());
172+
173+
// Ensure that none of the ranges overlap
174+
let disjoint_ranges =
175+
snippet_ranges.windows(2).all(|ranges| ranges[0].1.end() <= ranges[1].1.start());
176+
stdx::always!(disjoint_ranges);
177+
178+
SnippetEdit(snippet_ranges)
179+
}
180+
181+
/// Inserts all of the snippets into the given text.
182+
pub fn apply(&self, text: &mut String) {
183+
// Start from the back so that we don't have to adjust ranges
184+
for (index, range) in self.0.iter().rev() {
185+
if range.is_empty() {
186+
// is a tabstop
187+
text.insert_str(range.start().into(), &format!("${index}"));
188+
} else {
189+
// is a placeholder
190+
text.insert(range.end().into(), '}');
191+
text.insert_str(range.start().into(), &format!("${{{index}:"));
192+
}
193+
}
194+
}
195+
196+
/// Gets the underlying snippet index + text range
197+
/// Tabstops are represented by an empty range, and placeholders use the range that they were given
198+
pub fn into_edit_ranges(self) -> Vec<(u32, TextRange)> {
199+
self.0
200+
}
201+
}
202+
97203
pub struct SourceChangeBuilder {
98204
pub edit: TextEditBuilder,
99205
pub file_id: FileId,
@@ -275,6 +381,16 @@ impl SourceChangeBuilder {
275381

276382
pub fn finish(mut self) -> SourceChange {
277383
self.commit();
384+
385+
// Only one file can have snippet edits
386+
stdx::never!(self
387+
.source_change
388+
.source_file_edits
389+
.iter()
390+
.filter(|(_, (_, snippet_edit))| snippet_edit.is_some())
391+
.at_most_one()
392+
.is_err());
393+
278394
mem::take(&mut self.source_change)
279395
}
280396
}
@@ -296,6 +412,13 @@ impl From<FileSystemEdit> for SourceChange {
296412
}
297413
}
298414

415+
enum Snippet {
416+
/// A tabstop snippet (e.g. `$0`).
417+
Tabstop(TextSize),
418+
/// A placeholder snippet (e.g. `${0:placeholder}`).
419+
Placeholder(TextRange),
420+
}
421+
299422
enum PlaceSnippet {
300423
/// Place a tabstop before an element
301424
Before(SyntaxElement),

crates/ide-diagnostics/src/tests.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,11 @@ fn check_nth_fix(nth: usize, ra_fixture_before: &str, ra_fixture_after: &str) {
4949
let file_id = *source_change.source_file_edits.keys().next().unwrap();
5050
let mut actual = db.file_text(file_id).to_string();
5151

52-
for edit in source_change.source_file_edits.values() {
52+
for (edit, snippet_edit) in source_change.source_file_edits.values() {
5353
edit.apply(&mut actual);
54+
if let Some(snippet_edit) = snippet_edit {
55+
snippet_edit.apply(&mut actual);
56+
}
5457
}
5558
actual
5659
};

0 commit comments

Comments
 (0)