Group file source edits by FileId

This commit is contained in:
Lukas Wirth 2021-01-14 18:35:22 +01:00
parent f88f3d6885
commit f51457a643
15 changed files with 188 additions and 180 deletions

View file

@ -10,7 +10,7 @@ use ide_db::{
}; };
use ide_db::{ use ide_db::{
label::Label, label::Label,
source_change::{FileSystemEdit, SourceChange, SourceFileEdit}, source_change::{FileSystemEdit, SourceChange, SourceFileEdits},
RootDatabase, RootDatabase,
}; };
use syntax::{ use syntax::{
@ -181,7 +181,7 @@ pub(crate) struct AssistBuilder {
edit: TextEditBuilder, edit: TextEditBuilder,
file_id: FileId, file_id: FileId,
is_snippet: bool, is_snippet: bool,
source_file_edits: Vec<SourceFileEdit>, source_file_edits: SourceFileEdits,
file_system_edits: Vec<FileSystemEdit>, file_system_edits: Vec<FileSystemEdit>,
} }
@ -191,7 +191,7 @@ impl AssistBuilder {
edit: TextEdit::builder(), edit: TextEdit::builder(),
file_id, file_id,
is_snippet: false, is_snippet: false,
source_file_edits: Vec::default(), source_file_edits: SourceFileEdits::default(),
file_system_edits: Vec::default(), file_system_edits: Vec::default(),
} }
} }
@ -204,15 +204,7 @@ impl AssistBuilder {
fn commit(&mut self) { fn commit(&mut self) {
let edit = mem::take(&mut self.edit).finish(); let edit = mem::take(&mut self.edit).finish();
if !edit.is_empty() { if !edit.is_empty() {
match self.source_file_edits.binary_search_by_key(&self.file_id, |edit| edit.file_id) { self.source_file_edits.insert(self.file_id, edit);
Ok(idx) => self.source_file_edits[idx]
.edit
.union(edit)
.expect("overlapping edits for same file"),
Err(idx) => self
.source_file_edits
.insert(idx, SourceFileEdit { file_id: self.file_id, edit }),
}
} }
} }

View file

@ -80,10 +80,8 @@ fn check_doc_test(assist_id: &str, before: &str, after: &str) {
let actual = { let actual = {
let source_change = assist.source_change.unwrap(); let source_change = assist.source_change.unwrap();
let mut actual = before; let mut actual = before;
for source_file_edit in source_change.source_file_edits { if let Some(source_file_edit) = source_change.source_file_edits.edits.get(&file_id) {
if source_file_edit.file_id == file_id { source_file_edit.apply(&mut actual);
source_file_edit.edit.apply(&mut actual)
}
} }
actual actual
}; };
@ -116,20 +114,19 @@ fn check(handler: Handler, before: &str, expected: ExpectedResult, assist_label:
match (assist, expected) { match (assist, expected) {
(Some(assist), ExpectedResult::After(after)) => { (Some(assist), ExpectedResult::After(after)) => {
let mut source_change = assist.source_change.unwrap(); let source_change = assist.source_change.unwrap();
assert!(!source_change.source_file_edits.is_empty()); assert!(!source_change.source_file_edits.is_empty());
let skip_header = source_change.source_file_edits.len() == 1 let skip_header = source_change.source_file_edits.len() == 1
&& source_change.file_system_edits.len() == 0; && source_change.file_system_edits.len() == 0;
source_change.source_file_edits.sort_by_key(|it| it.file_id);
let mut buf = String::new(); let mut buf = String::new();
for source_file_edit in source_change.source_file_edits { for (file_id, edit) in source_change.source_file_edits.edits {
let mut text = db.file_text(source_file_edit.file_id).as_ref().to_owned(); let mut text = db.file_text(file_id).as_ref().to_owned();
source_file_edit.edit.apply(&mut text); edit.apply(&mut text);
if !skip_header { if !skip_header {
let sr = db.file_source_root(source_file_edit.file_id); let sr = db.file_source_root(file_id);
let sr = db.source_root(sr); let sr = db.source_root(sr);
let path = sr.path_for_file(&source_file_edit.file_id).unwrap(); let path = sr.path_for_file(&file_id).unwrap();
format_to!(buf, "//- {}\n", path) format_to!(buf, "//- {}\n", path)
} }
buf.push_str(&text); buf.push_str(&text);

View file

@ -13,8 +13,7 @@ use hir::{
diagnostics::{Diagnostic as _, DiagnosticCode, DiagnosticSinkBuilder}, diagnostics::{Diagnostic as _, DiagnosticCode, DiagnosticSinkBuilder},
Semantics, Semantics,
}; };
use ide_db::base_db::SourceDatabase; use ide_db::{base_db::SourceDatabase, source_change::SourceFileEdits, RootDatabase};
use ide_db::RootDatabase;
use itertools::Itertools; use itertools::Itertools;
use rustc_hash::FxHashSet; use rustc_hash::FxHashSet;
use syntax::{ use syntax::{
@ -23,7 +22,7 @@ use syntax::{
}; };
use text_edit::TextEdit; use text_edit::TextEdit;
use crate::{FileId, Label, SourceChange, SourceFileEdit}; use crate::{FileId, Label, SourceChange};
use self::fixes::DiagnosticWithFix; use self::fixes::DiagnosticWithFix;
@ -220,7 +219,7 @@ fn check_unnecessary_braces_in_use_statement(
Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string()) Diagnostic::hint(use_range, "Unnecessary braces in use statement".to_string())
.with_fix(Some(Fix::new( .with_fix(Some(Fix::new(
"Remove unnecessary braces", "Remove unnecessary braces",
SourceFileEdit { file_id, edit }.into(), SourceFileEdits::from_text_edit(file_id, edit).into(),
use_range, use_range,
))), ))),
); );
@ -265,13 +264,11 @@ mod tests {
.unwrap(); .unwrap();
let fix = diagnostic.fix.unwrap(); let fix = diagnostic.fix.unwrap();
let actual = { let actual = {
let file_id = fix.source_change.source_file_edits.first().unwrap().file_id; let file_id = *fix.source_change.source_file_edits.edits.keys().next().unwrap();
let mut actual = analysis.file_text(file_id).unwrap().to_string(); let mut actual = analysis.file_text(file_id).unwrap().to_string();
// Go from the last one to the first one, so that ranges won't be affected by previous edits. for edit in fix.source_change.source_file_edits.edits.values() {
// FIXME: https://github.com/rust-analyzer/rust-analyzer/issues/4901#issuecomment-644675309 edit.apply(&mut actual);
for edit in fix.source_change.source_file_edits.iter().rev() {
edit.edit.apply(&mut actual);
} }
actual actual
}; };
@ -616,7 +613,9 @@ fn test_fn() {
Fix { Fix {
label: "Create module", label: "Create module",
source_change: SourceChange { source_change: SourceChange {
source_file_edits: [], source_file_edits: SourceFileEdits {
edits: {},
},
file_system_edits: [ file_system_edits: [
CreateFile { CreateFile {
dst: AnchoredPathBuf { dst: AnchoredPathBuf {

View file

@ -1,8 +1,7 @@
//! Suggests shortening `Foo { field: field }` to `Foo { field }` in both //! Suggests shortening `Foo { field: field }` to `Foo { field }` in both
//! expressions and patterns. //! expressions and patterns.
use ide_db::base_db::FileId; use ide_db::{base_db::FileId, source_change::SourceFileEdits};
use ide_db::source_change::SourceFileEdit;
use syntax::{ast, match_ast, AstNode, SyntaxNode}; use syntax::{ast, match_ast, AstNode, SyntaxNode};
use text_edit::TextEdit; use text_edit::TextEdit;
@ -50,7 +49,7 @@ fn check_expr_field_shorthand(
Diagnostic::hint(field_range, "Shorthand struct initialization".to_string()).with_fix( Diagnostic::hint(field_range, "Shorthand struct initialization".to_string()).with_fix(
Some(Fix::new( Some(Fix::new(
"Use struct shorthand initialization", "Use struct shorthand initialization",
SourceFileEdit { file_id, edit }.into(), SourceFileEdits::from_text_edit(file_id, edit).into(),
field_range, field_range,
)), )),
), ),
@ -89,7 +88,7 @@ fn check_pat_field_shorthand(
acc.push(Diagnostic::hint(field_range, "Shorthand struct pattern".to_string()).with_fix( acc.push(Diagnostic::hint(field_range, "Shorthand struct pattern".to_string()).with_fix(
Some(Fix::new( Some(Fix::new(
"Use struct field shorthand", "Use struct field shorthand",
SourceFileEdit { file_id, edit }.into(), SourceFileEdits::from_text_edit(file_id, edit).into(),
field_range, field_range,
)), )),
)); ));

View file

@ -8,9 +8,9 @@ use hir::{
}, },
HasSource, HirDisplay, InFile, Semantics, VariantDef, HasSource, HirDisplay, InFile, Semantics, VariantDef,
}; };
use ide_db::base_db::{AnchoredPathBuf, FileId};
use ide_db::{ use ide_db::{
source_change::{FileSystemEdit, SourceFileEdit}, base_db::{AnchoredPathBuf, FileId},
source_change::{FileSystemEdit, SourceFileEdits},
RootDatabase, RootDatabase,
}; };
use syntax::{ use syntax::{
@ -88,7 +88,7 @@ impl DiagnosticWithFix for MissingFields {
}; };
Some(Fix::new( Some(Fix::new(
"Fill struct fields", "Fill struct fields",
SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(), SourceFileEdits::from_text_edit(self.file.original_file(sema.db), edit).into(),
sema.original_range(&field_list_parent.syntax()).range, sema.original_range(&field_list_parent.syntax()).range,
)) ))
} }
@ -102,7 +102,7 @@ impl DiagnosticWithFix for MissingOkOrSomeInTailExpr {
let replacement = format!("{}({})", self.required, tail_expr.syntax()); let replacement = format!("{}({})", self.required, tail_expr.syntax());
let edit = TextEdit::replace(tail_expr_range, replacement); let edit = TextEdit::replace(tail_expr_range, replacement);
let source_change = let source_change =
SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(); SourceFileEdits::from_text_edit(self.file.original_file(sema.db), edit).into();
let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" }; let name = if self.required == "Ok" { "Wrap with Ok" } else { "Wrap with Some" };
Some(Fix::new(name, source_change, tail_expr_range)) Some(Fix::new(name, source_change, tail_expr_range))
} }
@ -123,7 +123,7 @@ impl DiagnosticWithFix for RemoveThisSemicolon {
let edit = TextEdit::delete(semicolon); let edit = TextEdit::delete(semicolon);
let source_change = let source_change =
SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(); SourceFileEdits::from_text_edit(self.file.original_file(sema.db), edit).into();
Some(Fix::new("Remove this semicolon", source_change, semicolon)) Some(Fix::new("Remove this semicolon", source_change, semicolon))
} }
@ -204,10 +204,10 @@ fn missing_record_expr_field_fix(
new_field = format!(",{}", new_field); new_field = format!(",{}", new_field);
} }
let source_change = SourceFileEdit { let source_change = SourceFileEdits::from_text_edit(
file_id: def_file_id, def_file_id,
edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field), TextEdit::insert(last_field_syntax.text_range().end(), new_field),
}; );
return Some(Fix::new( return Some(Fix::new(
"Create field", "Create field",
source_change.into(), source_change.into(),

View file

@ -98,7 +98,7 @@ pub use ide_db::{
label::Label, label::Label,
line_index::{LineCol, LineIndex}, line_index::{LineCol, LineIndex},
search::SearchScope, search::SearchScope,
source_change::{FileSystemEdit, SourceChange, SourceFileEdit}, source_change::{FileSystemEdit, SourceChange, SourceFileEdits},
symbol_index::Query, symbol_index::Query,
RootDatabase, RootDatabase,
}; };
@ -553,7 +553,7 @@ impl Analysis {
let rule: ssr::SsrRule = query.parse()?; let rule: ssr::SsrRule = query.parse()?;
let mut match_finder = ssr::MatchFinder::in_context(db, resolve_context, selections); let mut match_finder = ssr::MatchFinder::in_context(db, resolve_context, selections);
match_finder.add_rule(rule)?; match_finder.add_rule(rule)?;
let edits = if parse_only { Vec::new() } else { match_finder.edits() }; let edits = if parse_only { Default::default() } else { match_finder.edits() };
Ok(SourceChange::from(edits)) Ok(SourceChange::from(edits))
}) })
} }

View file

@ -21,7 +21,7 @@ use text_edit::TextEdit;
use crate::{ use crate::{
FilePosition, FileSystemEdit, RangeInfo, ReferenceKind, ReferenceSearchResult, SourceChange, FilePosition, FileSystemEdit, RangeInfo, ReferenceKind, ReferenceSearchResult, SourceChange,
SourceFileEdit, TextRange, TextSize, SourceFileEdits, TextRange, TextSize,
}; };
type RenameResult<T> = Result<T, RenameError>; type RenameResult<T> = Result<T, RenameError>;
@ -58,7 +58,7 @@ pub(crate) fn prepare_rename(
rename_self_to_param(&sema, position, self_token, "dummy") rename_self_to_param(&sema, position, self_token, "dummy")
} else { } else {
let RangeInfo { range, .. } = find_all_refs(&sema, position)?; let RangeInfo { range, .. } = find_all_refs(&sema, position)?;
Ok(RangeInfo::new(range, SourceChange::from(vec![]))) Ok(RangeInfo::new(range, SourceChange::default()))
} }
.map(|info| RangeInfo::new(info.range, ())) .map(|info| RangeInfo::new(info.range, ()))
} }
@ -176,7 +176,7 @@ fn source_edit_from_references(
file_id: FileId, file_id: FileId,
references: &[FileReference], references: &[FileReference],
new_name: &str, new_name: &str,
) -> SourceFileEdit { ) -> (FileId, TextEdit) {
let mut edit = TextEdit::builder(); let mut edit = TextEdit::builder();
for reference in references { for reference in references {
let mut replacement_text = String::new(); let mut replacement_text = String::new();
@ -209,8 +209,7 @@ fn source_edit_from_references(
}; };
edit.replace(range, replacement_text); edit.replace(range, replacement_text);
} }
(file_id, edit.finish())
SourceFileEdit { file_id, edit: edit.finish() }
} }
fn edit_text_range_for_record_field_expr_or_pat( fn edit_text_range_for_record_field_expr_or_pat(
@ -250,7 +249,7 @@ fn rename_mod(
if IdentifierKind::Ident != check_identifier(new_name)? { if IdentifierKind::Ident != check_identifier(new_name)? {
bail!("Invalid name `{0}`: cannot rename module to {0}", new_name); bail!("Invalid name `{0}`: cannot rename module to {0}", new_name);
} }
let mut source_file_edits = Vec::new(); let mut source_file_edits = SourceFileEdits::default();
let mut file_system_edits = Vec::new(); let mut file_system_edits = Vec::new();
let src = module.definition_source(sema.db); let src = module.definition_source(sema.db);
@ -273,11 +272,8 @@ fn rename_mod(
if let Some(src) = module.declaration_source(sema.db) { if let Some(src) = module.declaration_source(sema.db) {
let file_id = src.file_id.original_file(sema.db); let file_id = src.file_id.original_file(sema.db);
let name = src.value.name().unwrap(); let name = src.value.name().unwrap();
let edit = SourceFileEdit { source_file_edits
file_id, .insert(file_id, TextEdit::replace(name.syntax().text_range(), new_name.into()));
edit: TextEdit::replace(name.syntax().text_range(), new_name.into()),
};
source_file_edits.push(edit);
} }
let RangeInfo { range, info: refs } = find_all_refs(sema, position)?; let RangeInfo { range, info: refs } = find_all_refs(sema, position)?;
@ -335,20 +331,13 @@ fn rename_to_self(
let RangeInfo { range, info: refs } = find_all_refs(sema, position)?; let RangeInfo { range, info: refs } = find_all_refs(sema, position)?;
let mut edits = refs let mut edits = SourceFileEdits::default();
.references() edits.extend(refs.references().iter().map(|(&file_id, references)| {
.iter() source_edit_from_references(sema, file_id, references, "self")
.map(|(&file_id, references)| { }));
source_edit_from_references(sema, file_id, references, "self") edits.insert(position.file_id, TextEdit::replace(param_range, String::from(self_param)));
})
.collect::<Vec<_>>();
edits.push(SourceFileEdit { Ok(RangeInfo::new(range, edits.into()))
file_id: position.file_id,
edit: TextEdit::replace(param_range, String::from(self_param)),
});
Ok(RangeInfo::new(range, SourceChange::from(edits)))
} }
fn text_edit_from_self_param( fn text_edit_from_self_param(
@ -402,7 +391,7 @@ fn rename_self_to_param(
.ok_or_else(|| format_err!("No surrounding method declaration found"))?; .ok_or_else(|| format_err!("No surrounding method declaration found"))?;
let search_range = fn_def.syntax().text_range(); let search_range = fn_def.syntax().text_range();
let mut edits: Vec<SourceFileEdit> = vec![]; let mut edits = SourceFileEdits::default();
for (idx, _) in text.match_indices("self") { for (idx, _) in text.match_indices("self") {
let offset: TextSize = idx.try_into().unwrap(); let offset: TextSize = idx.try_into().unwrap();
@ -416,7 +405,7 @@ fn rename_self_to_param(
} else { } else {
TextEdit::replace(usage.text_range(), String::from(new_name)) TextEdit::replace(usage.text_range(), String::from(new_name))
}; };
edits.push(SourceFileEdit { file_id: position.file_id, edit }); edits.insert(position.file_id, edit);
} }
} }
@ -427,7 +416,7 @@ fn rename_self_to_param(
let range = ast::SelfParam::cast(self_token.parent()) let range = ast::SelfParam::cast(self_token.parent())
.map_or(self_token.text_range(), |p| p.syntax().text_range()); .map_or(self_token.text_range(), |p| p.syntax().text_range());
Ok(RangeInfo::new(range, SourceChange::from(edits))) Ok(RangeInfo::new(range, edits.into()))
} }
fn rename_reference( fn rename_reference(
@ -464,14 +453,12 @@ fn rename_reference(
(IdentifierKind::Ident, _) | (IdentifierKind::Underscore, _) => mark::hit!(rename_ident), (IdentifierKind::Ident, _) | (IdentifierKind::Underscore, _) => mark::hit!(rename_ident),
} }
let edit = refs let mut edits = SourceFileEdits::default();
.into_iter() edits.extend(refs.into_iter().map(|(file_id, references)| {
.map(|(file_id, references)| { source_edit_from_references(sema, file_id, &references, new_name)
source_edit_from_references(sema, file_id, &references, new_name) }));
})
.collect::<Vec<_>>();
Ok(RangeInfo::new(range, SourceChange::from(edit))) Ok(RangeInfo::new(range, edits.into()))
} }
#[cfg(test)] #[cfg(test)]
@ -493,9 +480,9 @@ mod tests {
Ok(source_change) => { Ok(source_change) => {
let mut text_edit_builder = TextEdit::builder(); let mut text_edit_builder = TextEdit::builder();
let mut file_id: Option<FileId> = None; let mut file_id: Option<FileId> = None;
for edit in source_change.info.source_file_edits { for edit in source_change.info.source_file_edits.edits {
file_id = Some(edit.file_id); file_id = Some(edit.0);
for indel in edit.edit.into_iter() { for indel in edit.1.into_iter() {
text_edit_builder.replace(indel.delete, indel.insert); text_edit_builder.replace(indel.delete, indel.insert);
} }
} }
@ -895,12 +882,11 @@ mod foo$0;
RangeInfo { RangeInfo {
range: 4..7, range: 4..7,
info: SourceChange { info: SourceChange {
source_file_edits: [ source_file_edits: SourceFileEdits {
SourceFileEdit { edits: {
file_id: FileId( FileId(
1, 1,
), ): TextEdit {
edit: TextEdit {
indels: [ indels: [
Indel { Indel {
insert: "foo2", insert: "foo2",
@ -909,7 +895,7 @@ mod foo$0;
], ],
}, },
}, },
], },
file_system_edits: [ file_system_edits: [
MoveFile { MoveFile {
src: FileId( src: FileId(
@ -950,12 +936,11 @@ use crate::foo$0::FooContent;
RangeInfo { RangeInfo {
range: 11..14, range: 11..14,
info: SourceChange { info: SourceChange {
source_file_edits: [ source_file_edits: SourceFileEdits {
SourceFileEdit { edits: {
file_id: FileId( FileId(
0, 0,
), ): TextEdit {
edit: TextEdit {
indels: [ indels: [
Indel { Indel {
insert: "quux", insert: "quux",
@ -963,12 +948,9 @@ use crate::foo$0::FooContent;
}, },
], ],
}, },
}, FileId(
SourceFileEdit {
file_id: FileId(
2, 2,
), ): TextEdit {
edit: TextEdit {
indels: [ indels: [
Indel { Indel {
insert: "quux", insert: "quux",
@ -977,7 +959,7 @@ use crate::foo$0::FooContent;
], ],
}, },
}, },
], },
file_system_edits: [ file_system_edits: [
MoveFile { MoveFile {
src: FileId( src: FileId(
@ -1012,12 +994,11 @@ mod fo$0o;
RangeInfo { RangeInfo {
range: 4..7, range: 4..7,
info: SourceChange { info: SourceChange {
source_file_edits: [ source_file_edits: SourceFileEdits {
SourceFileEdit { edits: {
file_id: FileId( FileId(
0, 0,
), ): TextEdit {
edit: TextEdit {
indels: [ indels: [
Indel { Indel {
insert: "foo2", insert: "foo2",
@ -1026,7 +1007,7 @@ mod fo$0o;
], ],
}, },
}, },
], },
file_system_edits: [ file_system_edits: [
MoveFile { MoveFile {
src: FileId( src: FileId(
@ -1062,12 +1043,11 @@ mod outer { mod fo$0o; }
RangeInfo { RangeInfo {
range: 16..19, range: 16..19,
info: SourceChange { info: SourceChange {
source_file_edits: [ source_file_edits: SourceFileEdits {
SourceFileEdit { edits: {
file_id: FileId( FileId(
0, 0,
), ): TextEdit {
edit: TextEdit {
indels: [ indels: [
Indel { Indel {
insert: "bar", insert: "bar",
@ -1076,7 +1056,7 @@ mod outer { mod fo$0o; }
], ],
}, },
}, },
], },
file_system_edits: [ file_system_edits: [
MoveFile { MoveFile {
src: FileId( src: FileId(
@ -1135,12 +1115,21 @@ pub mod foo$0;
RangeInfo { RangeInfo {
range: 8..11, range: 8..11,
info: SourceChange { info: SourceChange {
source_file_edits: [ source_file_edits: SourceFileEdits {
SourceFileEdit { edits: {
file_id: FileId( FileId(
0,
): TextEdit {
indels: [
Indel {
insert: "foo2",
delete: 27..30,
},
],
},
FileId(
1, 1,
), ): TextEdit {
edit: TextEdit {
indels: [ indels: [
Indel { Indel {
insert: "foo2", insert: "foo2",
@ -1149,20 +1138,7 @@ pub mod foo$0;
], ],
}, },
}, },
SourceFileEdit { },
file_id: FileId(
0,
),
edit: TextEdit {
indels: [
Indel {
insert: "foo2",
delete: 27..30,
},
],
},
},
],
file_system_edits: [ file_system_edits: [
MoveFile { MoveFile {
src: FileId( src: FileId(

View file

@ -15,8 +15,11 @@
mod on_enter; mod on_enter;
use ide_db::base_db::{FilePosition, SourceDatabase}; use ide_db::{
use ide_db::{source_change::SourceFileEdit, RootDatabase}; base_db::{FilePosition, SourceDatabase},
source_change::SourceFileEdits,
RootDatabase,
};
use syntax::{ use syntax::{
algo::find_node_at_offset, algo::find_node_at_offset,
ast::{self, edit::IndentLevel, AstToken}, ast::{self, edit::IndentLevel, AstToken},
@ -56,7 +59,7 @@ pub(crate) fn on_char_typed(
let file = &db.parse(position.file_id).tree(); let file = &db.parse(position.file_id).tree();
assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed)); assert_eq!(file.syntax().text().char_at(position.offset), Some(char_typed));
let edit = on_char_typed_inner(file, position.offset, char_typed)?; let edit = on_char_typed_inner(file, position.offset, char_typed)?;
Some(SourceFileEdit { file_id: position.file_id, edit }.into()) Some(SourceFileEdits::from_text_edit(position.file_id, edit).into())
} }
fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> { fn on_char_typed_inner(file: &SourceFile, offset: TextSize, char_typed: char) -> Option<TextEdit> {

View file

@ -3,12 +3,18 @@
//! //!
//! It can be viewed as a dual for `AnalysisChange`. //! It can be viewed as a dual for `AnalysisChange`.
use std::{
collections::hash_map::Entry,
iter::{self, FromIterator},
};
use base_db::{AnchoredPathBuf, FileId}; use base_db::{AnchoredPathBuf, FileId};
use rustc_hash::FxHashMap;
use text_edit::TextEdit; use text_edit::TextEdit;
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
pub struct SourceChange { pub struct SourceChange {
pub source_file_edits: Vec<SourceFileEdit>, pub source_file_edits: SourceFileEdits,
pub file_system_edits: Vec<FileSystemEdit>, pub file_system_edits: Vec<FileSystemEdit>,
pub is_snippet: bool, pub is_snippet: bool,
} }
@ -17,27 +23,51 @@ impl SourceChange {
/// Creates a new SourceChange with the given label /// Creates a new SourceChange with the given label
/// from the edits. /// from the edits.
pub fn from_edits( pub fn from_edits(
source_file_edits: Vec<SourceFileEdit>, source_file_edits: SourceFileEdits,
file_system_edits: Vec<FileSystemEdit>, file_system_edits: Vec<FileSystemEdit>,
) -> Self { ) -> Self {
SourceChange { source_file_edits, file_system_edits, is_snippet: false } SourceChange { source_file_edits, file_system_edits, is_snippet: false }
} }
} }
#[derive(Debug, Clone)] #[derive(Default, Debug, Clone)]
pub struct SourceFileEdit { pub struct SourceFileEdits {
pub file_id: FileId, pub edits: FxHashMap<FileId, TextEdit>,
pub edit: TextEdit,
} }
impl From<SourceFileEdit> for SourceChange { impl SourceFileEdits {
fn from(edit: SourceFileEdit) -> SourceChange { pub fn from_text_edit(file_id: FileId, edit: TextEdit) -> Self {
vec![edit].into() SourceFileEdits { edits: FxHashMap::from_iter(iter::once((file_id, edit))) }
}
pub fn len(&self) -> usize {
self.edits.len()
}
pub fn is_empty(&self) -> bool {
self.edits.is_empty()
}
pub fn insert(&mut self, file_id: FileId, edit: TextEdit) {
match self.edits.entry(file_id) {
Entry::Occupied(mut entry) => {
entry.get_mut().union(edit).expect("overlapping edits for same file");
}
Entry::Vacant(entry) => {
entry.insert(edit);
}
}
} }
} }
impl From<Vec<SourceFileEdit>> for SourceChange { impl Extend<(FileId, TextEdit)> for SourceFileEdits {
fn from(source_file_edits: Vec<SourceFileEdit>) -> SourceChange { fn extend<T: IntoIterator<Item = (FileId, TextEdit)>>(&mut self, iter: T) {
iter.into_iter().for_each(|(file_id, edit)| self.insert(file_id, edit));
}
}
impl From<SourceFileEdits> for SourceChange {
fn from(source_file_edits: SourceFileEdits) -> SourceChange {
SourceChange { source_file_edits, file_system_edits: Vec::new(), is_snippet: false } SourceChange { source_file_edits, file_system_edits: Vec::new(), is_snippet: false }
} }
} }
@ -51,7 +81,7 @@ pub enum FileSystemEdit {
impl From<FileSystemEdit> for SourceChange { impl From<FileSystemEdit> for SourceChange {
fn from(edit: FileSystemEdit) -> SourceChange { fn from(edit: FileSystemEdit) -> SourceChange {
SourceChange { SourceChange {
source_file_edits: Vec::new(), source_file_edits: Default::default(),
file_system_edits: vec![edit], file_system_edits: vec![edit],
is_snippet: false, is_snippet: false,
} }

View file

@ -12,10 +12,10 @@ pub fn apply_ssr_rules(rules: Vec<SsrRule>) -> Result<()> {
match_finder.add_rule(rule)?; match_finder.add_rule(rule)?;
} }
let edits = match_finder.edits(); let edits = match_finder.edits();
for edit in edits { for (file_id, edit) in edits.edits {
if let Some(path) = vfs.file_path(edit.file_id).as_path() { if let Some(path) = vfs.file_path(file_id).as_path() {
let mut contents = db.file_text(edit.file_id).to_string(); let mut contents = db.file_text(file_id).to_string();
edit.edit.apply(&mut contents); edit.apply(&mut contents);
std::fs::write(path, contents)?; std::fs::write(path, contents)?;
} }
} }

View file

@ -260,15 +260,15 @@ pub(crate) fn handle_on_type_formatting(
} }
let edit = snap.analysis.on_char_typed(position, char_typed)?; let edit = snap.analysis.on_char_typed(position, char_typed)?;
let mut edit = match edit { let edit = match edit {
Some(it) => it, Some(it) => it,
None => return Ok(None), None => return Ok(None),
}; };
// This should be a single-file edit // This should be a single-file edit
let edit = edit.source_file_edits.pop().unwrap(); let (_, edit) = edit.source_file_edits.edits.into_iter().next().unwrap();
let change = to_proto::text_edit_vec(&line_index, line_endings, edit.edit); let change = to_proto::text_edit_vec(&line_index, line_endings, edit);
Ok(Some(change)) Ok(Some(change))
} }
@ -463,8 +463,12 @@ pub(crate) fn handle_will_rename_files(
.collect(); .collect();
// Drop file system edits since we're just renaming things on the same level // Drop file system edits since we're just renaming things on the same level
let edits = source_changes.into_iter().map(|it| it.source_file_edits).flatten().collect(); let mut source_changes = source_changes.into_iter();
let source_change = SourceChange::from_edits(edits, Vec::new()); let mut source_file_edits =
source_changes.next().map_or_else(Default::default, |it| it.source_file_edits);
// no collect here because we want to merge text edits on same file ids
source_file_edits.extend(source_changes.map(|it| it.source_file_edits.edits).flatten());
let source_change = SourceChange::from_edits(source_file_edits, Vec::new());
let workspace_edit = to_proto::workspace_edit(&snap, source_change)?; let workspace_edit = to_proto::workspace_edit(&snap, source_change)?;
Ok(Some(workspace_edit)) Ok(Some(workspace_edit))

View file

@ -8,8 +8,7 @@ use ide::{
Assist, AssistKind, CallInfo, CompletionItem, CompletionItemKind, Documentation, FileId, Assist, AssistKind, CallInfo, CompletionItem, CompletionItemKind, Documentation, FileId,
FileRange, FileSystemEdit, Fold, FoldKind, Highlight, HlMod, HlPunct, HlRange, HlTag, Indel, FileRange, FileSystemEdit, Fold, FoldKind, Highlight, HlMod, HlPunct, HlRange, HlTag, Indel,
InlayHint, InlayKind, InsertTextFormat, LineIndex, Markup, NavigationTarget, ReferenceAccess, InlayHint, InlayKind, InsertTextFormat, LineIndex, Markup, NavigationTarget, ReferenceAccess,
RenameError, Runnable, Severity, SourceChange, SourceFileEdit, SymbolKind, TextEdit, TextRange, RenameError, Runnable, Severity, SourceChange, SymbolKind, TextEdit, TextRange, TextSize,
TextSize,
}; };
use itertools::Itertools; use itertools::Itertools;
@ -634,13 +633,13 @@ pub(crate) fn goto_definition_response(
pub(crate) fn snippet_text_document_edit( pub(crate) fn snippet_text_document_edit(
snap: &GlobalStateSnapshot, snap: &GlobalStateSnapshot,
is_snippet: bool, is_snippet: bool,
source_file_edit: SourceFileEdit, file_id: FileId,
edit: TextEdit,
) -> Result<lsp_ext::SnippetTextDocumentEdit> { ) -> Result<lsp_ext::SnippetTextDocumentEdit> {
let text_document = optional_versioned_text_document_identifier(snap, source_file_edit.file_id); let text_document = optional_versioned_text_document_identifier(snap, file_id);
let line_index = snap.analysis.file_line_index(source_file_edit.file_id)?; let line_index = snap.analysis.file_line_index(file_id)?;
let line_endings = snap.file_line_endings(source_file_edit.file_id); let line_endings = snap.file_line_endings(file_id);
let edits = source_file_edit let edits = edit
.edit
.into_iter() .into_iter()
.map(|it| snippet_text_edit(&line_index, line_endings, is_snippet, it)) .map(|it| snippet_text_edit(&line_index, line_endings, is_snippet, it))
.collect(); .collect();
@ -699,8 +698,8 @@ pub(crate) fn snippet_workspace_edit(
let ops = snippet_text_document_ops(snap, op); let ops = snippet_text_document_ops(snap, op);
document_changes.extend_from_slice(&ops); document_changes.extend_from_slice(&ops);
} }
for edit in source_change.source_file_edits { for (file_id, edit) in source_change.source_file_edits.edits {
let edit = snippet_text_document_edit(&snap, source_change.is_snippet, edit)?; let edit = snippet_text_document_edit(&snap, source_change.is_snippet, file_id, edit)?;
document_changes.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit)); document_changes.push(lsp_ext::SnippetDocumentChangeOperation::Edit(edit));
} }
let workspace_edit = let workspace_edit =

View file

@ -74,8 +74,10 @@ pub use crate::errors::SsrError;
pub use crate::matching::Match; pub use crate::matching::Match;
use crate::matching::MatchFailureReason; use crate::matching::MatchFailureReason;
use hir::Semantics; use hir::Semantics;
use ide_db::base_db::{FileId, FilePosition, FileRange}; use ide_db::{
use ide_db::source_change::SourceFileEdit; base_db::{FileId, FilePosition, FileRange},
source_change::SourceFileEdits,
};
use resolving::ResolvedRule; use resolving::ResolvedRule;
use rustc_hash::FxHashMap; use rustc_hash::FxHashMap;
use syntax::{ast, AstNode, SyntaxNode, TextRange}; use syntax::{ast, AstNode, SyntaxNode, TextRange};
@ -159,7 +161,7 @@ impl<'db> MatchFinder<'db> {
} }
/// Finds matches for all added rules and returns edits for all found matches. /// Finds matches for all added rules and returns edits for all found matches.
pub fn edits(&self) -> Vec<SourceFileEdit> { pub fn edits(&self) -> SourceFileEdits {
use ide_db::base_db::SourceDatabaseExt; use ide_db::base_db::SourceDatabaseExt;
let mut matches_by_file = FxHashMap::default(); let mut matches_by_file = FxHashMap::default();
for m in self.matches().matches { for m in self.matches().matches {
@ -169,13 +171,21 @@ impl<'db> MatchFinder<'db> {
.matches .matches
.push(m); .push(m);
} }
let mut edits = vec![]; SourceFileEdits {
for (file_id, matches) in matches_by_file { edits: matches_by_file
let edit = .into_iter()
replacing::matches_to_edit(&matches, &self.sema.db.file_text(file_id), &self.rules); .map(|(file_id, matches)| {
edits.push(SourceFileEdit { file_id, edit }); (
file_id,
replacing::matches_to_edit(
&matches,
&self.sema.db.file_text(file_id),
&self.rules,
),
)
})
.collect(),
} }
edits
} }
/// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you /// Adds a search pattern. For use if you intend to only call `find_matches_in_file`. If you

View file

@ -810,9 +810,9 @@ mod tests {
let edits = match_finder.edits(); let edits = match_finder.edits();
assert_eq!(edits.len(), 1); assert_eq!(edits.len(), 1);
let edit = &edits[0]; let edit = &edits.edits[&position.file_id];
let mut after = input.to_string(); let mut after = input.to_string();
edit.edit.apply(&mut after); edit.apply(&mut after);
assert_eq!(after, "fn foo() {} fn bar() {} fn main() { bar(1+2); }"); assert_eq!(after, "fn foo() {} fn bar() {} fn main() { bar(1+2); }");
} }
} }

View file

@ -103,11 +103,10 @@ fn assert_ssr_transforms(rules: &[&str], input: &str, expected: Expect) {
if edits.is_empty() { if edits.is_empty() {
panic!("No edits were made"); panic!("No edits were made");
} }
assert_eq!(edits[0].file_id, position.file_id);
// Note, db.file_text is not necessarily the same as `input`, since fixture parsing alters // Note, db.file_text is not necessarily the same as `input`, since fixture parsing alters
// stuff. // stuff.
let mut actual = db.file_text(position.file_id).to_string(); let mut actual = db.file_text(position.file_id).to_string();
edits[0].edit.apply(&mut actual); edits.edits[&position.file_id].apply(&mut actual);
expected.assert_eq(&actual); expected.assert_eq(&actual);
} }