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