aboutsummaryrefslogtreecommitdiff
path: root/crates/ide/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/ide/src')
-rw-r--r--crates/ide/src/completion.rs2
-rw-r--r--crates/ide/src/diagnostics.rs95
-rw-r--r--crates/ide/src/diagnostics/field_shorthand.rs206
-rw-r--r--crates/ide/src/doc_links.rs31
-rw-r--r--crates/ide/src/goto_definition.rs8
-rw-r--r--crates/ide/src/hover.rs29
-rw-r--r--crates/ide/src/references.rs6
-rw-r--r--crates/ide/src/references/rename.rs6
-rw-r--r--crates/ide/src/syntax_highlighting.rs224
-rw-r--r--crates/ide/src/syntax_highlighting/format.rs78
-rw-r--r--crates/ide/src/syntax_highlighting/macro_rules.rs129
11 files changed, 501 insertions, 313 deletions
diff --git a/crates/ide/src/completion.rs b/crates/ide/src/completion.rs
index 697f691b0..b0e35b2bd 100644
--- a/crates/ide/src/completion.rs
+++ b/crates/ide/src/completion.rs
@@ -61,6 +61,8 @@ pub use crate::completion::{
61// - `expr.refm` -> `&mut expr` 61// - `expr.refm` -> `&mut expr`
62// - `expr.not` -> `!expr` 62// - `expr.not` -> `!expr`
63// - `expr.dbg` -> `dbg!(expr)` 63// - `expr.dbg` -> `dbg!(expr)`
64// - `expr.dbgr` -> `dbg!(&expr)`
65// - `expr.call` -> `(expr)`
64// 66//
65// There also snippet completions: 67// There also snippet completions:
66// 68//
diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs
index b30cdb6ed..1e5ea4617 100644
--- a/crates/ide/src/diagnostics.rs
+++ b/crates/ide/src/diagnostics.rs
@@ -5,6 +5,7 @@
5//! original files. So we need to map the ranges. 5//! original files. So we need to map the ranges.
6 6
7mod fixes; 7mod fixes;
8mod field_shorthand;
8 9
9use std::cell::RefCell; 10use std::cell::RefCell;
10 11
@@ -80,7 +81,7 @@ pub(crate) fn diagnostics(
80 81
81 for node in parse.tree().syntax().descendants() { 82 for node in parse.tree().syntax().descendants() {
82 check_unnecessary_braces_in_use_statement(&mut res, file_id, &node); 83 check_unnecessary_braces_in_use_statement(&mut res, file_id, &node);
83 check_struct_shorthand_initialization(&mut res, file_id, &node); 84 field_shorthand::check(&mut res, file_id, &node);
84 } 85 }
85 let res = RefCell::new(res); 86 let res = RefCell::new(res);
86 let sink_builder = DiagnosticSinkBuilder::new() 87 let sink_builder = DiagnosticSinkBuilder::new()
@@ -188,42 +189,6 @@ fn text_edit_for_remove_unnecessary_braces_with_self_in_use_statement(
188 None 189 None
189} 190}
190 191
191fn check_struct_shorthand_initialization(
192 acc: &mut Vec<Diagnostic>,
193 file_id: FileId,
194 node: &SyntaxNode,
195) -> Option<()> {
196 let record_lit = ast::RecordExpr::cast(node.clone())?;
197 let record_field_list = record_lit.record_expr_field_list()?;
198 for record_field in record_field_list.fields() {
199 if let (Some(name_ref), Some(expr)) = (record_field.name_ref(), record_field.expr()) {
200 let field_name = name_ref.syntax().text().to_string();
201 let field_expr = expr.syntax().text().to_string();
202 let field_name_is_tup_index = name_ref.as_tuple_field().is_some();
203 if field_name == field_expr && !field_name_is_tup_index {
204 let mut edit_builder = TextEdit::builder();
205 edit_builder.delete(record_field.syntax().text_range());
206 edit_builder.insert(record_field.syntax().text_range().start(), field_name);
207 let edit = edit_builder.finish();
208
209 let field_range = record_field.syntax().text_range();
210 acc.push(Diagnostic {
211 // name: None,
212 range: field_range,
213 message: "Shorthand struct initialization".to_string(),
214 severity: Severity::WeakWarning,
215 fix: Some(Fix::new(
216 "Use struct shorthand initialization",
217 SourceFileEdit { file_id, edit }.into(),
218 field_range,
219 )),
220 });
221 }
222 }
223 }
224 Some(())
225}
226
227#[cfg(test)] 192#[cfg(test)]
228mod tests { 193mod tests {
229 use expect_test::{expect, Expect}; 194 use expect_test::{expect, Expect};
@@ -237,7 +202,7 @@ mod tests {
237 /// * a diagnostic is produced 202 /// * a diagnostic is produced
238 /// * this diagnostic fix trigger range touches the input cursor position 203 /// * this diagnostic fix trigger range touches the input cursor position
239 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied 204 /// * that the contents of the file containing the cursor match `after` after the diagnostic fix is applied
240 fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) { 205 pub(super) fn check_fix(ra_fixture_before: &str, ra_fixture_after: &str) {
241 let after = trim_indent(ra_fixture_after); 206 let after = trim_indent(ra_fixture_after);
242 207
243 let (analysis, file_position) = fixture::position(ra_fixture_before); 208 let (analysis, file_position) = fixture::position(ra_fixture_before);
@@ -319,7 +284,7 @@ mod tests {
319 284
320 /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics 285 /// Takes a multi-file input fixture with annotated cursor position and checks that no diagnostics
321 /// apply to the file containing the cursor. 286 /// apply to the file containing the cursor.
322 fn check_no_diagnostics(ra_fixture: &str) { 287 pub(crate) fn check_no_diagnostics(ra_fixture: &str) {
323 let (analysis, files) = fixture::files(ra_fixture); 288 let (analysis, files) = fixture::files(ra_fixture);
324 let diagnostics = files 289 let diagnostics = files
325 .into_iter() 290 .into_iter()
@@ -720,58 +685,6 @@ mod a {
720 } 685 }
721 686
722 #[test] 687 #[test]
723 fn test_check_struct_shorthand_initialization() {
724 check_no_diagnostics(
725 r#"
726struct A { a: &'static str }
727fn main() { A { a: "hello" } }
728"#,
729 );
730 check_no_diagnostics(
731 r#"
732struct A(usize);
733fn main() { A { 0: 0 } }
734"#,
735 );
736
737 check_fix(
738 r#"
739struct A { a: &'static str }
740fn main() {
741 let a = "haha";
742 A { a<|>: a }
743}
744"#,
745 r#"
746struct A { a: &'static str }
747fn main() {
748 let a = "haha";
749 A { a }
750}
751"#,
752 );
753
754 check_fix(
755 r#"
756struct A { a: &'static str, b: &'static str }
757fn main() {
758 let a = "haha";
759 let b = "bb";
760 A { a<|>: a, b }
761}
762"#,
763 r#"
764struct A { a: &'static str, b: &'static str }
765fn main() {
766 let a = "haha";
767 let b = "bb";
768 A { a, b }
769}
770"#,
771 );
772 }
773
774 #[test]
775 fn test_add_field_from_usage() { 688 fn test_add_field_from_usage() {
776 check_fix( 689 check_fix(
777 r" 690 r"
diff --git a/crates/ide/src/diagnostics/field_shorthand.rs b/crates/ide/src/diagnostics/field_shorthand.rs
new file mode 100644
index 000000000..2c4acd783
--- /dev/null
+++ b/crates/ide/src/diagnostics/field_shorthand.rs
@@ -0,0 +1,206 @@
1//! Suggests shortening `Foo { field: field }` to `Foo { field }` in both
2//! expressions and patterns.
3
4use base_db::FileId;
5use ide_db::source_change::SourceFileEdit;
6use syntax::{ast, match_ast, AstNode, SyntaxNode};
7use text_edit::TextEdit;
8
9use crate::{Diagnostic, Fix, Severity};
10
11pub(super) fn check(acc: &mut Vec<Diagnostic>, file_id: FileId, node: &SyntaxNode) {
12 match_ast! {
13 match node {
14 ast::RecordExpr(it) => check_expr_field_shorthand(acc, file_id, it),
15 ast::RecordPat(it) => check_pat_field_shorthand(acc, file_id, it),
16 _ => ()
17 }
18 };
19}
20
21fn check_expr_field_shorthand(
22 acc: &mut Vec<Diagnostic>,
23 file_id: FileId,
24 record_expr: ast::RecordExpr,
25) {
26 let record_field_list = match record_expr.record_expr_field_list() {
27 Some(it) => it,
28 None => return,
29 };
30 for record_field in record_field_list.fields() {
31 let (name_ref, expr) = match record_field.name_ref().zip(record_field.expr()) {
32 Some(it) => it,
33 None => continue,
34 };
35
36 let field_name = name_ref.syntax().text().to_string();
37 let field_expr = expr.syntax().text().to_string();
38 let field_name_is_tup_index = name_ref.as_tuple_field().is_some();
39 if field_name != field_expr || field_name_is_tup_index {
40 continue;
41 }
42
43 let mut edit_builder = TextEdit::builder();
44 edit_builder.delete(record_field.syntax().text_range());
45 edit_builder.insert(record_field.syntax().text_range().start(), field_name);
46 let edit = edit_builder.finish();
47
48 let field_range = record_field.syntax().text_range();
49 acc.push(Diagnostic {
50 // name: None,
51 range: field_range,
52 message: "Shorthand struct initialization".to_string(),
53 severity: Severity::WeakWarning,
54 fix: Some(Fix::new(
55 "Use struct shorthand initialization",
56 SourceFileEdit { file_id, edit }.into(),
57 field_range,
58 )),
59 });
60 }
61}
62
63fn check_pat_field_shorthand(
64 acc: &mut Vec<Diagnostic>,
65 file_id: FileId,
66 record_pat: ast::RecordPat,
67) {
68 let record_pat_field_list = match record_pat.record_pat_field_list() {
69 Some(it) => it,
70 None => return,
71 };
72 for record_pat_field in record_pat_field_list.fields() {
73 let (name_ref, pat) = match record_pat_field.name_ref().zip(record_pat_field.pat()) {
74 Some(it) => it,
75 None => continue,
76 };
77
78 let field_name = name_ref.syntax().text().to_string();
79 let field_pat = pat.syntax().text().to_string();
80 let field_name_is_tup_index = name_ref.as_tuple_field().is_some();
81 if field_name != field_pat || field_name_is_tup_index {
82 continue;
83 }
84
85 let mut edit_builder = TextEdit::builder();
86 edit_builder.delete(record_pat_field.syntax().text_range());
87 edit_builder.insert(record_pat_field.syntax().text_range().start(), field_name);
88 let edit = edit_builder.finish();
89
90 let field_range = record_pat_field.syntax().text_range();
91 acc.push(Diagnostic {
92 // name: None,
93 range: field_range,
94 message: "Shorthand struct pattern".to_string(),
95 severity: Severity::WeakWarning,
96 fix: Some(Fix::new(
97 "Use struct field shorthand",
98 SourceFileEdit { file_id, edit }.into(),
99 field_range,
100 )),
101 });
102 }
103}
104
105#[cfg(test)]
106mod tests {
107 use crate::diagnostics::tests::{check_fix, check_no_diagnostics};
108
109 #[test]
110 fn test_check_expr_field_shorthand() {
111 check_no_diagnostics(
112 r#"
113struct A { a: &'static str }
114fn main() { A { a: "hello" } }
115"#,
116 );
117 check_no_diagnostics(
118 r#"
119struct A(usize);
120fn main() { A { 0: 0 } }
121"#,
122 );
123
124 check_fix(
125 r#"
126struct A { a: &'static str }
127fn main() {
128 let a = "haha";
129 A { a<|>: a }
130}
131"#,
132 r#"
133struct A { a: &'static str }
134fn main() {
135 let a = "haha";
136 A { a }
137}
138"#,
139 );
140
141 check_fix(
142 r#"
143struct A { a: &'static str, b: &'static str }
144fn main() {
145 let a = "haha";
146 let b = "bb";
147 A { a<|>: a, b }
148}
149"#,
150 r#"
151struct A { a: &'static str, b: &'static str }
152fn main() {
153 let a = "haha";
154 let b = "bb";
155 A { a, b }
156}
157"#,
158 );
159 }
160
161 #[test]
162 fn test_check_pat_field_shorthand() {
163 check_no_diagnostics(
164 r#"
165struct A { a: &'static str }
166fn f(a: A) { let A { a: hello } = a; }
167"#,
168 );
169 check_no_diagnostics(
170 r#"
171struct A(usize);
172fn f(a: A) { let A { 0: 0 } = a; }
173"#,
174 );
175
176 check_fix(
177 r#"
178struct A { a: &'static str }
179fn f(a: A) {
180 let A { a<|>: a } = a;
181}
182"#,
183 r#"
184struct A { a: &'static str }
185fn f(a: A) {
186 let A { a } = a;
187}
188"#,
189 );
190
191 check_fix(
192 r#"
193struct A { a: &'static str, b: &'static str }
194fn f(a: A) {
195 let A { a<|>: a, b } = a;
196}
197"#,
198 r#"
199struct A { a: &'static str, b: &'static str }
200fn f(a: A) {
201 let A { a, b } = a;
202}
203"#,
204 );
205 }
206}
diff --git a/crates/ide/src/doc_links.rs b/crates/ide/src/doc_links.rs
index 06af36b73..d9dc63b33 100644
--- a/crates/ide/src/doc_links.rs
+++ b/crates/ide/src/doc_links.rs
@@ -1,9 +1,10 @@
1//! Resolves and rewrites links in markdown documentation. 1//! Resolves and rewrites links in markdown documentation.
2 2
3use std::convert::TryFrom;
3use std::iter::once; 4use std::iter::once;
4 5
5use itertools::Itertools; 6use itertools::Itertools;
6use pulldown_cmark::{CowStr, Event, LinkType, Options, Parser, Tag}; 7use pulldown_cmark::{BrokenLink, CowStr, Event, InlineStr, LinkType, Options, Parser, Tag};
7use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions}; 8use pulldown_cmark_to_cmark::{cmark_with_options, Options as CmarkOptions};
8use url::Url; 9use url::Url;
9 10
@@ -13,7 +14,7 @@ use hir::{
13 ModuleDef, 14 ModuleDef,
14}; 15};
15use ide_db::{ 16use ide_db::{
16 defs::{classify_name, classify_name_ref, Definition}, 17 defs::{Definition, NameClass, NameRefClass},
17 RootDatabase, 18 RootDatabase,
18}; 19};
19use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T}; 20use syntax::{ast, match_ast, AstNode, SyntaxKind::*, SyntaxToken, TokenAtOffset, T};
@@ -24,11 +25,13 @@ pub type DocumentationLink = String;
24 25
25/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs) 26/// Rewrite documentation links in markdown to point to an online host (e.g. docs.rs)
26pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String { 27pub fn rewrite_links(db: &RootDatabase, markdown: &str, definition: &Definition) -> String {
27 let doc = Parser::new_with_broken_link_callback( 28 let mut cb = |link: BrokenLink| {
28 markdown, 29 Some((
29 Options::empty(), 30 /*url*/ link.reference.to_owned().into(),
30 Some(&|label, _| Some((/*url*/ label.to_string(), /*title*/ label.to_string()))), 31 /*title*/ link.reference.to_owned().into(),
31 ); 32 ))
33 };
34 let doc = Parser::new_with_broken_link_callback(markdown, Options::empty(), Some(&mut cb));
32 35
33 let doc = map_links(doc, |target, title: &str| { 36 let doc = map_links(doc, |target, title: &str| {
34 // This check is imperfect, there's some overlap between valid intra-doc links 37 // This check is imperfect, there's some overlap between valid intra-doc links
@@ -66,11 +69,11 @@ pub fn remove_links(markdown: &str) -> String {
66 let mut opts = Options::empty(); 69 let mut opts = Options::empty();
67 opts.insert(Options::ENABLE_FOOTNOTES); 70 opts.insert(Options::ENABLE_FOOTNOTES);
68 71
69 let doc = Parser::new_with_broken_link_callback( 72 let mut cb = |_: BrokenLink| {
70 markdown, 73 let empty = InlineStr::try_from("").unwrap();
71 opts, 74 Some((CowStr::Inlined(empty.clone()), CowStr::Inlined(empty)))
72 Some(&|_, _| Some((String::new(), String::new()))), 75 };
73 ); 76 let doc = Parser::new_with_broken_link_callback(markdown, opts, Some(&mut cb));
74 let doc = doc.filter_map(move |evt| match evt { 77 let doc = doc.filter_map(move |evt| match evt {
75 Event::Start(Tag::Link(link_type, ref target, ref title)) => { 78 Event::Start(Tag::Link(link_type, ref target, ref title)) => {
76 if link_type == LinkType::Inline && target.contains("://") { 79 if link_type == LinkType::Inline && target.contains("://") {
@@ -229,8 +232,8 @@ pub(crate) fn external_docs(
229 let node = token.parent(); 232 let node = token.parent();
230 let definition = match_ast! { 233 let definition = match_ast! {
231 match node { 234 match node {
232 ast::NameRef(name_ref) => classify_name_ref(&sema, &name_ref).map(|d| d.definition(sema.db)), 235 ast::NameRef(name_ref) => NameRefClass::classify(&sema, &name_ref).map(|d| d.referenced(sema.db)),
233 ast::Name(name) => classify_name(&sema, &name).map(|d| d.definition(sema.db)), 236 ast::Name(name) => NameClass::classify(&sema, &name).map(|d| d.referenced_or_defined(sema.db)),
234 _ => None, 237 _ => None,
235 } 238 }
236 }; 239 };
diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs
index 582bf4837..a87e31019 100644
--- a/crates/ide/src/goto_definition.rs
+++ b/crates/ide/src/goto_definition.rs
@@ -1,6 +1,6 @@
1use hir::Semantics; 1use hir::Semantics;
2use ide_db::{ 2use ide_db::{
3 defs::{classify_name, classify_name_ref}, 3 defs::{NameClass, NameRefClass},
4 symbol_index, RootDatabase, 4 symbol_index, RootDatabase,
5}; 5};
6use syntax::{ 6use syntax::{
@@ -40,7 +40,7 @@ pub(crate) fn goto_definition(
40 reference_definition(&sema, &name_ref).to_vec() 40 reference_definition(&sema, &name_ref).to_vec()
41 }, 41 },
42 ast::Name(name) => { 42 ast::Name(name) => {
43 let def = classify_name(&sema, &name)?.definition(sema.db); 43 let def = NameClass::classify(&sema, &name)?.referenced_or_defined(sema.db);
44 let nav = def.try_to_nav(sema.db)?; 44 let nav = def.try_to_nav(sema.db)?;
45 vec![nav] 45 vec![nav]
46 }, 46 },
@@ -81,9 +81,9 @@ pub(crate) fn reference_definition(
81 sema: &Semantics<RootDatabase>, 81 sema: &Semantics<RootDatabase>,
82 name_ref: &ast::NameRef, 82 name_ref: &ast::NameRef,
83) -> ReferenceResult { 83) -> ReferenceResult {
84 let name_kind = classify_name_ref(sema, name_ref); 84 let name_kind = NameRefClass::classify(sema, name_ref);
85 if let Some(def) = name_kind { 85 if let Some(def) = name_kind {
86 let def = def.definition(sema.db); 86 let def = def.referenced(sema.db);
87 return match def.try_to_nav(sema.db) { 87 return match def.try_to_nav(sema.db) {
88 Some(nav) => ReferenceResult::Exact(nav), 88 Some(nav) => ReferenceResult::Exact(nav),
89 None => ReferenceResult::Approximate(Vec::new()), 89 None => ReferenceResult::Approximate(Vec::new()),
diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs
index 6290b35bd..845333e2a 100644
--- a/crates/ide/src/hover.rs
+++ b/crates/ide/src/hover.rs
@@ -4,7 +4,7 @@ use hir::{
4 Module, ModuleDef, ModuleSource, Semantics, 4 Module, ModuleDef, ModuleSource, Semantics,
5}; 5};
6use ide_db::{ 6use ide_db::{
7 defs::{classify_name, classify_name_ref, Definition}, 7 defs::{Definition, NameClass, NameRefClass},
8 RootDatabase, 8 RootDatabase,
9}; 9};
10use itertools::Itertools; 10use itertools::Itertools;
@@ -107,8 +107,8 @@ pub(crate) fn hover(
107 let node = token.parent(); 107 let node = token.parent();
108 let definition = match_ast! { 108 let definition = match_ast! {
109 match node { 109 match node {
110 ast::NameRef(name_ref) => classify_name_ref(&sema, &name_ref).map(|d| d.definition(sema.db)), 110 ast::Name(name) => NameClass::classify(&sema, &name).and_then(|d| d.defined(sema.db)),
111 ast::Name(name) => classify_name(&sema, &name).map(|d| d.definition(sema.db)), 111 ast::NameRef(name_ref) => NameRefClass::classify(&sema, &name_ref).map(|d| d.referenced(sema.db)),
112 _ => None, 112 _ => None,
113 } 113 }
114 }; 114 };
@@ -3232,4 +3232,27 @@ fn main() { let foo_test = name_with_dashes::wrapper::Thing::new<|>(); }
3232 "#]], 3232 "#]],
3233 ) 3233 )
3234 } 3234 }
3235
3236 #[test]
3237 fn hover_field_pat_shorthand_ref_match_ergonomics() {
3238 check(
3239 r#"
3240struct S {
3241 f: i32,
3242}
3243
3244fn main() {
3245 let s = S { f: 0 };
3246 let S { f<|> } = &s;
3247}
3248"#,
3249 expect![[r#"
3250 *f*
3251
3252 ```rust
3253 &i32
3254 ```
3255 "#]],
3256 );
3257 }
3235} 3258}
diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs
index 88e2f2db3..67ec257a8 100644
--- a/crates/ide/src/references.rs
+++ b/crates/ide/src/references.rs
@@ -13,7 +13,7 @@ pub(crate) mod rename;
13 13
14use hir::Semantics; 14use hir::Semantics;
15use ide_db::{ 15use ide_db::{
16 defs::{classify_name, classify_name_ref, Definition}, 16 defs::{Definition, NameClass, NameRefClass},
17 search::SearchScope, 17 search::SearchScope,
18 RootDatabase, 18 RootDatabase,
19}; 19};
@@ -132,13 +132,13 @@ fn find_name(
132 opt_name: Option<ast::Name>, 132 opt_name: Option<ast::Name>,
133) -> Option<RangeInfo<Definition>> { 133) -> Option<RangeInfo<Definition>> {
134 if let Some(name) = opt_name { 134 if let Some(name) = opt_name {
135 let def = classify_name(sema, &name)?.definition(sema.db); 135 let def = NameClass::classify(sema, &name)?.referenced_or_defined(sema.db);
136 let range = name.syntax().text_range(); 136 let range = name.syntax().text_range();
137 return Some(RangeInfo::new(range, def)); 137 return Some(RangeInfo::new(range, def));
138 } 138 }
139 let name_ref = 139 let name_ref =
140 sema.find_node_at_offset_with_descend::<ast::NameRef>(&syntax, position.offset)?; 140 sema.find_node_at_offset_with_descend::<ast::NameRef>(&syntax, position.offset)?;
141 let def = classify_name_ref(sema, &name_ref)?.definition(sema.db); 141 let def = NameRefClass::classify(sema, &name_ref)?.referenced(sema.db);
142 let range = name_ref.syntax().text_range(); 142 let range = name_ref.syntax().text_range();
143 Some(RangeInfo::new(range, def)) 143 Some(RangeInfo::new(range, def))
144} 144}
diff --git a/crates/ide/src/references/rename.rs b/crates/ide/src/references/rename.rs
index f9a11e43d..35aafc49d 100644
--- a/crates/ide/src/references/rename.rs
+++ b/crates/ide/src/references/rename.rs
@@ -3,7 +3,7 @@
3use base_db::SourceDatabaseExt; 3use base_db::SourceDatabaseExt;
4use hir::{Module, ModuleDef, ModuleSource, Semantics}; 4use hir::{Module, ModuleDef, ModuleSource, Semantics};
5use ide_db::{ 5use ide_db::{
6 defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass}, 6 defs::{Definition, NameClass, NameRefClass},
7 RootDatabase, 7 RootDatabase,
8}; 8};
9 9
@@ -88,13 +88,13 @@ fn find_module_at_offset(
88 let module = match_ast! { 88 let module = match_ast! {
89 match (ident.parent()) { 89 match (ident.parent()) {
90 ast::NameRef(name_ref) => { 90 ast::NameRef(name_ref) => {
91 match classify_name_ref(sema, &name_ref)? { 91 match NameRefClass::classify(sema, &name_ref)? {
92 NameRefClass::Definition(Definition::ModuleDef(ModuleDef::Module(module))) => module, 92 NameRefClass::Definition(Definition::ModuleDef(ModuleDef::Module(module))) => module,
93 _ => return None, 93 _ => return None,
94 } 94 }
95 }, 95 },
96 ast::Name(name) => { 96 ast::Name(name) => {
97 match classify_name(&sema, &name)? { 97 match NameClass::classify(&sema, &name)? {
98 NameClass::Definition(Definition::ModuleDef(ModuleDef::Module(module))) => module, 98 NameClass::Definition(Definition::ModuleDef(ModuleDef::Module(module))) => module,
99 _ => return None, 99 _ => return None,
100 } 100 }
diff --git a/crates/ide/src/syntax_highlighting.rs b/crates/ide/src/syntax_highlighting.rs
index 6aafd6fd5..b35c03162 100644
--- a/crates/ide/src/syntax_highlighting.rs
+++ b/crates/ide/src/syntax_highlighting.rs
@@ -1,12 +1,14 @@
1mod tags; 1mod format;
2mod html; 2mod html;
3mod injection; 3mod injection;
4mod macro_rules;
5mod tags;
4#[cfg(test)] 6#[cfg(test)]
5mod tests; 7mod tests;
6 8
7use hir::{Local, Name, Semantics, VariantDef}; 9use hir::{Local, Name, Semantics, VariantDef};
8use ide_db::{ 10use ide_db::{
9 defs::{classify_name, classify_name_ref, Definition, NameClass, NameRefClass}, 11 defs::{Definition, NameClass, NameRefClass},
10 RootDatabase, 12 RootDatabase,
11}; 13};
12use rustc_hash::FxHashMap; 14use rustc_hash::FxHashMap;
@@ -17,9 +19,11 @@ use syntax::{
17 SyntaxNode, SyntaxToken, TextRange, WalkEvent, T, 19 SyntaxNode, SyntaxToken, TextRange, WalkEvent, T,
18}; 20};
19 21
20use crate::FileId; 22use crate::{
23 syntax_highlighting::{format::FormatStringHighlighter, macro_rules::MacroRulesHighlighter},
24 FileId,
25};
21 26
22use ast::FormatSpecifier;
23pub(crate) use html::highlight_as_html; 27pub(crate) use html::highlight_as_html;
24pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag}; 28pub use tags::{Highlight, HighlightModifier, HighlightModifiers, HighlightTag};
25 29
@@ -68,8 +72,9 @@ pub(crate) fn highlight(
68 // When we leave a node, the we use it to flatten the highlighted ranges. 72 // When we leave a node, the we use it to flatten the highlighted ranges.
69 let mut stack = HighlightedRangeStack::new(); 73 let mut stack = HighlightedRangeStack::new();
70 74
71 let mut current_macro_call: Option<(ast::MacroCall, Option<MacroMatcherParseState>)> = None; 75 let mut current_macro_call: Option<ast::MacroCall> = None;
72 let mut format_string: Option<SyntaxElement> = None; 76 let mut format_string_highlighter = FormatStringHighlighter::default();
77 let mut macro_rules_highlighter = MacroRulesHighlighter::default();
73 78
74 // Walk all nodes, keeping track of whether we are inside a macro or not. 79 // Walk all nodes, keeping track of whether we are inside a macro or not.
75 // If in macro, expand it first and highlight the expanded code. 80 // If in macro, expand it first and highlight the expanded code.
@@ -99,9 +104,8 @@ pub(crate) fn highlight(
99 binding_hash: None, 104 binding_hash: None,
100 }); 105 });
101 } 106 }
102 let mut is_macro_rules = None;
103 if let Some(name) = mc.is_macro_rules() { 107 if let Some(name) = mc.is_macro_rules() {
104 is_macro_rules = Some(MacroMatcherParseState::new()); 108 macro_rules_highlighter.init();
105 if let Some((highlight, binding_hash)) = highlight_element( 109 if let Some((highlight, binding_hash)) = highlight_element(
106 &sema, 110 &sema,
107 &mut bindings_shadow_count, 111 &mut bindings_shadow_count,
@@ -115,13 +119,14 @@ pub(crate) fn highlight(
115 }); 119 });
116 } 120 }
117 } 121 }
118 current_macro_call = Some((mc.clone(), is_macro_rules)); 122 current_macro_call = Some(mc.clone());
119 continue; 123 continue;
120 } 124 }
121 WalkEvent::Leave(Some(mc)) => { 125 WalkEvent::Leave(Some(mc)) => {
122 assert!(current_macro_call.map(|it| it.0) == Some(mc)); 126 assert!(current_macro_call == Some(mc));
123 current_macro_call = None; 127 current_macro_call = None;
124 format_string = None; 128 format_string_highlighter = FormatStringHighlighter::default();
129 macro_rules_highlighter = MacroRulesHighlighter::default();
125 } 130 }
126 _ => (), 131 _ => (),
127 } 132 }
@@ -148,20 +153,6 @@ pub(crate) fn highlight(
148 WalkEvent::Leave(_) => continue, 153 WalkEvent::Leave(_) => continue,
149 }; 154 };
150 155
151 // check if in matcher part of a macro_rules rule
152 if let Some((_, Some(ref mut state))) = current_macro_call {
153 if let Some(tok) = element.as_token() {
154 if matches!(
155 update_macro_rules_state(tok, state),
156 RuleState::Matcher | RuleState::Expander
157 ) {
158 if skip_metavariables(element.clone()) {
159 continue;
160 }
161 }
162 }
163 }
164
165 let range = element.text_range(); 156 let range = element.text_range();
166 157
167 let element_to_highlight = if current_macro_call.is_some() && element.kind() != COMMENT { 158 let element_to_highlight = if current_macro_call.is_some() && element.kind() != COMMENT {
@@ -173,29 +164,9 @@ pub(crate) fn highlight(
173 let token = sema.descend_into_macros(token.clone()); 164 let token = sema.descend_into_macros(token.clone());
174 let parent = token.parent(); 165 let parent = token.parent();
175 166
176 // Check if macro takes a format string and remember it for highlighting later. 167 format_string_highlighter.check_for_format_string(&parent);
177 // The macros that accept a format string expand to a compiler builtin macros 168 if let Some(tok) = element.as_token() {
178 // `format_args` and `format_args_nl`. 169 macro_rules_highlighter.advance(tok);
179 if let Some(name) = parent
180 .parent()
181 .and_then(ast::MacroCall::cast)
182 .and_then(|mc| mc.path())
183 .and_then(|p| p.segment())
184 .and_then(|s| s.name_ref())
185 {
186 match name.text().as_str() {
187 "format_args" | "format_args_nl" => {
188 format_string = parent
189 .children_with_tokens()
190 .filter(|t| t.kind() != WHITESPACE)
191 .nth(1)
192 .filter(|e| {
193 ast::String::can_cast(e.kind())
194 || ast::RawString::can_cast(e.kind())
195 })
196 }
197 _ => {}
198 }
199 } 170 }
200 171
201 // We only care Name and Name_ref 172 // We only care Name and Name_ref
@@ -214,31 +185,20 @@ pub(crate) fn highlight(
214 } 185 }
215 } 186 }
216 187
217 let is_format_string = format_string.as_ref() == Some(&element_to_highlight);
218
219 if let Some((highlight, binding_hash)) = highlight_element( 188 if let Some((highlight, binding_hash)) = highlight_element(
220 &sema, 189 &sema,
221 &mut bindings_shadow_count, 190 &mut bindings_shadow_count,
222 syntactic_name_ref_highlighting, 191 syntactic_name_ref_highlighting,
223 element_to_highlight.clone(), 192 element_to_highlight.clone(),
224 ) { 193 ) {
225 stack.add(HighlightedRange { range, highlight, binding_hash }); 194 if macro_rules_highlighter.highlight(element_to_highlight.clone()).is_none() {
195 stack.add(HighlightedRange { range, highlight, binding_hash });
196 }
197
226 if let Some(string) = 198 if let Some(string) =
227 element_to_highlight.as_token().cloned().and_then(ast::String::cast) 199 element_to_highlight.as_token().cloned().and_then(ast::String::cast)
228 { 200 {
229 if is_format_string { 201 format_string_highlighter.highlight_format_string(&mut stack, &string, range);
230 stack.push();
231 string.lex_format_specifier(|piece_range, kind| {
232 if let Some(highlight) = highlight_format_specifier(kind) {
233 stack.add(HighlightedRange {
234 range: piece_range + range.start(),
235 highlight: highlight.into(),
236 binding_hash: None,
237 });
238 }
239 });
240 stack.pop();
241 }
242 // Highlight escape sequences 202 // Highlight escape sequences
243 if let Some(char_ranges) = string.char_ranges() { 203 if let Some(char_ranges) = string.char_ranges() {
244 stack.push(); 204 stack.push();
@@ -256,19 +216,7 @@ pub(crate) fn highlight(
256 } else if let Some(string) = 216 } else if let Some(string) =
257 element_to_highlight.as_token().cloned().and_then(ast::RawString::cast) 217 element_to_highlight.as_token().cloned().and_then(ast::RawString::cast)
258 { 218 {
259 if is_format_string { 219 format_string_highlighter.highlight_format_string(&mut stack, &string, range);
260 stack.push();
261 string.lex_format_specifier(|piece_range, kind| {
262 if let Some(highlight) = highlight_format_specifier(kind) {
263 stack.add(HighlightedRange {
264 range: piece_range + range.start(),
265 highlight: highlight.into(),
266 binding_hash: None,
267 });
268 }
269 });
270 stack.pop();
271 }
272 } 220 }
273 } 221 }
274 } 222 }
@@ -436,24 +384,6 @@ impl HighlightedRangeStack {
436 } 384 }
437} 385}
438 386
439fn highlight_format_specifier(kind: FormatSpecifier) -> Option<HighlightTag> {
440 Some(match kind {
441 FormatSpecifier::Open
442 | FormatSpecifier::Close
443 | FormatSpecifier::Colon
444 | FormatSpecifier::Fill
445 | FormatSpecifier::Align
446 | FormatSpecifier::Sign
447 | FormatSpecifier::NumberSign
448 | FormatSpecifier::DollarSign
449 | FormatSpecifier::Dot
450 | FormatSpecifier::Asterisk
451 | FormatSpecifier::QuestionMark => HighlightTag::FormatSpecifier,
452 FormatSpecifier::Integer | FormatSpecifier::Zero => HighlightTag::NumericLiteral,
453 FormatSpecifier::Identifier => HighlightTag::Local,
454 })
455}
456
457fn macro_call_range(macro_call: &ast::MacroCall) -> Option<TextRange> { 387fn macro_call_range(macro_call: &ast::MacroCall) -> Option<TextRange> {
458 let path = macro_call.path()?; 388 let path = macro_call.path()?;
459 let name_ref = path.segment()?.name_ref()?; 389 let name_ref = path.segment()?.name_ref()?;
@@ -513,7 +443,7 @@ fn highlight_element(
513 // Highlight definitions depending on the "type" of the definition. 443 // Highlight definitions depending on the "type" of the definition.
514 NAME => { 444 NAME => {
515 let name = element.into_node().and_then(ast::Name::cast).unwrap(); 445 let name = element.into_node().and_then(ast::Name::cast).unwrap();
516 let name_kind = classify_name(sema, &name); 446 let name_kind = NameClass::classify(sema, &name);
517 447
518 if let Some(NameClass::Definition(Definition::Local(local))) = &name_kind { 448 if let Some(NameClass::Definition(Definition::Local(local))) = &name_kind {
519 if let Some(name) = local.name(db) { 449 if let Some(name) = local.name(db) {
@@ -529,9 +459,9 @@ fn highlight_element(
529 highlight_def(db, def) | HighlightModifier::Definition 459 highlight_def(db, def) | HighlightModifier::Definition
530 } 460 }
531 Some(NameClass::ConstReference(def)) => highlight_def(db, def), 461 Some(NameClass::ConstReference(def)) => highlight_def(db, def),
532 Some(NameClass::FieldShorthand { field, .. }) => { 462 Some(NameClass::PatFieldShorthand { field_ref, .. }) => {
533 let mut h = HighlightTag::Field.into(); 463 let mut h = HighlightTag::Field.into();
534 if let Definition::Field(field) = field { 464 if let Definition::Field(field) = field_ref {
535 if let VariantDef::Union(_) = field.parent_def(db) { 465 if let VariantDef::Union(_) = field.parent_def(db) {
536 h |= HighlightModifier::Unsafe; 466 h |= HighlightModifier::Unsafe;
537 } 467 }
@@ -550,7 +480,7 @@ fn highlight_element(
550 NAME_REF => { 480 NAME_REF => {
551 let name_ref = element.into_node().and_then(ast::NameRef::cast).unwrap(); 481 let name_ref = element.into_node().and_then(ast::NameRef::cast).unwrap();
552 highlight_func_by_name_ref(sema, &name_ref).unwrap_or_else(|| { 482 highlight_func_by_name_ref(sema, &name_ref).unwrap_or_else(|| {
553 match classify_name_ref(sema, &name_ref) { 483 match NameRefClass::classify(sema, &name_ref) {
554 Some(name_kind) => match name_kind { 484 Some(name_kind) => match name_kind {
555 NameRefClass::ExternCrate(_) => HighlightTag::Module.into(), 485 NameRefClass::ExternCrate(_) => HighlightTag::Module.into(),
556 NameRefClass::Definition(def) => { 486 NameRefClass::Definition(def) => {
@@ -934,99 +864,3 @@ fn highlight_name_ref_by_syntax(name: ast::NameRef, sema: &Semantics<RootDatabas
934 _ => default.into(), 864 _ => default.into(),
935 } 865 }
936} 866}
937
938struct MacroMatcherParseState {
939 /// Opening and corresponding closing bracket of the matcher or expander of the current rule
940 paren_ty: Option<(SyntaxKind, SyntaxKind)>,
941 paren_level: usize,
942 rule_state: RuleState,
943 /// Whether we are inside the outer `{` `}` macro block that holds the rules
944 in_invoc_body: bool,
945}
946
947impl MacroMatcherParseState {
948 fn new() -> Self {
949 MacroMatcherParseState {
950 paren_ty: None,
951 paren_level: 0,
952 in_invoc_body: false,
953 rule_state: RuleState::None,
954 }
955 }
956}
957
958#[derive(Copy, Clone, PartialEq)]
959enum RuleState {
960 Matcher,
961 Expander,
962 Between,
963 None,
964}
965
966impl RuleState {
967 fn transition(&mut self) {
968 *self = match self {
969 RuleState::Matcher => RuleState::Between,
970 RuleState::Expander => RuleState::None,
971 RuleState::Between => RuleState::Expander,
972 RuleState::None => RuleState::Matcher,
973 };
974 }
975}
976
977fn update_macro_rules_state(tok: &SyntaxToken, state: &mut MacroMatcherParseState) -> RuleState {
978 if !state.in_invoc_body {
979 if tok.kind() == T!['{'] {
980 state.in_invoc_body = true;
981 }
982 return state.rule_state;
983 }
984
985 match state.paren_ty {
986 Some((open, close)) => {
987 if tok.kind() == open {
988 state.paren_level += 1;
989 } else if tok.kind() == close {
990 state.paren_level -= 1;
991 if state.paren_level == 0 {
992 let res = state.rule_state;
993 state.rule_state.transition();
994 state.paren_ty = None;
995 return res;
996 }
997 }
998 }
999 None => {
1000 match tok.kind() {
1001 T!['('] => {
1002 state.paren_ty = Some((T!['('], T![')']));
1003 }
1004 T!['{'] => {
1005 state.paren_ty = Some((T!['{'], T!['}']));
1006 }
1007 T!['['] => {
1008 state.paren_ty = Some((T!['['], T![']']));
1009 }
1010 _ => (),
1011 }
1012 if state.paren_ty.is_some() {
1013 state.paren_level = 1;
1014 state.rule_state.transition();
1015 }
1016 }
1017 }
1018 state.rule_state
1019}
1020
1021fn skip_metavariables(element: SyntaxElement) -> bool {
1022 let tok = match element.as_token() {
1023 Some(tok) => tok,
1024 None => return false,
1025 };
1026 let is_fragment = || tok.prev_token().map(|tok| tok.kind()) == Some(T![$]);
1027 match tok.kind() {
1028 IDENT if is_fragment() => true,
1029 kind if kind.is_keyword() && is_fragment() => true,
1030 _ => false,
1031 }
1032}
diff --git a/crates/ide/src/syntax_highlighting/format.rs b/crates/ide/src/syntax_highlighting/format.rs
new file mode 100644
index 000000000..71bde24f0
--- /dev/null
+++ b/crates/ide/src/syntax_highlighting/format.rs
@@ -0,0 +1,78 @@
1//! Syntax highlighting for format macro strings.
2use syntax::{
3 ast::{self, FormatSpecifier, HasFormatSpecifier},
4 AstNode, AstToken, SyntaxElement, SyntaxKind, SyntaxNode, TextRange,
5};
6
7use crate::{syntax_highlighting::HighlightedRangeStack, HighlightTag, HighlightedRange};
8
9#[derive(Default)]
10pub(super) struct FormatStringHighlighter {
11 format_string: Option<SyntaxElement>,
12}
13
14impl FormatStringHighlighter {
15 pub(super) fn check_for_format_string(&mut self, parent: &SyntaxNode) {
16 // Check if macro takes a format string and remember it for highlighting later.
17 // The macros that accept a format string expand to a compiler builtin macros
18 // `format_args` and `format_args_nl`.
19 if let Some(name) = parent
20 .parent()
21 .and_then(ast::MacroCall::cast)
22 .and_then(|mc| mc.path())
23 .and_then(|p| p.segment())
24 .and_then(|s| s.name_ref())
25 {
26 match name.text().as_str() {
27 "format_args" | "format_args_nl" => {
28 self.format_string = parent
29 .children_with_tokens()
30 .filter(|t| t.kind() != SyntaxKind::WHITESPACE)
31 .nth(1)
32 .filter(|e| {
33 ast::String::can_cast(e.kind()) || ast::RawString::can_cast(e.kind())
34 })
35 }
36 _ => {}
37 }
38 }
39 }
40 pub(super) fn highlight_format_string(
41 &self,
42 range_stack: &mut HighlightedRangeStack,
43 string: &impl HasFormatSpecifier,
44 range: TextRange,
45 ) {
46 if self.format_string.as_ref() == Some(&SyntaxElement::from(string.syntax().clone())) {
47 range_stack.push();
48 string.lex_format_specifier(|piece_range, kind| {
49 if let Some(highlight) = highlight_format_specifier(kind) {
50 range_stack.add(HighlightedRange {
51 range: piece_range + range.start(),
52 highlight: highlight.into(),
53 binding_hash: None,
54 });
55 }
56 });
57 range_stack.pop();
58 }
59 }
60}
61
62fn highlight_format_specifier(kind: FormatSpecifier) -> Option<HighlightTag> {
63 Some(match kind {
64 FormatSpecifier::Open
65 | FormatSpecifier::Close
66 | FormatSpecifier::Colon
67 | FormatSpecifier::Fill
68 | FormatSpecifier::Align
69 | FormatSpecifier::Sign
70 | FormatSpecifier::NumberSign
71 | FormatSpecifier::DollarSign
72 | FormatSpecifier::Dot
73 | FormatSpecifier::Asterisk
74 | FormatSpecifier::QuestionMark => HighlightTag::FormatSpecifier,
75 FormatSpecifier::Integer | FormatSpecifier::Zero => HighlightTag::NumericLiteral,
76 FormatSpecifier::Identifier => HighlightTag::Local,
77 })
78}
diff --git a/crates/ide/src/syntax_highlighting/macro_rules.rs b/crates/ide/src/syntax_highlighting/macro_rules.rs
new file mode 100644
index 000000000..4462af47e
--- /dev/null
+++ b/crates/ide/src/syntax_highlighting/macro_rules.rs
@@ -0,0 +1,129 @@
1//! Syntax highlighting for macro_rules!.
2use syntax::{SyntaxElement, SyntaxKind, SyntaxToken, TextRange, T};
3
4use crate::{HighlightTag, HighlightedRange};
5
6#[derive(Default)]
7pub(super) struct MacroRulesHighlighter {
8 state: Option<MacroMatcherParseState>,
9}
10
11impl MacroRulesHighlighter {
12 pub(super) fn init(&mut self) {
13 self.state = Some(MacroMatcherParseState::default());
14 }
15
16 pub(super) fn advance(&mut self, token: &SyntaxToken) {
17 if let Some(state) = self.state.as_mut() {
18 update_macro_rules_state(state, token);
19 }
20 }
21
22 pub(super) fn highlight(&self, element: SyntaxElement) -> Option<HighlightedRange> {
23 if let Some(state) = self.state.as_ref() {
24 if matches!(state.rule_state, RuleState::Matcher | RuleState::Expander) {
25 if let Some(range) = is_metavariable(element) {
26 return Some(HighlightedRange {
27 range,
28 highlight: HighlightTag::UnresolvedReference.into(),
29 binding_hash: None,
30 });
31 }
32 }
33 }
34 None
35 }
36}
37
38struct MacroMatcherParseState {
39 /// Opening and corresponding closing bracket of the matcher or expander of the current rule
40 paren_ty: Option<(SyntaxKind, SyntaxKind)>,
41 paren_level: usize,
42 rule_state: RuleState,
43 /// Whether we are inside the outer `{` `}` macro block that holds the rules
44 in_invoc_body: bool,
45}
46
47impl Default for MacroMatcherParseState {
48 fn default() -> Self {
49 MacroMatcherParseState {
50 paren_ty: None,
51 paren_level: 0,
52 in_invoc_body: false,
53 rule_state: RuleState::None,
54 }
55 }
56}
57
58#[derive(Copy, Clone, Debug, PartialEq)]
59enum RuleState {
60 Matcher,
61 Expander,
62 Between,
63 None,
64}
65
66impl RuleState {
67 fn transition(&mut self) {
68 *self = match self {
69 RuleState::Matcher => RuleState::Between,
70 RuleState::Expander => RuleState::None,
71 RuleState::Between => RuleState::Expander,
72 RuleState::None => RuleState::Matcher,
73 };
74 }
75}
76
77fn update_macro_rules_state(state: &mut MacroMatcherParseState, tok: &SyntaxToken) {
78 if !state.in_invoc_body {
79 if tok.kind() == T!['{'] {
80 state.in_invoc_body = true;
81 }
82 return;
83 }
84
85 match state.paren_ty {
86 Some((open, close)) => {
87 if tok.kind() == open {
88 state.paren_level += 1;
89 } else if tok.kind() == close {
90 state.paren_level -= 1;
91 if state.paren_level == 0 {
92 state.rule_state.transition();
93 state.paren_ty = None;
94 }
95 }
96 }
97 None => {
98 match tok.kind() {
99 T!['('] => {
100 state.paren_ty = Some((T!['('], T![')']));
101 }
102 T!['{'] => {
103 state.paren_ty = Some((T!['{'], T!['}']));
104 }
105 T!['['] => {
106 state.paren_ty = Some((T!['['], T![']']));
107 }
108 _ => (),
109 }
110 if state.paren_ty.is_some() {
111 state.paren_level = 1;
112 state.rule_state.transition();
113 }
114 }
115 }
116}
117
118fn is_metavariable(element: SyntaxElement) -> Option<TextRange> {
119 let tok = element.as_token()?;
120 match tok.kind() {
121 kind if kind == SyntaxKind::IDENT || kind.is_keyword() => {
122 if let Some(_dollar) = tok.prev_token().filter(|t| t.kind() == SyntaxKind::DOLLAR) {
123 return Some(tok.text_range());
124 }
125 }
126 _ => (),
127 };
128 None
129}