]>
Commit | Line | Data |
---|---|---|
f2b60f7d FG |
1 | //! SCIP generator |
2 | ||
3 | use std::{ | |
4 | collections::{HashMap, HashSet}, | |
fe692bf9 | 5 | path::PathBuf, |
f2b60f7d FG |
6 | time::Instant, |
7 | }; | |
8 | ||
f2b60f7d | 9 | use ide::{ |
2b03887a FG |
10 | LineCol, MonikerDescriptorKind, StaticIndex, StaticIndexedFile, TextRange, TokenId, |
11 | TokenStaticData, | |
f2b60f7d FG |
12 | }; |
13 | use ide_db::LineIndexDatabase; | |
4b012472 | 14 | use load_cargo::{load_workspace_at, LoadCargoConfig, ProcMacroServerChoice}; |
f2b60f7d | 15 | use scip::types as scip_types; |
f2b60f7d | 16 | |
add651ee FG |
17 | use crate::{ |
18 | cli::flags, | |
19 | line_index::{LineEndings, LineIndex, PositionEncoding}, | |
f2b60f7d FG |
20 | }; |
21 | ||
22 | impl flags::Scip { | |
add651ee | 23 | pub fn run(self) -> anyhow::Result<()> { |
f2b60f7d FG |
24 | eprintln!("Generating SCIP start..."); |
25 | let now = Instant::now(); | |
f2b60f7d | 26 | |
9c376795 | 27 | let no_progress = &|s| (eprintln!("rust-analyzer: Loading {s}")); |
f2b60f7d FG |
28 | let load_cargo_config = LoadCargoConfig { |
29 | load_out_dirs_from_check: true, | |
9ffffee4 | 30 | with_proc_macro_server: ProcMacroServerChoice::Sysroot, |
f2b60f7d FG |
31 | prefill_caches: true, |
32 | }; | |
4b012472 FG |
33 | let root = vfs::AbsPathBuf::assert(std::env::current_dir()?.join(&self.path)).normalize(); |
34 | ||
35 | let mut config = crate::config::Config::new( | |
36 | root.clone(), | |
37 | lsp_types::ClientCapabilities::default(), | |
38 | /* workspace_roots = */ vec![], | |
39 | /* is_visual_studio_code = */ false, | |
40 | ); | |
41 | ||
42 | if let Some(p) = self.config_path { | |
43 | let mut file = std::io::BufReader::new(std::fs::File::open(p)?); | |
44 | let json = serde_json::from_reader(&mut file)?; | |
45 | config.update(json)?; | |
46 | } | |
47 | let cargo_config = config.cargo(); | |
48 | let (host, vfs, _) = load_workspace_at( | |
49 | root.as_path().as_ref(), | |
50 | &cargo_config, | |
51 | &load_cargo_config, | |
52 | &no_progress, | |
53 | )?; | |
f2b60f7d FG |
54 | let db = host.raw_database(); |
55 | let analysis = host.analysis(); | |
56 | ||
57 | let si = StaticIndex::compute(&analysis); | |
58 | ||
487cf647 FG |
59 | let metadata = scip_types::Metadata { |
60 | version: scip_types::ProtocolVersion::UnspecifiedProtocolVersion.into(), | |
61 | tool_info: Some(scip_types::ToolInfo { | |
62 | name: "rust-analyzer".to_owned(), | |
781aab86 | 63 | version: format!("{}", crate::version::version()), |
487cf647 FG |
64 | arguments: vec![], |
65 | special_fields: Default::default(), | |
f2b60f7d FG |
66 | }) |
67 | .into(), | |
487cf647 FG |
68 | project_root: format!( |
69 | "file://{}", | |
4b012472 | 70 | root.as_os_str() |
487cf647 | 71 | .to_str() |
add651ee | 72 | .ok_or(anyhow::format_err!("Unable to normalize project_root path"))? |
487cf647 FG |
73 | ), |
74 | text_document_encoding: scip_types::TextEncoding::UTF8.into(), | |
75 | special_fields: Default::default(), | |
f2b60f7d | 76 | }; |
487cf647 | 77 | let mut documents = Vec::new(); |
f2b60f7d FG |
78 | |
79 | let mut symbols_emitted: HashSet<TokenId> = HashSet::default(); | |
80 | let mut tokens_to_symbol: HashMap<TokenId, String> = HashMap::new(); | |
81 | ||
2b03887a | 82 | for StaticIndexedFile { file_id, tokens, .. } in si.files { |
f2b60f7d FG |
83 | let mut local_count = 0; |
84 | let mut new_local_symbol = || { | |
85 | let new_symbol = scip::types::Symbol::new_local(local_count); | |
86 | local_count += 1; | |
87 | ||
88 | new_symbol | |
89 | }; | |
90 | ||
4b012472 | 91 | let relative_path = match get_relative_filepath(&vfs, &root, file_id) { |
f2b60f7d FG |
92 | Some(relative_path) => relative_path, |
93 | None => continue, | |
94 | }; | |
95 | ||
96 | let line_index = LineIndex { | |
97 | index: db.line_index(file_id), | |
487cf647 | 98 | encoding: PositionEncoding::Utf8, |
f2b60f7d FG |
99 | endings: LineEndings::Unix, |
100 | }; | |
101 | ||
487cf647 FG |
102 | let mut occurrences = Vec::new(); |
103 | let mut symbols = Vec::new(); | |
f2b60f7d | 104 | |
487cf647 | 105 | tokens.into_iter().for_each(|(text_range, id)| { |
f2b60f7d FG |
106 | let token = si.tokens.get(id).unwrap(); |
107 | ||
487cf647 FG |
108 | let range = text_range_to_scip_range(&line_index, text_range); |
109 | let symbol = tokens_to_symbol | |
2b03887a FG |
110 | .entry(id) |
111 | .or_insert_with(|| { | |
9c376795 | 112 | let symbol = token_to_symbol(token).unwrap_or_else(&mut new_local_symbol); |
2b03887a FG |
113 | scip::symbol::format_symbol(symbol) |
114 | }) | |
115 | .clone(); | |
f2b60f7d | 116 | |
487cf647 FG |
117 | let mut symbol_roles = Default::default(); |
118 | ||
f2b60f7d | 119 | if let Some(def) = token.definition { |
487cf647 FG |
120 | if def.range == text_range { |
121 | symbol_roles |= scip_types::SymbolRole::Definition as i32; | |
f2b60f7d FG |
122 | } |
123 | ||
2b03887a | 124 | if symbols_emitted.insert(id) { |
487cf647 FG |
125 | let documentation = token |
126 | .hover | |
127 | .as_ref() | |
128 | .map(|hover| hover.markup.as_str()) | |
129 | .filter(|it| !it.is_empty()) | |
130 | .map(|it| vec![it.to_owned()]); | |
131 | let symbol_info = scip_types::SymbolInformation { | |
132 | symbol: symbol.clone(), | |
133 | documentation: documentation.unwrap_or_default(), | |
134 | relationships: Vec::new(), | |
135 | special_fields: Default::default(), | |
4b012472 FG |
136 | kind: Default::default(), |
137 | display_name: String::new(), | |
138 | signature_documentation: Default::default(), | |
139 | enclosing_symbol: String::new(), | |
487cf647 FG |
140 | }; |
141 | ||
142 | symbols.push(symbol_info) | |
f2b60f7d FG |
143 | } |
144 | } | |
145 | ||
487cf647 FG |
146 | occurrences.push(scip_types::Occurrence { |
147 | range, | |
148 | symbol, | |
149 | symbol_roles, | |
150 | override_documentation: Vec::new(), | |
151 | syntax_kind: Default::default(), | |
152 | diagnostics: Vec::new(), | |
153 | special_fields: Default::default(), | |
4b012472 | 154 | enclosing_range: Vec::new(), |
487cf647 | 155 | }); |
f2b60f7d FG |
156 | }); |
157 | ||
487cf647 | 158 | if occurrences.is_empty() { |
f2b60f7d FG |
159 | continue; |
160 | } | |
161 | ||
487cf647 FG |
162 | documents.push(scip_types::Document { |
163 | relative_path, | |
164 | language: "rust".to_string(), | |
165 | occurrences, | |
166 | symbols, | |
167 | special_fields: Default::default(), | |
4b012472 | 168 | text: String::new(), |
487cf647 | 169 | }); |
f2b60f7d FG |
170 | } |
171 | ||
487cf647 FG |
172 | let index = scip_types::Index { |
173 | metadata: Some(metadata).into(), | |
174 | documents, | |
175 | external_symbols: Vec::new(), | |
176 | special_fields: Default::default(), | |
177 | }; | |
178 | ||
fe692bf9 FG |
179 | let out_path = self.output.unwrap_or_else(|| PathBuf::from(r"index.scip")); |
180 | scip::write_message_to_file(out_path, index) | |
add651ee | 181 | .map_err(|err| anyhow::format_err!("Failed to write scip to file: {}", err))?; |
f2b60f7d FG |
182 | |
183 | eprintln!("Generating SCIP finished {:?}", now.elapsed()); | |
184 | Ok(()) | |
185 | } | |
186 | } | |
187 | ||
188 | fn get_relative_filepath( | |
189 | vfs: &vfs::Vfs, | |
190 | rootpath: &vfs::AbsPathBuf, | |
191 | file_id: ide::FileId, | |
192 | ) -> Option<String> { | |
9c376795 | 193 | Some(vfs.file_path(file_id).as_path()?.strip_prefix(rootpath)?.as_ref().to_str()?.to_string()) |
f2b60f7d FG |
194 | } |
195 | ||
196 | // SCIP Ranges have a (very large) optimization that ranges if they are on the same line | |
197 | // only encode as a vector of [start_line, start_col, end_col]. | |
198 | // | |
199 | // This transforms a line index into the optimized SCIP Range. | |
200 | fn text_range_to_scip_range(line_index: &LineIndex, range: TextRange) -> Vec<i32> { | |
201 | let LineCol { line: start_line, col: start_col } = line_index.index.line_col(range.start()); | |
202 | let LineCol { line: end_line, col: end_col } = line_index.index.line_col(range.end()); | |
203 | ||
204 | if start_line == end_line { | |
205 | vec![start_line as i32, start_col as i32, end_col as i32] | |
206 | } else { | |
207 | vec![start_line as i32, start_col as i32, end_line as i32, end_col as i32] | |
208 | } | |
209 | } | |
210 | ||
211 | fn new_descriptor_str( | |
212 | name: &str, | |
213 | suffix: scip_types::descriptor::Suffix, | |
214 | ) -> scip_types::Descriptor { | |
215 | scip_types::Descriptor { | |
216 | name: name.to_string(), | |
217 | disambiguator: "".to_string(), | |
218 | suffix: suffix.into(), | |
487cf647 | 219 | special_fields: Default::default(), |
f2b60f7d FG |
220 | } |
221 | } | |
222 | ||
fe692bf9 FG |
223 | fn new_descriptor(name: &str, suffix: scip_types::descriptor::Suffix) -> scip_types::Descriptor { |
224 | if name.contains('\'') { | |
225 | new_descriptor_str(&format!("`{name}`"), suffix) | |
226 | } else { | |
227 | new_descriptor_str(&name, suffix) | |
f2b60f7d | 228 | } |
f2b60f7d FG |
229 | } |
230 | ||
231 | /// Loosely based on `def_to_moniker` | |
232 | /// | |
233 | /// Only returns a Symbol when it's a non-local symbol. | |
234 | /// So if the visibility isn't outside of a document, then it will return None | |
2b03887a | 235 | fn token_to_symbol(token: &TokenStaticData) -> Option<scip_types::Symbol> { |
f2b60f7d FG |
236 | use scip_types::descriptor::Suffix::*; |
237 | ||
2b03887a FG |
238 | let moniker = token.moniker.as_ref()?; |
239 | ||
f2b60f7d FG |
240 | let package_name = moniker.package_information.name.clone(); |
241 | let version = moniker.package_information.version.clone(); | |
242 | let descriptors = moniker | |
243 | .identifier | |
244 | .description | |
245 | .iter() | |
246 | .map(|desc| { | |
247 | new_descriptor( | |
fe692bf9 | 248 | &desc.name, |
f2b60f7d FG |
249 | match desc.desc { |
250 | MonikerDescriptorKind::Namespace => Namespace, | |
251 | MonikerDescriptorKind::Type => Type, | |
252 | MonikerDescriptorKind::Term => Term, | |
253 | MonikerDescriptorKind::Method => Method, | |
254 | MonikerDescriptorKind::TypeParameter => TypeParameter, | |
255 | MonikerDescriptorKind::Parameter => Parameter, | |
256 | MonikerDescriptorKind::Macro => Macro, | |
257 | MonikerDescriptorKind::Meta => Meta, | |
258 | }, | |
259 | ) | |
260 | }) | |
261 | .collect(); | |
262 | ||
2b03887a | 263 | Some(scip_types::Symbol { |
f2b60f7d FG |
264 | scheme: "rust-analyzer".into(), |
265 | package: Some(scip_types::Package { | |
266 | manager: "cargo".to_string(), | |
267 | name: package_name, | |
487cf647 FG |
268 | version: version.unwrap_or_else(|| ".".to_string()), |
269 | special_fields: Default::default(), | |
f2b60f7d FG |
270 | }) |
271 | .into(), | |
272 | descriptors, | |
487cf647 | 273 | special_fields: Default::default(), |
2b03887a | 274 | }) |
f2b60f7d FG |
275 | } |
276 | ||
277 | #[cfg(test)] | |
278 | mod test { | |
279 | use super::*; | |
2b03887a FG |
280 | use ide::{AnalysisHost, FilePosition, StaticIndex, TextSize}; |
281 | use ide_db::base_db::fixture::ChangeFixture; | |
f2b60f7d | 282 | use scip::symbol::format_symbol; |
f2b60f7d FG |
283 | |
284 | fn position(ra_fixture: &str) -> (AnalysisHost, FilePosition) { | |
285 | let mut host = AnalysisHost::default(); | |
286 | let change_fixture = ChangeFixture::parse(ra_fixture); | |
287 | host.raw_database_mut().apply_change(change_fixture.change); | |
288 | let (file_id, range_or_offset) = | |
add651ee | 289 | change_fixture.file_position.expect("expected a marker ()"); |
f2b60f7d FG |
290 | let offset = range_or_offset.expect_offset(); |
291 | (host, FilePosition { file_id, offset }) | |
292 | } | |
293 | ||
294 | /// If expected == "", then assert that there are no symbols (this is basically local symbol) | |
295 | #[track_caller] | |
296 | fn check_symbol(ra_fixture: &str, expected: &str) { | |
297 | let (host, position) = position(ra_fixture); | |
298 | ||
2b03887a FG |
299 | let analysis = host.analysis(); |
300 | let si = StaticIndex::compute(&analysis); | |
301 | ||
f2b60f7d FG |
302 | let FilePosition { file_id, offset } = position; |
303 | ||
2b03887a FG |
304 | let mut found_symbol = None; |
305 | for file in &si.files { | |
306 | if file.file_id != file_id { | |
307 | continue; | |
308 | } | |
309 | for &(range, id) in &file.tokens { | |
310 | if range.contains(offset - TextSize::from(1)) { | |
311 | let token = si.tokens.get(id).unwrap(); | |
312 | found_symbol = token_to_symbol(token); | |
313 | break; | |
314 | } | |
315 | } | |
316 | } | |
f2b60f7d FG |
317 | |
318 | if expected == "" { | |
9c376795 | 319 | assert!(found_symbol.is_none(), "must have no symbols {found_symbol:?}"); |
f2b60f7d FG |
320 | return; |
321 | } | |
322 | ||
9c376795 | 323 | assert!(found_symbol.is_some(), "must have one symbol {found_symbol:?}"); |
2b03887a FG |
324 | let res = found_symbol.unwrap(); |
325 | let formatted = format_symbol(res); | |
f2b60f7d FG |
326 | assert_eq!(formatted, expected); |
327 | } | |
328 | ||
329 | #[test] | |
330 | fn basic() { | |
331 | check_symbol( | |
332 | r#" | |
333 | //- /lib.rs crate:main deps:foo | |
334 | use foo::example_mod::func; | |
335 | fn main() { | |
336 | func$0(); | |
337 | } | |
add651ee | 338 | //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library |
f2b60f7d FG |
339 | pub mod example_mod { |
340 | pub fn func() {} | |
341 | } | |
342 | "#, | |
343 | "rust-analyzer cargo foo 0.1.0 example_mod/func().", | |
344 | ); | |
345 | } | |
346 | ||
347 | #[test] | |
348 | fn symbol_for_trait() { | |
349 | check_symbol( | |
350 | r#" | |
add651ee | 351 | //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library |
f2b60f7d FG |
352 | pub mod module { |
353 | pub trait MyTrait { | |
354 | pub fn func$0() {} | |
355 | } | |
356 | } | |
357 | "#, | |
358 | "rust-analyzer cargo foo 0.1.0 module/MyTrait#func().", | |
359 | ); | |
360 | } | |
361 | ||
362 | #[test] | |
363 | fn symbol_for_trait_constant() { | |
364 | check_symbol( | |
365 | r#" | |
add651ee | 366 | //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library |
f2b60f7d FG |
367 | pub mod module { |
368 | pub trait MyTrait { | |
369 | const MY_CONST$0: u8; | |
370 | } | |
371 | } | |
372 | "#, | |
373 | "rust-analyzer cargo foo 0.1.0 module/MyTrait#MY_CONST.", | |
374 | ); | |
375 | } | |
376 | ||
377 | #[test] | |
378 | fn symbol_for_trait_type() { | |
379 | check_symbol( | |
380 | r#" | |
add651ee | 381 | //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library |
f2b60f7d FG |
382 | pub mod module { |
383 | pub trait MyTrait { | |
384 | type MyType$0; | |
385 | } | |
386 | } | |
387 | "#, | |
388 | // "foo::module::MyTrait::MyType", | |
389 | "rust-analyzer cargo foo 0.1.0 module/MyTrait#[MyType]", | |
390 | ); | |
391 | } | |
392 | ||
393 | #[test] | |
394 | fn symbol_for_trait_impl_function() { | |
395 | check_symbol( | |
396 | r#" | |
add651ee | 397 | //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library |
f2b60f7d FG |
398 | pub mod module { |
399 | pub trait MyTrait { | |
400 | pub fn func() {} | |
401 | } | |
402 | ||
403 | struct MyStruct {} | |
404 | ||
405 | impl MyTrait for MyStruct { | |
406 | pub fn func$0() {} | |
407 | } | |
408 | } | |
409 | "#, | |
410 | // "foo::module::MyStruct::MyTrait::func", | |
411 | "rust-analyzer cargo foo 0.1.0 module/MyStruct#MyTrait#func().", | |
412 | ); | |
413 | } | |
414 | ||
415 | #[test] | |
416 | fn symbol_for_field() { | |
417 | check_symbol( | |
418 | r#" | |
419 | //- /lib.rs crate:main deps:foo | |
420 | use foo::St; | |
421 | fn main() { | |
422 | let x = St { a$0: 2 }; | |
423 | } | |
add651ee | 424 | //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library |
f2b60f7d FG |
425 | pub struct St { |
426 | pub a: i32, | |
427 | } | |
428 | "#, | |
429 | "rust-analyzer cargo foo 0.1.0 St#a.", | |
430 | ); | |
431 | } | |
432 | ||
add651ee FG |
433 | #[test] |
434 | fn symbol_for_param() { | |
435 | check_symbol( | |
436 | r#" | |
437 | //- /lib.rs crate:main deps:foo | |
438 | use foo::example_mod::func; | |
439 | fn main() { | |
440 | func(42); | |
441 | } | |
442 | //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library | |
443 | pub mod example_mod { | |
444 | pub fn func(x$0: usize) {} | |
445 | } | |
446 | "#, | |
447 | "rust-analyzer cargo foo 0.1.0 example_mod/func().(x)", | |
448 | ); | |
449 | } | |
450 | ||
451 | #[test] | |
452 | fn symbol_for_closure_param() { | |
453 | check_symbol( | |
454 | r#" | |
455 | //- /lib.rs crate:main deps:foo | |
456 | use foo::example_mod::func; | |
457 | fn main() { | |
458 | func(); | |
459 | } | |
460 | //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library | |
461 | pub mod example_mod { | |
462 | pub fn func() { | |
463 | let f = |x$0: usize| {}; | |
464 | } | |
465 | } | |
466 | "#, | |
467 | "rust-analyzer cargo foo 0.1.0 example_mod/func().(x)", | |
468 | ); | |
469 | } | |
470 | ||
f2b60f7d FG |
471 | #[test] |
472 | fn local_symbol_for_local() { | |
473 | check_symbol( | |
474 | r#" | |
475 | //- /lib.rs crate:main deps:foo | |
476 | use foo::module::func; | |
477 | fn main() { | |
478 | func(); | |
479 | } | |
add651ee | 480 | //- /foo/lib.rs crate:foo@0.1.0,https://a.b/foo.git library |
f2b60f7d FG |
481 | pub mod module { |
482 | pub fn func() { | |
483 | let x$0 = 2; | |
484 | } | |
485 | } | |
486 | "#, | |
487 | "", | |
488 | ); | |
489 | } | |
487cf647 FG |
490 | |
491 | #[test] | |
492 | fn global_symbol_for_pub_struct() { | |
493 | check_symbol( | |
494 | r#" | |
495 | //- /lib.rs crate:main | |
496 | mod foo; | |
497 | ||
498 | fn main() { | |
499 | let _bar = foo::Bar { i: 0 }; | |
500 | } | |
501 | //- /foo.rs | |
502 | pub struct Bar$0 { | |
503 | pub i: i32, | |
504 | } | |
505 | "#, | |
506 | "rust-analyzer cargo main . foo/Bar#", | |
507 | ); | |
508 | } | |
509 | ||
510 | #[test] | |
511 | fn global_symbol_for_pub_struct_reference() { | |
512 | check_symbol( | |
513 | r#" | |
514 | //- /lib.rs crate:main | |
515 | mod foo; | |
516 | ||
517 | fn main() { | |
518 | let _bar = foo::Bar$0 { i: 0 }; | |
519 | } | |
520 | //- /foo.rs | |
521 | pub struct Bar { | |
522 | pub i: i32, | |
523 | } | |
524 | "#, | |
525 | "rust-analyzer cargo main . foo/Bar#", | |
526 | ); | |
527 | } | |
f2b60f7d | 528 | } |