aboutsummaryrefslogtreecommitdiff
path: root/crates/ra_ide/src
diff options
context:
space:
mode:
authorbors[bot] <26634292+bors[bot]@users.noreply.github.com>2020-08-12 14:44:13 +0100
committerGitHub <[email protected]>2020-08-12 14:44:13 +0100
commit5b8fdfe23100b88e4fd8e210ccf6b852f5c9bf2a (patch)
treee321c900fe4997ec5ffe20cbb09946502745849c /crates/ra_ide/src
parent11de7ac2fb6514484076217acb8d93eb36468681 (diff)
parentdb12ccee96bf37367b39ad99638d06da7123c088 (diff)
Merge #5553
5553: Add fix ranges for diagnostics r=matklad a=SomeoneToIgnore A follow-up of https://rust-lang.zulipchat.com/#narrow/stream/185405-t-compiler.2Fwg-rls-2.2E0/topic/Less.20red.20in.20the.20code Now diagnostics can apply fixes in a range that's different from the range used to highlight the diagnostics. Previous logic did not consider the fix range, having both ranges equal, which could cause a lot of red noise in the editor. Now, the fix range gets used with the fix, the diagnostics range is used for everything else which allows to improve the error highlighting. before: <img width="191" alt="image" src="https://user-images.githubusercontent.com/2690773/88590727-df9a6a00-d063-11ea-97ed-9809c1c5e6e6.png"> after: <img width="222" alt="image" src="https://user-images.githubusercontent.com/2690773/88590734-e1fcc400-d063-11ea-9b7c-25701cbd5352.png"> `MissingFields` and `MissingPatFields` diagnostics now have the fix range as `ast::RecordFieldList` of the expression with an error (as it was before this PR), and the diagnostics range as a `ast::Path` of the expression, if it's present (do you have any example of `ast::Expr::RecordLit` that has no path btw?). The rest of the diagnostics have both ranges equal, same as it was before this PR. Co-authored-by: Kirill Bulatov <[email protected]>
Diffstat (limited to 'crates/ra_ide/src')
-rw-r--r--crates/ra_ide/src/diagnostics.rs184
-rw-r--r--crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs171
-rw-r--r--crates/ra_ide/src/lib.rs10
3 files changed, 211 insertions, 154 deletions
diff --git a/crates/ra_ide/src/diagnostics.rs b/crates/ra_ide/src/diagnostics.rs
index 73c0b8275..1046d7ab3 100644
--- a/crates/ra_ide/src/diagnostics.rs
+++ b/crates/ra_ide/src/diagnostics.rs
@@ -6,22 +6,21 @@
6 6
7use std::cell::RefCell; 7use std::cell::RefCell;
8 8
9use hir::{ 9use hir::{diagnostics::DiagnosticSinkBuilder, Semantics};
10 diagnostics::{AstDiagnostic, Diagnostic as _, DiagnosticSinkBuilder},
11 HasSource, HirDisplay, Semantics, VariantDef,
12};
13use itertools::Itertools; 10use itertools::Itertools;
14use ra_db::SourceDatabase; 11use ra_db::SourceDatabase;
15use ra_ide_db::RootDatabase; 12use ra_ide_db::RootDatabase;
16use ra_prof::profile; 13use ra_prof::profile;
17use ra_syntax::{ 14use ra_syntax::{
18 algo, 15 ast::{self, AstNode},
19 ast::{self, edit::IndentLevel, make, AstNode},
20 SyntaxNode, TextRange, T, 16 SyntaxNode, TextRange, T,
21}; 17};
22use ra_text_edit::{TextEdit, TextEditBuilder}; 18use ra_text_edit::{TextEdit, TextEditBuilder};
23 19
24use crate::{Diagnostic, FileId, FileSystemEdit, Fix, SourceFileEdit}; 20use crate::{Diagnostic, FileId, Fix, SourceFileEdit};
21
22mod diagnostics_with_fix;
23use diagnostics_with_fix::DiagnosticWithFix;
25 24
26#[derive(Debug, Copy, Clone)] 25#[derive(Debug, Copy, Clone)]
27pub enum Severity { 26pub enum Severity {
@@ -54,73 +53,16 @@ pub(crate) fn diagnostics(
54 let res = RefCell::new(res); 53 let res = RefCell::new(res);
55 let mut sink = DiagnosticSinkBuilder::new() 54 let mut sink = DiagnosticSinkBuilder::new()
56 .on::<hir::diagnostics::UnresolvedModule, _>(|d| { 55 .on::<hir::diagnostics::UnresolvedModule, _>(|d| {
57 let original_file = d.source().file_id.original_file(db); 56 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
58 let fix = Fix::new(
59 "Create module",
60 FileSystemEdit::CreateFile { anchor: original_file, dst: d.candidate.clone() }
61 .into(),
62 );
63 res.borrow_mut().push(Diagnostic {
64 range: sema.diagnostics_range(d).range,
65 message: d.message(),
66 severity: Severity::Error,
67 fix: Some(fix),
68 })
69 }) 57 })
70 .on::<hir::diagnostics::MissingFields, _>(|d| { 58 .on::<hir::diagnostics::MissingFields, _>(|d| {
71 // Note that although we could add a diagnostics to 59 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
72 // fill the missing tuple field, e.g :
73 // `struct A(usize);`
74 // `let a = A { 0: () }`
75 // but it is uncommon usage and it should not be encouraged.
76 let fix = if d.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) {
77 None
78 } else {
79 let mut field_list = d.ast(db);
80 for f in d.missed_fields.iter() {
81 let field = make::record_expr_field(
82 make::name_ref(&f.to_string()),
83 Some(make::expr_unit()),
84 );
85 field_list = field_list.append_field(&field);
86 }
87
88 let edit = {
89 let mut builder = TextEditBuilder::default();
90 algo::diff(&d.ast(db).syntax(), &field_list.syntax())
91 .into_text_edit(&mut builder);
92 builder.finish()
93 };
94 Some(Fix::new("Fill struct fields", SourceFileEdit { file_id, edit }.into()))
95 };
96
97 res.borrow_mut().push(Diagnostic {
98 range: sema.diagnostics_range(d).range,
99 message: d.message(),
100 severity: Severity::Error,
101 fix,
102 })
103 }) 60 })
104 .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| { 61 .on::<hir::diagnostics::MissingOkInTailExpr, _>(|d| {
105 let node = d.ast(db); 62 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
106 let replacement = format!("Ok({})", node.syntax());
107 let edit = TextEdit::replace(node.syntax().text_range(), replacement);
108 let source_change = SourceFileEdit { file_id, edit }.into();
109 let fix = Fix::new("Wrap with ok", source_change);
110 res.borrow_mut().push(Diagnostic {
111 range: sema.diagnostics_range(d).range,
112 message: d.message(),
113 severity: Severity::Error,
114 fix: Some(fix),
115 })
116 }) 63 })
117 .on::<hir::diagnostics::NoSuchField, _>(|d| { 64 .on::<hir::diagnostics::NoSuchField, _>(|d| {
118 res.borrow_mut().push(Diagnostic { 65 res.borrow_mut().push(diagnostic_with_fix(d, &sema));
119 range: sema.diagnostics_range(d).range,
120 message: d.message(),
121 severity: Severity::Error,
122 fix: missing_struct_field_fix(&sema, file_id, d),
123 })
124 }) 66 })
125 // Only collect experimental diagnostics when they're enabled. 67 // Only collect experimental diagnostics when they're enabled.
126 .filter(|diag| !diag.is_experimental() || enable_experimental) 68 .filter(|diag| !diag.is_experimental() || enable_experimental)
@@ -128,7 +70,7 @@ pub(crate) fn diagnostics(
128 .build(|d| { 70 .build(|d| {
129 res.borrow_mut().push(Diagnostic { 71 res.borrow_mut().push(Diagnostic {
130 message: d.message(), 72 message: d.message(),
131 range: sema.diagnostics_range(d).range, 73 range: sema.diagnostics_display_range(d).range,
132 severity: Severity::Error, 74 severity: Severity::Error,
133 fix: None, 75 fix: None,
134 }) 76 })
@@ -141,77 +83,12 @@ pub(crate) fn diagnostics(
141 res.into_inner() 83 res.into_inner()
142} 84}
143 85
144fn missing_struct_field_fix( 86fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic {
145 sema: &Semantics<RootDatabase>, 87 Diagnostic {
146 usage_file_id: FileId, 88 range: sema.diagnostics_display_range(d).range,
147 d: &hir::diagnostics::NoSuchField, 89 message: d.message(),
148) -> Option<Fix> { 90 severity: Severity::Error,
149 let record_expr = sema.ast(d); 91 fix: d.fix(&sema),
150
151 let record_lit = ast::RecordExpr::cast(record_expr.syntax().parent()?.parent()?)?;
152 let def_id = sema.resolve_variant(record_lit)?;
153 let module;
154 let def_file_id;
155 let record_fields = match VariantDef::from(def_id) {
156 VariantDef::Struct(s) => {
157 module = s.module(sema.db);
158 let source = s.source(sema.db);
159 def_file_id = source.file_id;
160 let fields = source.value.field_list()?;
161 record_field_list(fields)?
162 }
163 VariantDef::Union(u) => {
164 module = u.module(sema.db);
165 let source = u.source(sema.db);
166 def_file_id = source.file_id;
167 source.value.record_field_list()?
168 }
169 VariantDef::EnumVariant(e) => {
170 module = e.module(sema.db);
171 let source = e.source(sema.db);
172 def_file_id = source.file_id;
173 let fields = source.value.field_list()?;
174 record_field_list(fields)?
175 }
176 };
177 let def_file_id = def_file_id.original_file(sema.db);
178
179 let new_field_type = sema.type_of_expr(&record_expr.expr()?)?;
180 if new_field_type.is_unknown() {
181 return None;
182 }
183 let new_field = make::record_field(
184 record_expr.field_name()?,
185 make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
186 );
187
188 let last_field = record_fields.fields().last()?;
189 let last_field_syntax = last_field.syntax();
190 let indent = IndentLevel::from_node(last_field_syntax);
191
192 let mut new_field = new_field.to_string();
193 if usage_file_id != def_file_id {
194 new_field = format!("pub(crate) {}", new_field);
195 }
196 new_field = format!("\n{}{}", indent, new_field);
197
198 let needs_comma = !last_field_syntax.to_string().ends_with(',');
199 if needs_comma {
200 new_field = format!(",{}", new_field);
201 }
202
203 let source_change = SourceFileEdit {
204 file_id: def_file_id,
205 edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field),
206 };
207 let fix = Fix::new("Create field", source_change.into());
208 return Some(fix);
209
210 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
211 match field_def_list {
212 ast::FieldList::RecordFieldList(it) => Some(it),
213 ast::FieldList::TupleFieldList(_) => None,
214 }
215 } 92 }
216} 93}
217 94
@@ -222,24 +99,25 @@ fn check_unnecessary_braces_in_use_statement(
222) -> Option<()> { 99) -> Option<()> {
223 let use_tree_list = ast::UseTreeList::cast(node.clone())?; 100 let use_tree_list = ast::UseTreeList::cast(node.clone())?;
224 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() { 101 if let Some((single_use_tree,)) = use_tree_list.use_trees().collect_tuple() {
225 let range = use_tree_list.syntax().text_range(); 102 let use_range = use_tree_list.syntax().text_range();
226 let edit = 103 let edit =
227 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree) 104 text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(&single_use_tree)
228 .unwrap_or_else(|| { 105 .unwrap_or_else(|| {
229 let to_replace = single_use_tree.syntax().text().to_string(); 106 let to_replace = single_use_tree.syntax().text().to_string();
230 let mut edit_builder = TextEditBuilder::default(); 107 let mut edit_builder = TextEditBuilder::default();
231 edit_builder.delete(range); 108 edit_builder.delete(use_range);
232 edit_builder.insert(range.start(), to_replace); 109 edit_builder.insert(use_range.start(), to_replace);
233 edit_builder.finish() 110 edit_builder.finish()
234 }); 111 });
235 112
236 acc.push(Diagnostic { 113 acc.push(Diagnostic {
237 range, 114 range: use_range,
238 message: "Unnecessary braces in use statement".to_string(), 115 message: "Unnecessary braces in use statement".to_string(),
239 severity: Severity::WeakWarning, 116 severity: Severity::WeakWarning,
240 fix: Some(Fix::new( 117 fix: Some(Fix::new(
241 "Remove unnecessary braces", 118 "Remove unnecessary braces",
242 SourceFileEdit { file_id, edit }.into(), 119 SourceFileEdit { file_id, edit }.into(),
120 use_range,
243 )), 121 )),
244 }); 122 });
245 } 123 }
@@ -254,8 +132,7 @@ fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
254 if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] { 132 if single_use_tree.path()?.segment()?.syntax().first_child_or_token()?.kind() == T![self] {
255 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start(); 133 let start = use_tree_list_node.prev_sibling_or_token()?.text_range().start();
256 let end = use_tree_list_node.text_range().end(); 134 let end = use_tree_list_node.text_range().end();
257 let range = TextRange::new(start, end); 135 return Some(TextEdit::delete(TextRange::new(start, end)));
258 return Some(TextEdit::delete(range));
259 } 136 }
260 None 137 None
261} 138}
@@ -278,13 +155,15 @@ fn check_struct_shorthand_initialization(
278 edit_builder.insert(record_field.syntax().text_range().start(), field_name); 155 edit_builder.insert(record_field.syntax().text_range().start(), field_name);
279 let edit = edit_builder.finish(); 156 let edit = edit_builder.finish();
280 157
158 let field_range = record_field.syntax().text_range();
281 acc.push(Diagnostic { 159 acc.push(Diagnostic {
282 range: record_field.syntax().text_range(), 160 range: field_range,
283 message: "Shorthand struct initialization".to_string(), 161 message: "Shorthand struct initialization".to_string(),
284 severity: Severity::WeakWarning, 162 severity: Severity::WeakWarning,
285 fix: Some(Fix::new( 163 fix: Some(Fix::new(
286 "Use struct shorthand initialization", 164 "Use struct shorthand initialization",
287 SourceFileEdit { file_id, edit }.into(), 165 SourceFileEdit { file_id, edit }.into(),
166 field_range,
288 )), 167 )),
289 }); 168 });
290 } 169 }
@@ -304,7 +183,7 @@ mod tests {
304 /// Takes a multi-file input fixture with annotated cursor positions, 183 /// Takes a multi-file input fixture with annotated cursor positions,
305 /// and checks that: 184 /// and checks that:
306 /// * a diagnostic is produced 185 /// * a diagnostic is produced
307 /// * this diagnostic touches the input cursor position 186 /// * this diagnostic fix trigger range touches the input cursor position
308 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied 187 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
309 fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) { 188 fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
310 let after = trim_indent(ra_fixture_after); 189 let after = trim_indent(ra_fixture_after);
@@ -322,10 +201,10 @@ mod tests {
322 201
323 assert_eq_text!(&after, &actual); 202 assert_eq_text!(&after, &actual);
324 assert!( 203 assert!(
325 diagnostic.range.start() <= file_position.offset 204 fix.fix_trigger_range.start() <= file_position.offset
326 && diagnostic.range.end() >= file_position.offset, 205 && fix.fix_trigger_range.end() >= file_position.offset,
327 "diagnostic range {:?} does not touch cursor position {:?}", 206 "diagnostic fix range {:?} does not touch cursor position {:?}",
328 diagnostic.range, 207 fix.fix_trigger_range,
329 file_position.offset 208 file_position.offset
330 ); 209 );
331 } 210 }
@@ -642,6 +521,7 @@ fn test_fn() {
642 ], 521 ],
643 is_snippet: false, 522 is_snippet: false,
644 }, 523 },
524 fix_trigger_range: 0..8,
645 }, 525 },
646 ), 526 ),
647 }, 527 },
diff --git a/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs b/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs
new file mode 100644
index 000000000..f7c73773f
--- /dev/null
+++ b/crates/ra_ide/src/diagnostics/diagnostics_with_fix.rs
@@ -0,0 +1,171 @@
1//! Provides a way to attach fixes to the diagnostics.
2//! The same module also has all curret custom fixes for the diagnostics implemented.
3use crate::Fix;
4use ast::{edit::IndentLevel, make};
5use hir::{
6 db::AstDatabase,
7 diagnostics::{Diagnostic, MissingFields, MissingOkInTailExpr, NoSuchField, UnresolvedModule},
8 HasSource, HirDisplay, Semantics, VariantDef,
9};
10use ra_db::FileId;
11use ra_ide_db::{
12 source_change::{FileSystemEdit, SourceFileEdit},
13 RootDatabase,
14};
15use ra_syntax::{algo, ast, AstNode};
16use ra_text_edit::{TextEdit, TextEditBuilder};
17
18/// A [Diagnostic] that potentially has a fix available.
19///
20/// [Diagnostic]: hir::diagnostics::Diagnostic
21pub trait DiagnosticWithFix: Diagnostic {
22 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix>;
23}
24
25impl DiagnosticWithFix for UnresolvedModule {
26 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> {
27 let root = sema.db.parse_or_expand(self.file)?;
28 let unresolved_module = self.decl.to_node(&root);
29 Some(Fix::new(
30 "Create module",
31 FileSystemEdit::CreateFile {
32 anchor: self.file.original_file(sema.db),
33 dst: self.candidate.clone(),
34 }
35 .into(),
36 unresolved_module.syntax().text_range(),
37 ))
38 }
39}
40
41impl DiagnosticWithFix for NoSuchField {
42 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> {
43 let root = sema.db.parse_or_expand(self.file)?;
44 missing_record_expr_field_fix(
45 &sema,
46 self.file.original_file(sema.db),
47 &self.field.to_node(&root),
48 )
49 }
50}
51
52impl DiagnosticWithFix for MissingFields {
53 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> {
54 // Note that although we could add a diagnostics to
55 // fill the missing tuple field, e.g :
56 // `struct A(usize);`
57 // `let a = A { 0: () }`
58 // but it is uncommon usage and it should not be encouraged.
59 if self.missed_fields.iter().any(|it| it.as_tuple_index().is_some()) {
60 return None;
61 }
62
63 let root = sema.db.parse_or_expand(self.file)?;
64 let old_field_list = self.field_list_parent.to_node(&root).record_expr_field_list()?;
65 let mut new_field_list = old_field_list.clone();
66 for f in self.missed_fields.iter() {
67 let field =
68 make::record_expr_field(make::name_ref(&f.to_string()), Some(make::expr_unit()));
69 new_field_list = new_field_list.append_field(&field);
70 }
71
72 let edit = {
73 let mut builder = TextEditBuilder::default();
74 algo::diff(&old_field_list.syntax(), &new_field_list.syntax())
75 .into_text_edit(&mut builder);
76 builder.finish()
77 };
78 Some(Fix::new(
79 "Fill struct fields",
80 SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into(),
81 sema.original_range(&old_field_list.syntax()).range,
82 ))
83 }
84}
85
86impl DiagnosticWithFix for MissingOkInTailExpr {
87 fn fix(&self, sema: &Semantics<RootDatabase>) -> Option<Fix> {
88 let root = sema.db.parse_or_expand(self.file)?;
89 let tail_expr = self.expr.to_node(&root);
90 let tail_expr_range = tail_expr.syntax().text_range();
91 let edit = TextEdit::replace(tail_expr_range, format!("Ok({})", tail_expr.syntax()));
92 let source_change =
93 SourceFileEdit { file_id: self.file.original_file(sema.db), edit }.into();
94 Some(Fix::new("Wrap with ok", source_change, tail_expr_range))
95 }
96}
97
98fn missing_record_expr_field_fix(
99 sema: &Semantics<RootDatabase>,
100 usage_file_id: FileId,
101 record_expr_field: &ast::RecordExprField,
102) -> Option<Fix> {
103 let record_lit = ast::RecordExpr::cast(record_expr_field.syntax().parent()?.parent()?)?;
104 let def_id = sema.resolve_variant(record_lit)?;
105 let module;
106 let def_file_id;
107 let record_fields = match VariantDef::from(def_id) {
108 VariantDef::Struct(s) => {
109 module = s.module(sema.db);
110 let source = s.source(sema.db);
111 def_file_id = source.file_id;
112 let fields = source.value.field_list()?;
113 record_field_list(fields)?
114 }
115 VariantDef::Union(u) => {
116 module = u.module(sema.db);
117 let source = u.source(sema.db);
118 def_file_id = source.file_id;
119 source.value.record_field_list()?
120 }
121 VariantDef::EnumVariant(e) => {
122 module = e.module(sema.db);
123 let source = e.source(sema.db);
124 def_file_id = source.file_id;
125 let fields = source.value.field_list()?;
126 record_field_list(fields)?
127 }
128 };
129 let def_file_id = def_file_id.original_file(sema.db);
130
131 let new_field_type = sema.type_of_expr(&record_expr_field.expr()?)?;
132 if new_field_type.is_unknown() {
133 return None;
134 }
135 let new_field = make::record_field(
136 record_expr_field.field_name()?,
137 make::ty(&new_field_type.display_source_code(sema.db, module.into()).ok()?),
138 );
139
140 let last_field = record_fields.fields().last()?;
141 let last_field_syntax = last_field.syntax();
142 let indent = IndentLevel::from_node(last_field_syntax);
143
144 let mut new_field = new_field.to_string();
145 if usage_file_id != def_file_id {
146 new_field = format!("pub(crate) {}", new_field);
147 }
148 new_field = format!("\n{}{}", indent, new_field);
149
150 let needs_comma = !last_field_syntax.to_string().ends_with(',');
151 if needs_comma {
152 new_field = format!(",{}", new_field);
153 }
154
155 let source_change = SourceFileEdit {
156 file_id: def_file_id,
157 edit: TextEdit::insert(last_field_syntax.text_range().end(), new_field),
158 };
159 return Some(Fix::new(
160 "Create field",
161 source_change.into(),
162 record_expr_field.syntax().text_range(),
163 ));
164
165 fn record_field_list(field_def_list: ast::FieldList) -> Option<ast::RecordFieldList> {
166 match field_def_list {
167 ast::FieldList::RecordFieldList(it) => Some(it),
168 ast::FieldList::TupleFieldList(_) => None,
169 }
170 }
171}
diff --git a/crates/ra_ide/src/lib.rs b/crates/ra_ide/src/lib.rs
index 0fede0d87..89fcb6f17 100644
--- a/crates/ra_ide/src/lib.rs
+++ b/crates/ra_ide/src/lib.rs
@@ -112,13 +112,19 @@ pub struct Diagnostic {
112pub struct Fix { 112pub struct Fix {
113 pub label: String, 113 pub label: String,
114 pub source_change: SourceChange, 114 pub source_change: SourceChange,
115 /// Allows to trigger the fix only when the caret is in the range given
116 pub fix_trigger_range: TextRange,
115} 117}
116 118
117impl Fix { 119impl Fix {
118 pub fn new(label: impl Into<String>, source_change: SourceChange) -> Self { 120 pub fn new(
121 label: impl Into<String>,
122 source_change: SourceChange,
123 fix_trigger_range: TextRange,
124 ) -> Self {
119 let label = label.into(); 125 let label = label.into();
120 assert!(label.starts_with(char::is_uppercase) && !label.ends_with('.')); 126 assert!(label.starts_with(char::is_uppercase) && !label.ends_with('.'));
121 Self { label, source_change } 127 Self { label, source_change, fix_trigger_range }
122 } 128 }
123} 129}
124 130