]> git.lizzy.rs Git - rust.git/blob - src/tools/rust-analyzer/crates/rust-analyzer/src/cli/scip.rs
Rollup merge of #101555 - jhpratt:stabilize-mixed_integer_ops, r=joshtriplett
[rust.git] / src / tools / rust-analyzer / crates / rust-analyzer / src / cli / scip.rs
1 //! SCIP generator
2
3 use std::{
4     collections::{HashMap, HashSet},
5     time::Instant,
6 };
7
8 use crate::line_index::{LineEndings, LineIndex, OffsetEncoding};
9 use hir::Name;
10 use ide::{
11     LineCol, MonikerDescriptorKind, MonikerResult, StaticIndex, StaticIndexedFile, TextRange,
12     TokenId,
13 };
14 use ide_db::LineIndexDatabase;
15 use project_model::{CargoConfig, ProjectManifest, ProjectWorkspace};
16 use scip::types as scip_types;
17 use std::env;
18
19 use crate::cli::{
20     flags,
21     load_cargo::{load_workspace, LoadCargoConfig},
22     Result,
23 };
24
25 impl flags::Scip {
26     pub fn run(self) -> Result<()> {
27         eprintln!("Generating SCIP start...");
28         let now = Instant::now();
29         let cargo_config = CargoConfig::default();
30
31         let no_progress = &|s| (eprintln!("rust-analyzer: Loading {}", s));
32         let load_cargo_config = LoadCargoConfig {
33             load_out_dirs_from_check: true,
34             with_proc_macro: true,
35             prefill_caches: true,
36         };
37         let path = vfs::AbsPathBuf::assert(env::current_dir()?.join(&self.path));
38         let rootpath = path.normalize();
39         let manifest = ProjectManifest::discover_single(&path)?;
40
41         let workspace = ProjectWorkspace::load(manifest, &cargo_config, no_progress)?;
42
43         let (host, vfs, _) = load_workspace(workspace, &cargo_config, &load_cargo_config)?;
44         let db = host.raw_database();
45         let analysis = host.analysis();
46
47         let si = StaticIndex::compute(&analysis);
48
49         let mut index = scip_types::Index {
50             metadata: Some(scip_types::Metadata {
51                 version: scip_types::ProtocolVersion::UnspecifiedProtocolVersion.into(),
52                 tool_info: Some(scip_types::ToolInfo {
53                     name: "rust-analyzer".to_owned(),
54                     version: "0.1".to_owned(),
55                     arguments: vec![],
56                     ..Default::default()
57                 })
58                 .into(),
59                 project_root: format!(
60                     "file://{}",
61                     path.normalize()
62                         .as_os_str()
63                         .to_str()
64                         .ok_or(anyhow::anyhow!("Unable to normalize project_root path"))?
65                         .to_string()
66                 ),
67                 text_document_encoding: scip_types::TextEncoding::UTF8.into(),
68                 ..Default::default()
69             })
70             .into(),
71             ..Default::default()
72         };
73
74         let mut symbols_emitted: HashSet<TokenId> = HashSet::default();
75         let mut tokens_to_symbol: HashMap<TokenId, String> = HashMap::new();
76
77         for file in si.files {
78             let mut local_count = 0;
79             let mut new_local_symbol = || {
80                 let new_symbol = scip::types::Symbol::new_local(local_count);
81                 local_count += 1;
82
83                 new_symbol
84             };
85
86             let StaticIndexedFile { file_id, tokens, .. } = file;
87             let relative_path = match get_relative_filepath(&vfs, &rootpath, file_id) {
88                 Some(relative_path) => relative_path,
89                 None => continue,
90             };
91
92             let line_index = LineIndex {
93                 index: db.line_index(file_id),
94                 encoding: OffsetEncoding::Utf8,
95                 endings: LineEndings::Unix,
96             };
97
98             let mut doc = scip_types::Document {
99                 relative_path,
100                 language: "rust".to_string(),
101                 ..Default::default()
102             };
103
104             tokens.into_iter().for_each(|(range, id)| {
105                 let token = si.tokens.get(id).unwrap();
106
107                 let mut occurrence = scip_types::Occurrence::default();
108                 occurrence.range = text_range_to_scip_range(&line_index, range);
109                 occurrence.symbol = match tokens_to_symbol.get(&id) {
110                     Some(symbol) => symbol.clone(),
111                     None => {
112                         let symbol = match &token.moniker {
113                             Some(moniker) => moniker_to_symbol(&moniker),
114                             None => new_local_symbol(),
115                         };
116
117                         let symbol = scip::symbol::format_symbol(symbol);
118                         tokens_to_symbol.insert(id, symbol.clone());
119                         symbol
120                     }
121                 };
122
123                 if let Some(def) = token.definition {
124                     if def.range == range {
125                         occurrence.symbol_roles |= scip_types::SymbolRole::Definition as i32;
126                     }
127
128                     if !symbols_emitted.contains(&id) {
129                         symbols_emitted.insert(id);
130
131                         let mut symbol_info = scip_types::SymbolInformation::default();
132                         symbol_info.symbol = occurrence.symbol.clone();
133                         if let Some(hover) = &token.hover {
134                             if !hover.markup.as_str().is_empty() {
135                                 symbol_info.documentation = vec![hover.markup.as_str().to_string()];
136                             }
137                         }
138
139                         doc.symbols.push(symbol_info)
140                     }
141                 }
142
143                 doc.occurrences.push(occurrence);
144             });
145
146             if doc.occurrences.is_empty() {
147                 continue;
148             }
149
150             index.documents.push(doc);
151         }
152
153         scip::write_message_to_file("index.scip", index)
154             .map_err(|err| anyhow::anyhow!("Failed to write scip to file: {}", err))?;
155
156         eprintln!("Generating SCIP finished {:?}", now.elapsed());
157         Ok(())
158     }
159 }
160
161 fn get_relative_filepath(
162     vfs: &vfs::Vfs,
163     rootpath: &vfs::AbsPathBuf,
164     file_id: ide::FileId,
165 ) -> Option<String> {
166     Some(vfs.file_path(file_id).as_path()?.strip_prefix(&rootpath)?.as_ref().to_str()?.to_string())
167 }
168
169 // SCIP Ranges have a (very large) optimization that ranges if they are on the same line
170 // only encode as a vector of [start_line, start_col, end_col].
171 //
172 // This transforms a line index into the optimized SCIP Range.
173 fn text_range_to_scip_range(line_index: &LineIndex, range: TextRange) -> Vec<i32> {
174     let LineCol { line: start_line, col: start_col } = line_index.index.line_col(range.start());
175     let LineCol { line: end_line, col: end_col } = line_index.index.line_col(range.end());
176
177     if start_line == end_line {
178         vec![start_line as i32, start_col as i32, end_col as i32]
179     } else {
180         vec![start_line as i32, start_col as i32, end_line as i32, end_col as i32]
181     }
182 }
183
184 fn new_descriptor_str(
185     name: &str,
186     suffix: scip_types::descriptor::Suffix,
187 ) -> scip_types::Descriptor {
188     scip_types::Descriptor {
189         name: name.to_string(),
190         disambiguator: "".to_string(),
191         suffix: suffix.into(),
192         ..Default::default()
193     }
194 }
195
196 fn new_descriptor(name: Name, suffix: scip_types::descriptor::Suffix) -> scip_types::Descriptor {
197     let mut name = name.to_string();
198     if name.contains("'") {
199         name = format!("`{}`", name);
200     }
201
202     new_descriptor_str(name.as_str(), suffix)
203 }
204
205 /// Loosely based on `def_to_moniker`
206 ///
207 /// Only returns a Symbol when it's a non-local symbol.
208 ///     So if the visibility isn't outside of a document, then it will return None
209 fn moniker_to_symbol(moniker: &MonikerResult) -> scip_types::Symbol {
210     use scip_types::descriptor::Suffix::*;
211
212     let package_name = moniker.package_information.name.clone();
213     let version = moniker.package_information.version.clone();
214     let descriptors = moniker
215         .identifier
216         .description
217         .iter()
218         .map(|desc| {
219             new_descriptor(
220                 desc.name.clone(),
221                 match desc.desc {
222                     MonikerDescriptorKind::Namespace => Namespace,
223                     MonikerDescriptorKind::Type => Type,
224                     MonikerDescriptorKind::Term => Term,
225                     MonikerDescriptorKind::Method => Method,
226                     MonikerDescriptorKind::TypeParameter => TypeParameter,
227                     MonikerDescriptorKind::Parameter => Parameter,
228                     MonikerDescriptorKind::Macro => Macro,
229                     MonikerDescriptorKind::Meta => Meta,
230                 },
231             )
232         })
233         .collect();
234
235     scip_types::Symbol {
236         scheme: "rust-analyzer".into(),
237         package: Some(scip_types::Package {
238             manager: "cargo".to_string(),
239             name: package_name,
240             version,
241             ..Default::default()
242         })
243         .into(),
244         descriptors,
245         ..Default::default()
246     }
247 }
248
249 #[cfg(test)]
250 mod test {
251     use super::*;
252     use hir::Semantics;
253     use ide::{AnalysisHost, FilePosition};
254     use ide_db::defs::IdentClass;
255     use ide_db::{base_db::fixture::ChangeFixture, helpers::pick_best_token};
256     use scip::symbol::format_symbol;
257     use syntax::SyntaxKind::*;
258     use syntax::{AstNode, T};
259
260     fn position(ra_fixture: &str) -> (AnalysisHost, FilePosition) {
261         let mut host = AnalysisHost::default();
262         let change_fixture = ChangeFixture::parse(ra_fixture);
263         host.raw_database_mut().apply_change(change_fixture.change);
264         let (file_id, range_or_offset) =
265             change_fixture.file_position.expect("expected a marker ($0)");
266         let offset = range_or_offset.expect_offset();
267         (host, FilePosition { file_id, offset })
268     }
269
270     /// If expected == "", then assert that there are no symbols (this is basically local symbol)
271     #[track_caller]
272     fn check_symbol(ra_fixture: &str, expected: &str) {
273         let (host, position) = position(ra_fixture);
274
275         let FilePosition { file_id, offset } = position;
276
277         let db = host.raw_database();
278         let sema = &Semantics::new(db);
279         let file = sema.parse(file_id).syntax().clone();
280         let original_token = pick_best_token(file.token_at_offset(offset), |kind| match kind {
281             IDENT
282             | INT_NUMBER
283             | LIFETIME_IDENT
284             | T![self]
285             | T![super]
286             | T![crate]
287             | T![Self]
288             | COMMENT => 2,
289             kind if kind.is_trivia() => 0,
290             _ => 1,
291         })
292         .expect("OK OK");
293
294         let navs = sema
295             .descend_into_macros(original_token.clone())
296             .into_iter()
297             .filter_map(|token| {
298                 IdentClass::classify_token(sema, &token).map(IdentClass::definitions).map(|it| {
299                     it.into_iter().flat_map(|def| {
300                         let module = def.module(db).unwrap();
301                         let current_crate = module.krate();
302
303                         match MonikerResult::from_def(sema.db, def, current_crate) {
304                             Some(moniker_result) => Some(moniker_to_symbol(&moniker_result)),
305                             None => None,
306                         }
307                     })
308                 })
309             })
310             .flatten()
311             .collect::<Vec<_>>();
312
313         if expected == "" {
314             assert_eq!(0, navs.len(), "must have no symbols {:?}", navs);
315             return;
316         }
317
318         assert_eq!(1, navs.len(), "must have one symbol {:?}", navs);
319
320         let res = navs.get(0).unwrap();
321         let formatted = format_symbol(res.clone());
322         assert_eq!(formatted, expected);
323     }
324
325     #[test]
326     fn basic() {
327         check_symbol(
328             r#"
329 //- /lib.rs crate:main deps:foo
330 use foo::example_mod::func;
331 fn main() {
332     func$0();
333 }
334 //- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git
335 pub mod example_mod {
336     pub fn func() {}
337 }
338 "#,
339             "rust-analyzer cargo foo 0.1.0 example_mod/func().",
340         );
341     }
342
343     #[test]
344     fn symbol_for_trait() {
345         check_symbol(
346             r#"
347 //- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git
348 pub mod module {
349     pub trait MyTrait {
350         pub fn func$0() {}
351     }
352 }
353 "#,
354             "rust-analyzer cargo foo 0.1.0 module/MyTrait#func().",
355         );
356     }
357
358     #[test]
359     fn symbol_for_trait_constant() {
360         check_symbol(
361             r#"
362     //- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git
363     pub mod module {
364         pub trait MyTrait {
365             const MY_CONST$0: u8;
366         }
367     }
368     "#,
369             "rust-analyzer cargo foo 0.1.0 module/MyTrait#MY_CONST.",
370         );
371     }
372
373     #[test]
374     fn symbol_for_trait_type() {
375         check_symbol(
376             r#"
377     //- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git
378     pub mod module {
379         pub trait MyTrait {
380             type MyType$0;
381         }
382     }
383     "#,
384             // "foo::module::MyTrait::MyType",
385             "rust-analyzer cargo foo 0.1.0 module/MyTrait#[MyType]",
386         );
387     }
388
389     #[test]
390     fn symbol_for_trait_impl_function() {
391         check_symbol(
392             r#"
393     //- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git
394     pub mod module {
395         pub trait MyTrait {
396             pub fn func() {}
397         }
398
399         struct MyStruct {}
400
401         impl MyTrait for MyStruct {
402             pub fn func$0() {}
403         }
404     }
405     "#,
406             // "foo::module::MyStruct::MyTrait::func",
407             "rust-analyzer cargo foo 0.1.0 module/MyStruct#MyTrait#func().",
408         );
409     }
410
411     #[test]
412     fn symbol_for_field() {
413         check_symbol(
414             r#"
415     //- /lib.rs crate:main deps:foo
416     use foo::St;
417     fn main() {
418         let x = St { a$0: 2 };
419     }
420     //- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git
421     pub struct St {
422         pub a: i32,
423     }
424     "#,
425             "rust-analyzer cargo foo 0.1.0 St#a.",
426         );
427     }
428
429     #[test]
430     fn local_symbol_for_local() {
431         check_symbol(
432             r#"
433     //- /lib.rs crate:main deps:foo
434     use foo::module::func;
435     fn main() {
436         func();
437     }
438     //- /foo/lib.rs crate:foo@CratesIo:0.1.0,https://a.b/foo.git
439     pub mod module {
440         pub fn func() {
441             let x$0 = 2;
442         }
443     }
444     "#,
445             "",
446         );
447     }
448 }