aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--crates/assists/src/assist_context.rs36
-rw-r--r--crates/assists/src/tests.rs40
-rw-r--r--crates/ide/src/diagnostics.rs17
-rw-r--r--crates/ide/src/diagnostics/field_shorthand.rs7
-rw-r--r--crates/ide/src/diagnostics/fixes.rs26
-rw-r--r--crates/ide/src/lib.rs4
-rw-r--r--crates/ide/src/references/rename.rs239
-rw-r--r--crates/ide/src/typing.rs8
-rw-r--r--crates/ide_db/src/source_change.rs56
-rw-r--r--crates/rust-analyzer/src/cli/ssr.rs8
-rw-r--r--crates/rust-analyzer/src/handlers.rs14
-rw-r--r--crates/rust-analyzer/src/to_proto.rs19
-rw-r--r--crates/ssr/src/lib.rs24
-rw-r--r--crates/ssr/src/matching.rs4
-rw-r--r--crates/ssr/src/tests.rs3
-rw-r--r--crates/stdx/src/macros.rs2
16 files changed, 240 insertions, 267 deletions
diff --git a/crates/assists/src/assist_context.rs b/crates/assists/src/assist_context.rs
index 91cc63427..321fe77f3 100644
--- a/crates/assists/src/assist_context.rs
+++ b/crates/assists/src/assist_context.rs
@@ -10,7 +10,7 @@ use ide_db::{
10}; 10};
11use ide_db::{ 11use ide_db::{
12 label::Label, 12 label::Label,
13 source_change::{FileSystemEdit, SourceChange, SourceFileEdit}, 13 source_change::{FileSystemEdit, SourceChange},
14 RootDatabase, 14 RootDatabase,
15}; 15};
16use syntax::{ 16use syntax::{
@@ -180,20 +180,12 @@ impl Assists {
180pub(crate) struct AssistBuilder { 180pub(crate) struct AssistBuilder {
181 edit: TextEditBuilder, 181 edit: TextEditBuilder,
182 file_id: FileId, 182 file_id: FileId,
183 is_snippet: bool, 183 source_change: SourceChange,
184 source_file_edits: Vec<SourceFileEdit>,
185 file_system_edits: Vec<FileSystemEdit>,
186} 184}
187 185
188impl AssistBuilder { 186impl AssistBuilder {
189 pub(crate) fn new(file_id: FileId) -> AssistBuilder { 187 pub(crate) fn new(file_id: FileId) -> AssistBuilder {
190 AssistBuilder { 188 AssistBuilder { edit: TextEdit::builder(), file_id, source_change: SourceChange::default() }
191 edit: TextEdit::builder(),
192 file_id,
193 is_snippet: false,
194 source_file_edits: Vec::default(),
195 file_system_edits: Vec::default(),
196 }
197 } 189 }
198 190
199 pub(crate) fn edit_file(&mut self, file_id: FileId) { 191 pub(crate) fn edit_file(&mut self, file_id: FileId) {
@@ -204,15 +196,7 @@ impl AssistBuilder {
204 fn commit(&mut self) { 196 fn commit(&mut self) {
205 let edit = mem::take(&mut self.edit).finish(); 197 let edit = mem::take(&mut self.edit).finish();
206 if !edit.is_empty() { 198 if !edit.is_empty() {
207 match self.source_file_edits.binary_search_by_key(&self.file_id, |edit| edit.file_id) { 199 self.source_change.insert_source_edit(self.file_id, edit);
208 Ok(idx) => self.source_file_edits[idx]
209 .edit
210 .union(edit)
211 .expect("overlapping edits for same file"),
212 Err(idx) => self
213 .source_file_edits
214 .insert(idx, SourceFileEdit { file_id: self.file_id, edit }),
215 }
216 } 200 }
217 } 201 }
218 202
@@ -231,7 +215,7 @@ impl AssistBuilder {
231 offset: TextSize, 215 offset: TextSize,
232 snippet: impl Into<String>, 216 snippet: impl Into<String>,
233 ) { 217 ) {
234 self.is_snippet = true; 218 self.source_change.is_snippet = true;
235 self.insert(offset, snippet); 219 self.insert(offset, snippet);
236 } 220 }
237 /// Replaces specified `range` of text with a given string. 221 /// Replaces specified `range` of text with a given string.
@@ -245,7 +229,7 @@ impl AssistBuilder {
245 range: TextRange, 229 range: TextRange,
246 snippet: impl Into<String>, 230 snippet: impl Into<String>,
247 ) { 231 ) {
248 self.is_snippet = true; 232 self.source_change.is_snippet = true;
249 self.replace(range, snippet); 233 self.replace(range, snippet);
250 } 234 }
251 pub(crate) fn replace_ast<N: AstNode>(&mut self, old: N, new: N) { 235 pub(crate) fn replace_ast<N: AstNode>(&mut self, old: N, new: N) {
@@ -260,15 +244,11 @@ impl AssistBuilder {
260 pub(crate) fn create_file(&mut self, dst: AnchoredPathBuf, content: impl Into<String>) { 244 pub(crate) fn create_file(&mut self, dst: AnchoredPathBuf, content: impl Into<String>) {
261 let file_system_edit = 245 let file_system_edit =
262 FileSystemEdit::CreateFile { dst: dst.clone(), initial_contents: content.into() }; 246 FileSystemEdit::CreateFile { dst: dst.clone(), initial_contents: content.into() };
263 self.file_system_edits.push(file_system_edit); 247 self.source_change.push_file_system_edit(file_system_edit);
264 } 248 }
265 249
266 fn finish(mut self) -> SourceChange { 250 fn finish(mut self) -> SourceChange {
267 self.commit(); 251 self.commit();
268 SourceChange { 252 mem::take(&mut self.source_change)
269 source_file_edits: mem::take(&mut self.source_file_edits),
270 file_system_edits: mem::take(&mut self.file_system_edits),
271 is_snippet: self.is_snippet,
272 }
273 } 253 }
274} 254}
diff --git a/crates/assists/src/tests.rs b/crates/assists/src/tests.rs
index fef29a0b8..71431b406 100644
--- a/crates/assists/src/tests.rs
+++ b/crates/assists/src/tests.rs
@@ -80,10 +80,8 @@ fn check_doc_test(assist_id: &str, before: &str, after: &str) {
80 let actual = { 80 let actual = {
81 let source_change = assist.source_change.unwrap(); 81 let source_change = assist.source_change.unwrap();
82 let mut actual = before; 82 let mut actual = before;
83 for source_file_edit in source_change.source_file_edits { 83 if let Some(source_file_edit) = source_change.get_source_edit(file_id) {
84 if source_file_edit.file_id == file_id { 84 source_file_edit.apply(&mut actual);
85 source_file_edit.edit.apply(&mut actual)
86 }
87 } 85 }
88 actual 86 actual
89 }; 87 };
@@ -116,37 +114,33 @@ fn check(handler: Handler, before: &str, expected: ExpectedResult, assist_label:
116 114
117 match (assist, expected) { 115 match (assist, expected) {
118 (Some(assist), ExpectedResult::After(after)) => { 116 (Some(assist), ExpectedResult::After(after)) => {
119 let mut source_change = assist.source_change.unwrap(); 117 let source_change = assist.source_change.unwrap();
120 assert!(!source_change.source_file_edits.is_empty()); 118 assert!(!source_change.source_file_edits.is_empty());
121 let skip_header = source_change.source_file_edits.len() == 1 119 let skip_header = source_change.source_file_edits.len() == 1
122 && source_change.file_system_edits.len() == 0; 120 && source_change.file_system_edits.len() == 0;
123 source_change.source_file_edits.sort_by_key(|it| it.file_id);
124 121
125 let mut buf = String::new(); 122 let mut buf = String::new();
126 for source_file_edit in source_change.source_file_edits { 123 for (file_id, edit) in source_change.source_file_edits {
127 let mut text = db.file_text(source_file_edit.file_id).as_ref().to_owned(); 124 let mut text = db.file_text(file_id).as_ref().to_owned();
128 source_file_edit.edit.apply(&mut text); 125 edit.apply(&mut text);
129 if !skip_header { 126 if !skip_header {
130 let sr = db.file_source_root(source_file_edit.file_id); 127 let sr = db.file_source_root(file_id);
131 let sr = db.source_root(sr); 128 let sr = db.source_root(sr);
132 let path = sr.path_for_file(&source_file_edit.file_id).unwrap(); 129 let path = sr.path_for_file(&file_id).unwrap();
133 format_to!(buf, "//- {}\n", path) 130 format_to!(buf, "//- {}\n", path)
134 } 131 }
135 buf.push_str(&text); 132 buf.push_str(&text);
136 } 133 }
137 134
138 for file_system_edit in source_change.file_system_edits.clone() { 135 for file_system_edit in source_change.file_system_edits {
139 match file_system_edit { 136 if let FileSystemEdit::CreateFile { dst, initial_contents } = file_system_edit {
140 FileSystemEdit::CreateFile { dst, initial_contents } => { 137 let sr = db.file_source_root(dst.anchor);
141 let sr = db.file_source_root(dst.anchor); 138 let sr = db.source_root(sr);
142 let sr = db.source_root(sr); 139 let mut base = sr.path_for_file(&dst.anchor).unwrap().clone();
143 let mut base = sr.path_for_file(&dst.anchor).unwrap().clone(); 140 base.pop();
144 base.pop(); 141 let created_file_path = format!("{}{}", base.to_string(), &dst.path[1..]);
145 let created_file_path = format!("{}{}", base.to_string(), &dst.path[1..]); 142 format_to!(buf, "//- {}\n", created_file_path);
146 format_to!(buf, "//- {}\n", created_file_path); 143 buf.push_str(&initial_contents);
147 buf.push_str(&initial_contents);
148 }
149 _ => (),
150 } 144 }
151 } 145 }
152 146
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
index 055c0a79c..2e5395b51 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide/src/diagnostics.rs
@@ -13,8 +13,7 @@ use hir::{
13 diagnostics::{Diagnostic as _, DiagnosticCode, DiagnosticSinkBuilder}, 13 diagnostics::{Diagnostic as _, DiagnosticCode, DiagnosticSinkBuilder},
14 Semantics, 14 Semantics,
15}; 15};
16use ide_db::base_db::SourceDatabase; 16use ide_db::{base_db::SourceDatabase, RootDatabase};
17use ide_db::RootDatabase;
18use itertools::Itertools; 17use itertools::Itertools;
19use rustc_hash::FxHashSet; 18use rustc_hash::FxHashSet;
20use syntax::{ 19use syntax::{
@@ -23,7 +22,7 @@ use syntax::{
23}; 22};
24use text_edit::TextEdit; 23use text_edit::TextEdit;
25 24
26use crate::{FileId, Label, SourceChange, SourceFileEdit}; 25use crate::{FileId, Label, SourceChange};
27 26
28use self::fixes::DiagnosticWithFix; 27use self::fixes::DiagnosticWithFix;
29 28
@@ -220,7 +219,7 @@ fn check_unnecessary_braces_in_use_statement(
220 Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string()) 219 Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string())
221 .with_fix(Some(Fix::new( 220 .with_fix(Some(Fix::new(
222 "Remove unnecessary braces", 221 "Remove unnecessary braces",
223 SourceFileEdit { file_id, edit }.into(), 222 SourceChange::from_text_edit(file_id, edit),
224 use_range, 223 use_range,
225 ))), 224 ))),
226 ); 225 );
@@ -265,13 +264,11 @@ mod tests {
265 .unwrap(); 264 .unwrap();
266 let fix = diagnostic.fix.unwrap(); 265 let fix = diagnostic.fix.unwrap();
267 let actual = { 266 let actual = {
268 let file_id = fix.source_change.source_file_edits.first().unwrap().file_id; 267 let file_id = *fix.source_change.source_file_edits.keys().next().unwrap();
269 let mut actual = analysis.file_text(file_id).unwrap().to_string(); 268 let mut actual = analysis.file_text(file_id).unwrap().to_string();
270 269
271 // Go from the last one to the first one, so that ranges won't be affected by previous edits. 270 for edit in fix.source_change.source_file_edits.values() {
272 // FIXME: https://github.com/rust-analyzer/rust-analyzer/issues/4901#issuecomment-644675309 271 edit.apply(&mut actual);
273 for edit in fix.source_change.source_file_edits.iter().rev() {
274 edit.edit.apply(&mut actual);
275 } 272 }
276 actual 273 actual
277 }; 274 };
@@ -616,7 +613,7 @@ fn test_fn() {
616 Fix { 613 Fix {
617 label: "Create module", 614 label: "Create module",
618 source_change: SourceChange { 615 source_change: SourceChange {
619 source_file_edits: [], 616 source_file_edits: {},
620 file_system_edits: [ 617 file_system_edits: [
621 CreateFile { 618 CreateFile {
622 dst: AnchoredPathBuf { 619 dst: AnchoredPathBuf {
diff --git a/crates/ide/src/diagnostics/field_shorthand.rs b/crates/ide/src/diagnostics/field_shorthand.rs
index 16c6ea827..5c89e2170 100644
--- a/crates/ide/src/diagnostics/field_shorthand.rs
+++ b/crates/ide/src/diagnostics/field_shorthand.rs
@@ -1,8 +1,7 @@
1//! Suggests shortening `Foo { field: field }` to `Foo { field }` in both 1//! Suggests shortening `Foo { field: field }` to `Foo { field }` in both
2//! expressions and patterns. 2//! expressions and patterns.
3 3
4use ide_db::base_db::FileId; 4use ide_db::{base_db::FileId, source_change::SourceChange};
5use ide_db::source_change::SourceFileEdit;
6use syntax::{ast, match_ast, AstNode, SyntaxNode}; 5use syntax::{ast, match_ast, AstNode, SyntaxNode};
7use text_edit::TextEdit; 6use text_edit::TextEdit;
8 7
@@ -50,7 +49,7 @@ fn check_expr_field_shorthand(
50 Diagnostic::hint(field_range, "Shorthand struct initialization".to_string()).with_fix( 49 Diagnostic::hint(field_range, "Shorthand struct initialization".to_string()).with_fix(
51 Some(Fix::new( 50 Some(Fix::new(
52 "Use struct shorthand initialization", 51 "Use struct shorthand initialization",
53 SourceFileEdit { file_id, edit }.into(), 52 SourceChange::from_text_edit(file_id, edit),
54 field_range, 53 field_range,
55 )), 54 )),
56 ), 55 ),
@@ -89,7 +88,7 @@ fn check_pat_field_shorthand(
89 acc.push(Diagnostic::hint(field_range, "Shorthand struct pattern".to_string()).with_fix( 88 acc.push(Diagnostic::hint(field_range, "Shorthand struct pattern".to_string()).with_fix(
90 Some(Fix::new( 89 Some(Fix::new(
91 "Use struct field shorthand", 90 "Use struct field shorthand",
92 SourceFileEdit { file_id, edit }.into(), 91 SourceChange::from_text_edit(file_id, edit),
93 field_range, 92 field_range,
94 )), 93 )),
95 )); 94 ));
diff --git a/crates/ide/src/diagnostics/fixes.rs b/crates/ide/src/diagnostics/fixes.rs
index d7ad88ed5..e4335119b 100644
--- a/crates/ide/src/diagnostics/fixes.rs
+++ b/crates/ide/src/diagnostics/fixes.rs
@@ -8,9 +8,9 @@ use hir::{
8 }, 8 },
9 HasSource, HirDisplay, InFile, Semantics, VariantDef, 9 HasSource, HirDisplay, InFile, Semantics, VariantDef,
10}; 10};
11use ide_db::base_db::{AnchoredPathBuf, FileId};
12use ide_db::{ 11use ide_db::{
13 source_change::{FileSystemEdit, SourceFileEdit}, 12 base_db::{AnchoredPathBuf, FileId},
13 source_change::{FileSystemEdit, SourceChange},
14 RootDatabase, 14 RootDatabase,
15}; 15};
16use syntax::{ 16use syntax::{
@@ -88,7 +88,7 @@ impl DiagnosticWithFix for MissingFields {
88 }; 88 };
89 Some(Fix::new( 89 Some(Fix::new(
90 "Fill struct fields", 90 "Fill struct fields",
91 SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(), 91 SourceChange::from_text_edit(self.file.original_file(sema.db), edit),
92 sema.original_range(&field_list_parent.syntax()).range, 92 sema.original_range(&field_list_parent.syntax()).range,
93 )) 93 ))
94 } 94 }
@@ -101,8 +101,7 @@ impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
101 let tail_expr_range = tail_expr.syntax().text_range(); 101 let tail_expr_range = tail_expr.syntax().text_range();
102 let replacement = format!("{}({})", self.required, tail_expr.syntax()); 102 let replacement = format!("{}({})", self.required, tail_expr.syntax());
103 let edit = TextEdit::replace(tail_expr_range, replacement); 103 let edit = TextEdit::replace(tail_expr_range, replacement);
104 let source_change = 104 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
105 SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into();
106 let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; 105 let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" };
107 Some(Fix::new(name, source_change, tail_expr_range)) 106 Some(Fix::new(name, source_change, tail_expr_range))
108 } 107 }
@@ -122,8 +121,7 @@ impl DiagnosticWithFix for RemoveThisSemicolon {
122 .text_range(); 121 .text_range();
123 122
124 let edit = TextEdit::delete(semicolon); 123 let edit = TextEdit::delete(semicolon);
125 let source_change = 124 let source_change = SourceChange::from_text_edit(self.file.original_file(sema.db), edit);
126 SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into();
127 125
128 Some(Fix::new("Remove this semicolon", source_change, semicolon)) 126 Some(Fix::new("Remove this semicolon", source_change, semicolon))
129 } 127 }
@@ -204,15 +202,11 @@ fn missing_record_expr_field_fix(
204 new_field = format!(",{}", new_field); 202 new_field = format!(",{}", new_field);
205 } 203 }
206 204
207 let source_change = SourceFileEdit { 205 let source_change = SourceChange::from_text_edit(
208 file_id: def_file_id, 206 def_file_id,
209 edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field), 207 TextEdit::insert(last_field_syntax.text_range().end(), new_field),
210 }; 208 );
211 return Some(Fix::new( 209 return Some(Fix::new("Create field", source_change, record_expr_field.syntax().text_range()));
212 "Create field",
213 source_change.into(),
214 record_expr_field.syntax().text_range(),
215 ));
216 210
217 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> { 211 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
218 match field_def_list { 212 match field_def_list {
diff --git a/crates/ide/src/lib.rs b/crates/ide/src/lib.rs
index 1e03832ec..afd552008 100644
--- a/crates/ide/src/lib.rs
+++ b/crates/ide/src/lib.rs
@@ -98,7 +98,7 @@ pub use ide_db::{
98 label::Label, 98 label::Label,
99 line_index::{LineCol, LineIndex}, 99 line_index::{LineCol, LineIndex},
100 search::SearchScope, 100 search::SearchScope,
101 source_change::{FileSystemEdit, SourceChange, SourceFileEdit}, 101 source_change::{FileSystemEdit, SourceChange},
102 symbol_index::Query, 102 symbol_index::Query,
103 RootDatabase, 103 RootDatabase,
104}; 104};
@@ -553,7 +553,7 @@ impl Analysis {
553 let rule: ssr::SsrRule = query.parse()?; 553 let rule: ssr::SsrRule = query.parse()?;
554 let mut match_finder = ssr::MatchFinder::in_context(db, resolve_context, selections); 554 let mut match_finder = ssr::MatchFinder::in_context(db, resolve_context, selections);
555 match_finder.add_rule(rule)?; 555 match_finder.add_rule(rule)?;
556 let edits = if parse_only { Vec::new() } else { match_finder.edits() }; 556 let edits = if parse_only { Default::default() } else { match_finder.edits() };
557 Ok(SourceChange::from(edits)) 557 Ok(SourceChange::from(edits))
558 }) 558 })
559 } 559 }
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs
index c3ae568c2..039efb26f 100644
--- a/crates/ide/src/references/rename.rs
+++ b/crates/ide/src/references/rename.rs
@@ -21,7 +21,7 @@ use text_edit::TextEdit;
21 21
22use crate::{ 22use crate::{
23 FilePosition, FileSystemEdit, RangeInfo, ReferenceKind, ReferenceSearchResult, SourceChange, 23 FilePosition, FileSystemEdit, RangeInfo, ReferenceKind, ReferenceSearchResult, SourceChange,
24 SourceFileEdit, TextRange, TextSize, 24 TextRange, TextSize,
25}; 25};
26 26
27type RenameResult<T> = Result<T, RenameError>; 27type RenameResult<T> = Result<T, RenameError>;
@@ -58,7 +58,7 @@ pub(crate) fn prepare_rename(
58 rename_self_to_param(&sema, position, self_token, "dummy") 58 rename_self_to_param(&sema, position, self_token, "dummy")
59 } else { 59 } else {
60 let RangeInfo { range, .. } = find_all_refs(&sema, position)?; 60 let RangeInfo { range, .. } = find_all_refs(&sema, position)?;
61 Ok(RangeInfo::new(range, SourceChange::from(vec![]))) 61 Ok(RangeInfo::new(range, SourceChange::default()))
62 } 62 }
63 .map(|info| RangeInfo::new(info.range, ())) 63 .map(|info| RangeInfo::new(info.range, ()))
64} 64}
@@ -176,7 +176,7 @@ fn source_edit_from_references(
176 file_id: FileId, 176 file_id: FileId,
177 references: &[FileReference], 177 references: &[FileReference],
178 new_name: &str, 178 new_name: &str,
179) -> SourceFileEdit { 179) -> (FileId, TextEdit) {
180 let mut edit = TextEdit::builder(); 180 let mut edit = TextEdit::builder();
181 for reference in references { 181 for reference in references {
182 let mut replacement_text = String::new(); 182 let mut replacement_text = String::new();
@@ -209,8 +209,7 @@ fn source_edit_from_references(
209 }; 209 };
210 edit.replace(range, replacement_text); 210 edit.replace(range, replacement_text);
211 } 211 }
212 212 (file_id, edit.finish())
213 SourceFileEdit { file_id, edit: edit.finish() }
214} 213}
215 214
216fn edit_text_range_for_record_field_expr_or_pat( 215fn edit_text_range_for_record_field_expr_or_pat(
@@ -250,8 +249,8 @@ fn rename_mod(
250 if IdentifierKind::Ident != check_identifier(new_name)? { 249 if IdentifierKind::Ident != check_identifier(new_name)? {
251 bail!("Invalid name `{0}`: cannot rename module to {0}", new_name); 250 bail!("Invalid name `{0}`: cannot rename module to {0}", new_name);
252 } 251 }
253 let mut source_file_edits = Vec::new(); 252
254 let mut file_system_edits = Vec::new(); 253 let mut source_change = SourceChange::default();
255 254
256 let src = module.definition_source(sema.db); 255 let src = module.definition_source(sema.db);
257 let file_id = src.file_id.original_file(sema.db); 256 let file_id = src.file_id.original_file(sema.db);
@@ -265,7 +264,7 @@ fn rename_mod(
265 }; 264 };
266 let dst = AnchoredPathBuf { anchor: file_id, path }; 265 let dst = AnchoredPathBuf { anchor: file_id, path };
267 let move_file = FileSystemEdit::MoveFile { src: file_id, dst }; 266 let move_file = FileSystemEdit::MoveFile { src: file_id, dst };
268 file_system_edits.push(move_file); 267 source_change.push_file_system_edit(move_file);
269 } 268 }
270 ModuleSource::Module(..) => {} 269 ModuleSource::Module(..) => {}
271 } 270 }
@@ -273,20 +272,19 @@ fn rename_mod(
273 if let Some(src) = module.declaration_source(sema.db) { 272 if let Some(src) = module.declaration_source(sema.db) {
274 let file_id = src.file_id.original_file(sema.db); 273 let file_id = src.file_id.original_file(sema.db);
275 let name = src.value.name().unwrap(); 274 let name = src.value.name().unwrap();
276 let edit = SourceFileEdit { 275 source_change.insert_source_edit(
277 file_id, 276 file_id,
278 edit: TextEdit::replace(name.syntax().text_range(), new_name.into()), 277 TextEdit::replace(name.syntax().text_range(), new_name.into()),
279 }; 278 );
280 source_file_edits.push(edit);
281 } 279 }
282 280
283 let RangeInfo { range, info: refs } = find_all_refs(sema, position)?; 281 let RangeInfo { range, info: refs } = find_all_refs(sema, position)?;
284 let ref_edits = refs.references().iter().map(|(&file_id, references)| { 282 let ref_edits = refs.references().iter().map(|(&file_id, references)| {
285 source_edit_from_references(sema, file_id, references, new_name) 283 source_edit_from_references(sema, file_id, references, new_name)
286 }); 284 });
287 source_file_edits.extend(ref_edits); 285 source_change.extend(ref_edits);
288 286
289 Ok(RangeInfo::new(range, SourceChange::from_edits(source_file_edits, file_system_edits))) 287 Ok(RangeInfo::new(range, source_change))
290} 288}
291 289
292fn rename_to_self( 290fn rename_to_self(
@@ -335,20 +333,16 @@ fn rename_to_self(
335 333
336 let RangeInfo { range, info: refs } = find_all_refs(sema, position)?; 334 let RangeInfo { range, info: refs } = find_all_refs(sema, position)?;
337 335
338 let mut edits = refs 336 let mut source_change = SourceChange::default();
339 .references() 337 source_change.extend(refs.references().iter().map(|(&file_id, references)| {
340 .iter() 338 source_edit_from_references(sema, file_id, references, "self")
341 .map(|(&file_id, references)| { 339 }));
342 source_edit_from_references(sema, file_id, references, "self") 340 source_change.insert_source_edit(
343 }) 341 position.file_id,
344 .collect::<Vec<_>>(); 342 TextEdit::replace(param_range, String::from(self_param)),
345 343 );
346 edits.push(SourceFileEdit {
347 file_id: position.file_id,
348 edit: TextEdit::replace(param_range, String::from(self_param)),
349 });
350 344
351 Ok(RangeInfo::new(range, SourceChange::from(edits))) 345 Ok(RangeInfo::new(range, source_change))
352} 346}
353 347
354fn text_edit_from_self_param( 348fn text_edit_from_self_param(
@@ -402,7 +396,7 @@ fn rename_self_to_param(
402 .ok_or_else(|| format_err!("No surrounding method declaration found"))?; 396 .ok_or_else(|| format_err!("No surrounding method declaration found"))?;
403 let search_range = fn_def.syntax().text_range(); 397 let search_range = fn_def.syntax().text_range();
404 398
405 let mut edits: Vec<SourceFileEdit> = vec![]; 399 let mut source_change = SourceChange::default();
406 400
407 for (idx, _) in text.match_indices("self") { 401 for (idx, _) in text.match_indices("self") {
408 let offset: TextSize = idx.try_into().unwrap(); 402 let offset: TextSize = idx.try_into().unwrap();
@@ -416,18 +410,18 @@ fn rename_self_to_param(
416 } else { 410 } else {
417 TextEdit::replace(usage.text_range(), String::from(new_name)) 411 TextEdit::replace(usage.text_range(), String::from(new_name))
418 }; 412 };
419 edits.push(SourceFileEdit { file_id: position.file_id, edit }); 413 source_change.insert_source_edit(position.file_id, edit);
420 } 414 }
421 } 415 }
422 416
423 if edits.len() > 1 && ident_kind == IdentifierKind::Underscore { 417 if source_change.source_file_edits.len() > 1 && ident_kind == IdentifierKind::Underscore {
424 bail!("Cannot rename reference to `_` as it is being referenced multiple times"); 418 bail!("Cannot rename reference to `_` as it is being referenced multiple times");
425 } 419 }
426 420
427 let range = ast::SelfParam::cast(self_token.parent()) 421 let range = ast::SelfParam::cast(self_token.parent())
428 .map_or(self_token.text_range(), |p| p.syntax().text_range()); 422 .map_or(self_token.text_range(), |p| p.syntax().text_range());
429 423
430 Ok(RangeInfo::new(range, SourceChange::from(edits))) 424 Ok(RangeInfo::new(range, source_change))
431} 425}
432 426
433fn rename_reference( 427fn rename_reference(
@@ -464,14 +458,12 @@ fn rename_reference(
464 (IdentifierKind::Ident, _) | (IdentifierKind::Underscore, _) => mark::hit!(rename_ident), 458 (IdentifierKind::Ident, _) | (IdentifierKind::Underscore, _) => mark::hit!(rename_ident),
465 } 459 }
466 460
467 let edit = refs 461 let mut source_change = SourceChange::default();
468 .into_iter() 462 source_change.extend(refs.into_iter().map(|(file_id, references)| {
469 .map(|(file_id, references)| { 463 source_edit_from_references(sema, file_id, &references, new_name)
470 source_edit_from_references(sema, file_id, &references, new_name) 464 }));
471 })
472 .collect::<Vec<_>>();
473 465
474 Ok(RangeInfo::new(range, SourceChange::from(edit))) 466 Ok(RangeInfo::new(range, source_change))
475} 467}
476 468
477#[cfg(test)] 469#[cfg(test)]
@@ -494,8 +486,8 @@ mod tests {
494 let mut text_edit_builder = TextEdit::builder(); 486 let mut text_edit_builder = TextEdit::builder();
495 let mut file_id: Option<FileId> = None; 487 let mut file_id: Option<FileId> = None;
496 for edit in source_change.info.source_file_edits { 488 for edit in source_change.info.source_file_edits {
497 file_id = Some(edit.file_id); 489 file_id = Some(edit.0);
498 for indel in edit.edit.into_iter() { 490 for indel in edit.1.into_iter() {
499 text_edit_builder.replace(indel.delete, indel.insert); 491 text_edit_builder.replace(indel.delete, indel.insert);
500 } 492 }
501 } 493 }
@@ -895,21 +887,18 @@ mod foo$0;
895 RangeInfo { 887 RangeInfo {
896 range: 4..7, 888 range: 4..7,
897 info: SourceChange { 889 info: SourceChange {
898 source_file_edits: [ 890 source_file_edits: {
899 SourceFileEdit { 891 FileId(
900 file_id: FileId( 892 1,
901 1, 893 ): TextEdit {
902 ), 894 indels: [
903 edit: TextEdit { 895 Indel {
904 indels: [ 896 insert: "foo2",
905 Indel { 897 delete: 4..7,
906 insert: "foo2", 898 },
907 delete: 4..7, 899 ],
908 },
909 ],
910 },
911 }, 900 },
912 ], 901 },
913 file_system_edits: [ 902 file_system_edits: [
914 MoveFile { 903 MoveFile {
915 src: FileId( 904 src: FileId(
@@ -950,34 +939,28 @@ use crate::foo$0::FooContent;
950 RangeInfo { 939 RangeInfo {
951 range: 11..14, 940 range: 11..14,
952 info: SourceChange { 941 info: SourceChange {
953 source_file_edits: [ 942 source_file_edits: {
954 SourceFileEdit { 943 FileId(
955 file_id: FileId( 944 0,
956 0, 945 ): TextEdit {
957 ), 946 indels: [
958 edit: TextEdit { 947 Indel {
959 indels: [ 948 insert: "quux",
960 Indel { 949 delete: 8..11,
961 insert: "quux", 950 },
962 delete: 8..11, 951 ],
963 },
964 ],
965 },
966 }, 952 },
967 SourceFileEdit { 953 FileId(
968 file_id: FileId( 954 2,
969 2, 955 ): TextEdit {
970 ), 956 indels: [
971 edit: TextEdit { 957 Indel {
972 indels: [ 958 insert: "quux",
973 Indel { 959 delete: 11..14,
974 insert: "quux", 960 },
975 delete: 11..14, 961 ],
976 },
977 ],
978 },
979 }, 962 },
980 ], 963 },
981 file_system_edits: [ 964 file_system_edits: [
982 MoveFile { 965 MoveFile {
983 src: FileId( 966 src: FileId(
@@ -1012,21 +995,18 @@ mod fo$0o;
1012 RangeInfo { 995 RangeInfo {
1013 range: 4..7, 996 range: 4..7,
1014 info: SourceChange { 997 info: SourceChange {
1015 source_file_edits: [ 998 source_file_edits: {
1016 SourceFileEdit { 999 FileId(
1017 file_id: FileId( 1000 0,
1018 0, 1001 ): TextEdit {
1019 ), 1002 indels: [
1020 edit: TextEdit { 1003 Indel {
1021 indels: [ 1004 insert: "foo2",
1022 Indel { 1005 delete: 4..7,
1023 insert: "foo2", 1006 },
1024 delete: 4..7, 1007 ],
1025 },
1026 ],
1027 },
1028 }, 1008 },
1029 ], 1009 },
1030 file_system_edits: [ 1010 file_system_edits: [
1031 MoveFile { 1011 MoveFile {
1032 src: FileId( 1012 src: FileId(
@@ -1062,21 +1042,18 @@ mod outer { mod fo$0o; }
1062 RangeInfo { 1042 RangeInfo {
1063 range: 16..19, 1043 range: 16..19,
1064 info: SourceChange { 1044 info: SourceChange {
1065 source_file_edits: [ 1045 source_file_edits: {
1066 SourceFileEdit { 1046 FileId(
1067 file_id: FileId( 1047 0,
1068 0, 1048 ): TextEdit {
1069 ), 1049 indels: [
1070 edit: TextEdit { 1050 Indel {
1071 indels: [ 1051 insert: "bar",
1072 Indel { 1052 delete: 16..19,
1073 insert: "bar", 1053 },
1074 delete: 16..19, 1054 ],
1075 },
1076 ],
1077 },
1078 }, 1055 },
1079 ], 1056 },
1080 file_system_edits: [ 1057 file_system_edits: [
1081 MoveFile { 1058 MoveFile {
1082 src: FileId( 1059 src: FileId(
@@ -1135,34 +1112,28 @@ pub mod foo$0;
1135 RangeInfo { 1112 RangeInfo {
1136 range: 8..11, 1113 range: 8..11,
1137 info: SourceChange { 1114 info: SourceChange {
1138 source_file_edits: [ 1115 source_file_edits: {
1139 SourceFileEdit { 1116 FileId(
1140 file_id: FileId( 1117 0,
1141 1, 1118 ): TextEdit {
1142 ), 1119 indels: [
1143 edit: TextEdit { 1120 Indel {
1144 indels: [ 1121 insert: "foo2",
1145 Indel { 1122 delete: 27..30,
1146 insert: "foo2", 1123 },
1147 delete: 8..11, 1124 ],
1148 },
1149 ],
1150 },
1151 }, 1125 },
1152 SourceFileEdit { 1126 FileId(
1153 file_id: FileId( 1127 1,
1154 0, 1128 ): TextEdit {
1155 ), 1129 indels: [
1156 edit: TextEdit { 1130 Indel {
1157 indels: [ 1131 insert: "foo2",
1158 Indel { 1132 delete: 8..11,
1159 insert: "foo2", 1133 },
1160 delete: 27..30, 1134 ],
1161 },
1162 ],
1163 },
1164 }, 1135 },
1165 ], 1136 },
1166 file_system_edits: [ 1137 file_system_edits: [
1167 MoveFile { 1138 MoveFile {
1168 src: FileId( 1139 src: FileId(
diff --git a/crates/ide/src/typing.rs b/crates/ide/src/typing.rs
index 88c905003..e3c3aebac 100644
--- a/crates/ide/src/typing.rs
+++ b/crates/ide/src/typing.rs
@@ -15,8 +15,10 @@
15 15
16mod on_enter; 16mod on_enter;
17 17
18use ide_db::base_db::{FilePosition, SourceDatabase}; 18use ide_db::{
19use ide_db::{source_change::SourceFileEdit, RootDatabase}; 19 base_db::{FilePosition, SourceDatabase},
20 RootDatabase,
21};
20use syntax::{ 22use syntax::{
21 algo::find_node_at_offset, 23 algo::find_node_at_offset,
22 ast::{self, edit::IndentLevel, AstToken}, 24 ast::{self, edit::IndentLevel, AstToken},
@@ -56,7 +58,7 @@ pub(crate) fn on_char_typed(
56 let file = &db.parse(position.file_id).tree(); 58 let file = &db.parse(position.file_id).tree();
57 assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); 59 assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed));
58 let edit = on_char_typed_inner(file, position.offset, char_typed)?; 60 let edit = on_char_typed_inner(file, position.offset, char_typed)?;
59 Some(SourceFileEdit { file_id: position.file_id, edit }.into()) 61 Some(SourceChange::from_text_edit(position.file_id, edit))
60} 62}
61 63
62fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> { 64fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> {
diff --git a/crates/ide_db/src/source_change.rs b/crates/ide_db/src/source_change.rs
index 10c0abdac..b1f87731b 100644
--- a/crates/ide_db/src/source_change.rs
+++ b/crates/ide_db/src/source_change.rs
@@ -3,12 +3,19 @@
3//! 3//!
4//! It can be viewed as a dual for `AnalysisChange`. 4//! It can be viewed as a dual for `AnalysisChange`.
5 5
6use std::{
7 collections::hash_map::Entry,
8 iter::{self, FromIterator},
9};
10
6use base_db::{AnchoredPathBuf, FileId}; 11use base_db::{AnchoredPathBuf, FileId};
12use rustc_hash::FxHashMap;
13use stdx::assert_never;
7use text_edit::TextEdit; 14use text_edit::TextEdit;
8 15
9#[derive(Default, Debug, Clone)] 16#[derive(Default, Debug, Clone)]
10pub struct SourceChange { 17pub struct SourceChange {
11 pub source_file_edits: Vec<SourceFileEdit>, 18 pub source_file_edits: FxHashMap<FileId, TextEdit>,
12 pub file_system_edits: Vec<FileSystemEdit>, 19 pub file_system_edits: Vec<FileSystemEdit>,
13 pub is_snippet: bool, 20 pub is_snippet: bool,
14} 21}
@@ -17,27 +24,50 @@ impl SourceChange {
17 /// Creates a new SourceChange with the given label 24 /// Creates a new SourceChange with the given label
18 /// from the edits. 25 /// from the edits.
19 pub fn from_edits( 26 pub fn from_edits(
20 source_file_edits: Vec<SourceFileEdit>, 27 source_file_edits: FxHashMap<FileId, TextEdit>,
21 file_system_edits: Vec<FileSystemEdit>, 28 file_system_edits: Vec<FileSystemEdit>,
22 ) -> Self { 29 ) -> Self {
23 SourceChange { source_file_edits, file_system_edits, is_snippet: false } 30 SourceChange { source_file_edits, file_system_edits, is_snippet: false }
24 } 31 }
25}
26 32
27#[derive(Debug, Clone)] 33 pub fn from_text_edit(file_id: FileId, edit: TextEdit) -> Self {
28pub struct SourceFileEdit { 34 SourceChange {
29 pub file_id: FileId, 35 source_file_edits: FxHashMap::from_iter(iter::once((file_id, edit))),
30 pub edit: TextEdit, 36 ..Default::default()
37 }
38 }
39
40 pub fn insert_source_edit(&mut self, file_id: FileId, edit: TextEdit) {
41 match self.source_file_edits.entry(file_id) {
42 Entry::Occupied(mut entry) => {
43 assert_never!(
44 entry.get_mut().union(edit).is_err(),
45 "overlapping edits for same file"
46 );
47 }
48 Entry::Vacant(entry) => {
49 entry.insert(edit);
50 }
51 }
52 }
53
54 pub fn push_file_system_edit(&mut self, edit: FileSystemEdit) {
55 self.file_system_edits.push(edit);
56 }
57
58 pub fn get_source_edit(&self, file_id: FileId) -> Option<&TextEdit> {
59 self.source_file_edits.get(&file_id)
60 }
31} 61}
32 62
33impl From<SourceFileEdit> for SourceChange { 63impl Extend<(FileId, TextEdit)> for SourceChange {
34 fn from(edit: SourceFileEdit) -> SourceChange { 64 fn extend<T: IntoIterator<Item = (FileId, TextEdit)>>(&mut self, iter: T) {
35 vec![edit].into() 65 iter.into_iter().for_each(|(file_id, edit)| self.insert_source_edit(file_id, edit));
36 } 66 }
37} 67}
38 68
39impl From<Vec<SourceFileEdit>> for SourceChange { 69impl From<FxHashMap<FileId, TextEdit>> for SourceChange {
40 fn from(source_file_edits: Vec<SourceFileEdit>) -> SourceChange { 70 fn from(source_file_edits: FxHashMap<FileId, TextEdit>) -> SourceChange {
41 SourceChange { source_file_edits, file_system_edits: Vec::new(), is_snippet: false } 71 SourceChange { source_file_edits, file_system_edits: Vec::new(), is_snippet: false }
42 } 72 }
43} 73}
@@ -51,7 +81,7 @@ pub enum FileSystemEdit {
51impl From<FileSystemEdit> for SourceChange { 81impl From<FileSystemEdit> for SourceChange {
52 fn from(edit: FileSystemEdit) -> SourceChange { 82 fn from(edit: FileSystemEdit) -> SourceChange {
53 SourceChange { 83 SourceChange {
54 source_file_edits: Vec::new(), 84 source_file_edits: Default::default(),
55 file_system_edits: vec![edit], 85 file_system_edits: vec![edit],
56 is_snippet: false, 86 is_snippet: false,
57 } 87 }
diff --git a/crates/rust-analyzer/src/cli/ssr.rs b/crates/rust-analyzer/src/cli/ssr.rs
index a06631dac..bbb550ec9 100644
--- a/crates/rust-analyzer/src/cli/ssr.rs
+++ b/crates/rust-analyzer/src/cli/ssr.rs
@@ -12,10 +12,10 @@ pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> Result<()> {
12 match_finder.add_rule(rule)?; 12 match_finder.add_rule(rule)?;
13 } 13 }
14 let edits = match_finder.edits(); 14 let edits = match_finder.edits();
15 for edit in edits { 15 for (file_id, edit) in edits {
16 if let Some(path) = vfs.file_path(edit.file_id).as_path() { 16 if let Some(path) = vfs.file_path(file_id).as_path() {
17 let mut contents = db.file_text(edit.file_id).to_string(); 17 let mut contents = db.file_text(file_id).to_string();
18 edit.edit.apply(&mut contents); 18 edit.apply(&mut contents);
19 std::fs::write(path, contents)?; 19 std::fs::write(path, contents)?;
20 } 20 }
21 } 21 }
diff --git a/crates/rust-analyzer/src/handlers.rs b/crates/rust-analyzer/src/handlers.rs
index dc81f55d6..1a4e0dd32 100644
--- a/crates/rust-analyzer/src/handlers.rs
+++ b/crates/rust-analyzer/src/handlers.rs
@@ -260,15 +260,15 @@ pub(crate) fn handle_on_type_formatting(
260 } 260 }
261 261
262 let edit = snap.analysis.on_char_typed(position, char_typed)?; 262 let edit = snap.analysis.on_char_typed(position, char_typed)?;
263 let mut edit = match edit { 263 let edit = match edit {
264 Some(it) => it, 264 Some(it) => it,
265 None => return Ok(None), 265 None => return Ok(None),
266 }; 266 };
267 267
268 // This should be a single-file edit 268 // This should be a single-file edit
269 let edit = edit.source_file_edits.pop().unwrap(); 269 let (_, edit) = edit.source_file_edits.into_iter().next().unwrap();
270 270
271 let change = to_proto::text_edit_vec(&line_index, line_endings, edit.edit); 271 let change = to_proto::text_edit_vec(&line_index, line_endings, edit);
272 Ok(Some(change)) 272 Ok(Some(change))
273} 273}
274 274
@@ -463,9 +463,11 @@ pub(crate) fn handle_will_rename_files(
463 .collect(); 463 .collect();
464 464
465 // Drop file system edits since we're just renaming things on the same level 465 // Drop file system edits since we're just renaming things on the same level
466 let edits = source_changes.into_iter().map(|it| it.source_file_edits).flatten().collect(); 466 let mut source_changes = source_changes.into_iter();
467 let source_change = SourceChange::from_edits(edits, Vec::new()); 467 let mut source_change = source_changes.next().unwrap_or_default();
468 468 source_change.file_system_edits.clear();
469 // no collect here because we want to merge text edits on same file ids
470 source_change.extend(source_changes.map(|it| it.source_file_edits).flatten());
469 let workspace_edit = to_proto::workspace_edit(&snap, source_change)?; 471 let workspace_edit = to_proto::workspace_edit(&snap, source_change)?;
470 Ok(Some(workspace_edit)) 472 Ok(Some(workspace_edit))
471} 473}
diff --git a/crates/rust-analyzer/src/to_proto.rs b/crates/rust-analyzer/src/to_proto.rs
index a7ff8975a..dc67d19a7 100644
--- a/crates/rust-analyzer/src/to_proto.rs
+++ b/crates/rust-analyzer/src/to_proto.rs
@@ -8,8 +8,7 @@ use ide::{
8 Assist, AssistKind, CallInfo, CompletionItem, CompletionItemKind, Documentation, FileId, 8 Assist, AssistKind, CallInfo, CompletionItem, CompletionItemKind, Documentation, FileId,
9 FileRange, FileSystemEdit, Fold, FoldKind, Highlight, HlMod, HlPunct, HlRange, HlTag, Indel, 9 FileRange, FileSystemEdit, Fold, FoldKind, Highlight, HlMod, HlPunct, HlRange, HlTag, Indel,
10 InlayHint, InlayKind, InsertTextFormat, LineIndex, Markup, NavigationTarget, ReferenceAccess, 10 InlayHint, InlayKind, InsertTextFormat, LineIndex, Markup, NavigationTarget, ReferenceAccess,
11 RenameError, Runnable, Severity, SourceChange, SourceFileEdit, SymbolKind, TextEdit, TextRange, 11 RenameError, Runnable, Severity, SourceChange, SymbolKind, TextEdit, TextRange, TextSize,
12 TextSize,
13}; 12};
14use itertools::Itertools; 13use itertools::Itertools;
15 14
@@ -634,13 +633,13 @@ pub(crate) fn goto_definition_response(
634pub(crate) fn snippet_text_document_edit( 633pub(crate) fn snippet_text_document_edit(
635 snap: &GlobalStateSnapshot, 634 snap: &GlobalStateSnapshot,
636 is_snippet: bool, 635 is_snippet: bool,
637 source_file_edit: SourceFileEdit, 636 file_id: FileId,
637 edit: TextEdit,
638) -> Result<lsp_ext::SnippetTextDocumentEdit> { 638) -> Result<lsp_ext::SnippetTextDocumentEdit> {
639 let text_document = optional_versioned_text_document_identifier(snap, source_file_edit.file_id); 639 let text_document = optional_versioned_text_document_identifier(snap, file_id);
640 let line_index = snap.analysis.file_line_index(source_file_edit.file_id)?; 640 let line_index = snap.analysis.file_line_index(file_id)?;
641 let line_endings = snap.file_line_endings(source_file_edit.file_id); 641 let line_endings = snap.file_line_endings(file_id);
642 let edits = source_file_edit 642 let edits = edit
643 .edit
644 .into_iter() 643 .into_iter()
645 .map(|it| snippet_text_edit(&line_index, line_endings, is_snippet, it)) 644 .map(|it| snippet_text_edit(&line_index, line_endings, is_snippet, it))
646 .collect(); 645 .collect();
@@ -699,8 +698,8 @@ pub(crate) fn snippet_workspace_edit(
699 let ops = snippet_text_document_ops(snap, op); 698 let ops = snippet_text_document_ops(snap, op);
700 document_changes.extend_from_slice(&ops); 699 document_changes.extend_from_slice(&ops);
701 } 700 }
702 for edit in source_change.source_file_edits { 701 for (file_id, edit) in source_change.source_file_edits {
703 let edit = snippet_text_document_edit(&snap, source_change.is_snippet, edit)?; 702 let edit = snippet_text_document_edit(&snap, source_change.is_snippet, file_id, edit)?;
704 document_changes.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit)); 703 document_changes.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit));
705 } 704 }
706 let workspace_edit = 705 let workspace_edit =
diff --git a/crates/ssr/src/lib.rs b/crates/ssr/src/lib.rs
index 747ce495d..a97fc8bca 100644
--- a/crates/ssr/src/lib.rs
+++ b/crates/ssr/src/lib.rs
@@ -75,10 +75,10 @@ pub use crate::matching::Match;
75use crate::matching::MatchFailureReason; 75use crate::matching::MatchFailureReason;
76use hir::Semantics; 76use hir::Semantics;
77use ide_db::base_db::{FileId, FilePosition, FileRange}; 77use ide_db::base_db::{FileId, FilePosition, FileRange};
78use ide_db::source_change::SourceFileEdit;
79use resolving::ResolvedRule; 78use resolving::ResolvedRule;
80use rustc_hash::FxHashMap; 79use rustc_hash::FxHashMap;
81use syntax::{ast, AstNode, SyntaxNode, TextRange}; 80use syntax::{ast, AstNode, SyntaxNode, TextRange};
81use text_edit::TextEdit;
82 82
83// A structured search replace rule. Create by calling `parse` on a str. 83// A structured search replace rule. Create by calling `parse` on a str.
84#[derive(Debug)] 84#[derive(Debug)]
@@ -159,7 +159,7 @@ impl<'db> MatchFinder<'db> {
159 } 159 }
160 160
161 /// Finds matches for all added rules and returns edits for all found matches. 161 /// Finds matches for all added rules and returns edits for all found matches.
162 pub fn edits(&self) -> Vec<SourceFileEdit> { 162 pub fn edits(&self) -> FxHashMap<FileId, TextEdit> {
163 use ide_db::base_db::SourceDatabaseExt; 163 use ide_db::base_db::SourceDatabaseExt;
164 let mut matches_by_file = FxHashMap::default(); 164 let mut matches_by_file = FxHashMap::default();
165 for m in self.matches().matches { 165 for m in self.matches().matches {
@@ -169,13 +169,19 @@ impl<'db> MatchFinder<'db> {
169 .matches 169 .matches
170 .push(m); 170 .push(m);
171 } 171 }
172 let mut edits = vec![]; 172 matches_by_file
173 for (file_id, matches) in matches_by_file { 173 .into_iter()
174 let edit = 174 .map(|(file_id, matches)| {
175 replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id), &self.rules); 175 (
176 edits.push(SourceFileEdit { file_id, edit }); 176 file_id,
177 } 177 replacing::matches_to_edit(
178 edits 178 &matches,
179 &self.sema.db.file_text(file_id),
180 &self.rules,
181 ),
182 )
183 })
184 .collect()
179 } 185 }
180 186
181 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you 187 /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you
diff --git a/crates/ssr/src/matching.rs b/crates/ssr/src/matching.rs
index 6cf831431..42d313f91 100644
--- a/crates/ssr/src/matching.rs
+++ b/crates/ssr/src/matching.rs
@@ -810,9 +810,9 @@ mod tests {
810 810
811 let edits = match_finder.edits(); 811 let edits = match_finder.edits();
812 assert_eq!(edits.len(), 1); 812 assert_eq!(edits.len(), 1);
813 let edit = &edits[0]; 813 let edit = &edits[&position.file_id];
814 let mut after = input.to_string(); 814 let mut after = input.to_string();
815 edit.edit.apply(&mut after); 815 edit.apply(&mut after);
816 assert_eq!(after, "fn foo() {} fn bar() {} fn main() { bar(1+2); }"); 816 assert_eq!(after, "fn foo() {} fn bar() {} fn main() { bar(1+2); }");
817 } 817 }
818} 818}
diff --git a/crates/ssr/src/tests.rs b/crates/ssr/src/tests.rs
index d6918c22d..a3ea44f23 100644
--- a/crates/ssr/src/tests.rs
+++ b/crates/ssr/src/tests.rs
@@ -103,11 +103,10 @@ fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) {
103 if edits.is_empty() { 103 if edits.is_empty() {
104 panic!("No edits were made"); 104 panic!("No edits were made");
105 } 105 }
106 assert_eq!(edits[0].file_id, position.file_id);
107 // Note, db.file_text is not necessarily the same as `input`, since fixture parsing alters 106 // Note, db.file_text is not necessarily the same as `input`, since fixture parsing alters
108 // stuff. 107 // stuff.
109 let mut actual = db.file_text(position.file_id).to_string(); 108 let mut actual = db.file_text(position.file_id).to_string();
110 edits[0].edit.apply(&mut actual); 109 edits[&position.file_id].apply(&mut actual);
111 expected.assert_eq(&actual); 110 expected.assert_eq(&actual);
112} 111}
113 112
diff --git a/crates/stdx/src/macros.rs b/crates/stdx/src/macros.rs
index 263b938e3..4f5c6100d 100644
--- a/crates/stdx/src/macros.rs
+++ b/crates/stdx/src/macros.rs
@@ -66,7 +66,7 @@ macro_rules! impl_from {
66/// Shamelessly stolen from: https://www.sqlite.org/assert.html 66/// Shamelessly stolen from: https://www.sqlite.org/assert.html
67#[macro_export] 67#[macro_export]
68macro_rules! assert_never { 68macro_rules! assert_never {
69 ($cond:expr) => { $crate::assert_always!($cond, "") }; 69 ($cond:expr) => { $crate::assert_never!($cond, "") };
70 ($cond:expr, $($fmt:tt)*) => {{ 70 ($cond:expr, $($fmt:tt)*) => {{
71 let value = $cond; 71 let value = $cond;
72 if value { 72 if value {