1 use std::path::{Path, PathBuf};
2
3 use log::*;
4 use rls_analysis::{Def, DefKind};
5 use rls_span::{Column, Range, Row, Span, ZeroIndexed};
6 use rls_vfs::{self as vfs, Vfs};
7 use rustfmt_nightly::NewlineStyle;
8 use serde_derive::{Deserialize, Serialize};
9
10 use crate::actions::format::Rustfmt;
11 use crate::actions::requests;
12 use crate::actions::InitActionContext;
13 use crate::config::FmtConfig;
14 use crate::lsp_data::*;
15 use crate::server::ResponseError;
16
17 #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
18 pub struct Tooltip {
19 pub contents: Vec<MarkedString>,
20 pub range: Range<ZeroIndexed>,
21 }
22
23 /// Cleanup documentation code blocks. The `docs` are expected to have
24 /// the preceding `///` or `//!` prefixes already trimmed away. Rust code
25 /// blocks will ignore lines beginning with `#`. Code block annotations
26 /// that are common to Rust will be converted to `rust` allow for markdown
27 /// syntax coloring.
process_docs(docs: &str) -> String28 pub fn process_docs(docs: &str) -> String {
29 trace!("process_docs");
30 let mut in_codeblock = false;
31 let mut in_rust_codeblock = false;
32 let mut processed_docs = Vec::new();
33 let mut last_line_ignored = false;
34 for line in docs.lines() {
35 let trimmed = line.trim();
36 if trimmed.starts_with("```") {
37 in_rust_codeblock = trimmed == "```"
38 || trimmed.contains("rust")
39 || trimmed.contains("no_run")
40 || trimmed.contains("ignore")
41 || trimmed.contains("should_panic")
42 || trimmed.contains("compile_fail");
43 in_codeblock = !in_codeblock;
44 if !in_codeblock {
45 in_rust_codeblock = false;
46 }
47 }
48 let line = if in_rust_codeblock && trimmed.starts_with("```") {
49 "```rust".into()
50 } else {
51 line.to_string()
52 };
53
54 // Racer sometimes pulls out comment block headers from the standard library.
55 let ignore_slashes = line.starts_with("////");
56
57 let maybe_attribute = trimmed.starts_with("#[") || trimmed.starts_with("#![");
58 let is_attribute = maybe_attribute && in_rust_codeblock;
59 let is_hidden = trimmed.starts_with('#') && in_rust_codeblock && !is_attribute;
60
61 let ignore_whitespace = last_line_ignored && trimmed.is_empty();
62 let ignore_line = ignore_slashes || ignore_whitespace || is_hidden;
63
64 if !ignore_line {
65 processed_docs.push(line);
66 last_line_ignored = false;
67 } else {
68 last_line_ignored = true;
69 }
70 }
71
72 processed_docs.join("\n")
73 }
74
75 /// Extracts documentation from the `file` at the specified `row_start`.
76 /// If the row is equal to `0`, the scan will include the current row
77 /// and move _downward_. Otherwise, the scan will ignore the specified
78 /// row and move _upward_.
extract_docs( vfs: &Vfs, file: &Path, row_start: Row<ZeroIndexed>, ) -> Result<Vec<String>, vfs::Error>79 pub fn extract_docs(
80 vfs: &Vfs,
81 file: &Path,
82 row_start: Row<ZeroIndexed>,
83 ) -> Result<Vec<String>, vfs::Error> {
84 let up = row_start.0 > 0;
85 debug!("extract_docs: row_start = {:?}, up = {:?}, file = {:?}", row_start, up, file);
86
87 let mut docs: Vec<String> = Vec::new();
88 let mut row = if up {
89 Row::new_zero_indexed(row_start.0.saturating_sub(1))
90 } else {
91 Row::new_zero_indexed(row_start.0)
92 };
93 let mut in_meta = false;
94 let mut hit_top = false;
95 loop {
96 let line = vfs.load_line(file, row)?;
97
98 let next_row = if up {
99 Row::new_zero_indexed(row.0.saturating_sub(1))
100 } else {
101 Row::new_zero_indexed(row.0.saturating_add(1))
102 };
103
104 if row == next_row {
105 hit_top = true;
106 } else {
107 row = next_row;
108 }
109
110 let line = line.trim();
111
112 let attr_start = line.starts_with("#[") || line.starts_with("#![");
113
114 if attr_start && line.ends_with(']') && !hit_top {
115 // Ignore single-line attributes.
116 trace!(
117 "extract_docs: ignoring single-line attribute, next_row: {:?}, up: {}",
118 next_row,
119 up
120 );
121 continue;
122 }
123
124 // Continue with the next line when transitioning out of a
125 // multi-line attribute.
126 if attr_start || (line.ends_with(']') && !line.starts_with("//")) {
127 in_meta = !in_meta;
128 if !in_meta && !hit_top {
129 trace!(
130 "extract_docs: exiting multi-line attribute, next_row: {:?}, up: {}",
131 next_row,
132 up
133 );
134 continue;
135 };
136 }
137
138 if !hit_top && in_meta {
139 // Ignore milti-line attributes.
140 trace!(
141 "extract_docs: ignoring multi-line attribute, next_row: {:?}, up: {}, in_meta: {}",
142 next_row,
143 up,
144 in_meta
145 );
146 continue;
147 } else if line.starts_with("////") {
148 trace!(
149 "extract_docs: break on comment header block, next_row: {:?}, up: {}",
150 next_row,
151 up
152 );
153 break;
154 } else if line.starts_with("///") && !up {
155 trace!("extract_docs: break on non-module docs, next_row: {:?}, up: {}", next_row, up);
156 break;
157 } else if line.starts_with("//!") && up {
158 trace!("extract_docs: break on module docs, next_row: {:?}, up: {}", next_row, up);
159 break;
160 } else if line.starts_with("///") || line.starts_with("//!") {
161 let pos =
162 if line.chars().nth(3).map(char::is_whitespace).unwrap_or(false) { 4 } else { 3 };
163 let doc_line = line[pos..].into();
164 if up {
165 docs.insert(0, doc_line);
166 } else {
167 docs.push(doc_line);
168 }
169 }
170
171 if hit_top {
172 // The top of the file was reached.
173 debug!(
174 "extract_docs: bailing out: prev_row == next_row; next_row = {:?}, up = {}",
175 next_row, up
176 );
177 break;
178 } else if line.starts_with("//") {
179 trace!(
180 "extract_docs: continuing after comment line, next_row: {:?}, up: {}",
181 next_row,
182 up
183 );
184 continue;
185 } else if line.is_empty() {
186 trace!(
187 "extract_docs: continuing after empty line, next_row: {:?}, up: {}",
188 next_row,
189 up
190 );
191 continue;
192 } else {
193 trace!("extract_docs: end of docs, next_row: {:?}, up: {}", next_row, up);
194 break;
195 }
196 }
197 debug!(
198 "extract_docs: complete: row_end = {:?} (exclusive), up = {:?}, file = {:?}",
199 row, up, file
200 );
201 Ok(docs)
202 }
203
extract_and_process_docs(vfs: &Vfs, file: &Path, row_start: Row<ZeroIndexed>) -> Option<String>204 fn extract_and_process_docs(vfs: &Vfs, file: &Path, row_start: Row<ZeroIndexed>) -> Option<String> {
205 extract_docs(vfs, &file, row_start)
206 .map_err(|e| {
207 error!("failed to extract docs: row: {:?}, file: {:?} ({:?})", row_start, file, e);
208 })
209 .ok()
210 .map(|docs| docs.join("\n"))
211 .map(|docs| process_docs(&docs))
212 .and_then(empty_to_none)
213 }
214
215 /// Extracts a function, method, struct, enum, or trait declaration from source.
extract_decl( vfs: &Vfs, file: &Path, mut row: Row<ZeroIndexed>, ) -> Result<Vec<String>, vfs::Error>216 pub fn extract_decl(
217 vfs: &Vfs,
218 file: &Path,
219 mut row: Row<ZeroIndexed>,
220 ) -> Result<Vec<String>, vfs::Error> {
221 debug!("extract_decl: row_start: {:?}, file: {:?}", row, file);
222 let mut lines = Vec::new();
223 loop {
224 match vfs.load_line(file, row) {
225 Ok(line) => {
226 row = Row::new_zero_indexed(row.0.saturating_add(1));
227 let mut line = line.trim();
228 if let Some(pos) = line.rfind('{') {
229 line = &line[0..pos].trim_end();
230 lines.push(line.into());
231 break;
232 } else if line.ends_with(';') {
233 let pos = line.len() - 1;
234 line = &line[0..pos].trim_end();
235 lines.push(line.into());
236 break;
237 } else {
238 lines.push(line.into());
239 }
240 }
241 Err(e) => {
242 error!("extract_decl: error: {:?}", e);
243 return Err(e);
244 }
245 }
246 }
247 Ok(lines)
248 }
249
tooltip_local_variable_usage( ctx: &InitActionContext, def: &Def, doc_url: Option<String>, ) -> Vec<MarkedString>250 fn tooltip_local_variable_usage(
251 ctx: &InitActionContext,
252 def: &Def,
253 doc_url: Option<String>,
254 ) -> Vec<MarkedString> {
255 debug!("tooltip_local_variable_usage: {}", def.name);
256
257 let the_type = def.value.trim().into();
258 let mut context = String::new();
259 if ctx.config.lock().unwrap().show_hover_context {
260 match ctx.vfs.load_line(&def.span.file, def.span.range.row_start) {
261 Ok(line) => {
262 context.push_str(line.trim());
263 }
264 Err(e) => {
265 error!("tooltip_local_variable_usage: error = {:?}", e);
266 }
267 }
268 if context.ends_with('{') {
269 context.push_str(" ... }");
270 }
271 }
272
273 let context = empty_to_none(context);
274 let docs = None;
275
276 create_tooltip(the_type, doc_url, context, docs)
277 }
278
tooltip_type(ctx: &InitActionContext, def: &Def, doc_url: Option<String>) -> Vec<MarkedString>279 fn tooltip_type(ctx: &InitActionContext, def: &Def, doc_url: Option<String>) -> Vec<MarkedString> {
280 debug!("tooltip_type: {}", def.name);
281
282 let vfs = &ctx.vfs;
283
284 let the_type = || def.value.trim().into();
285 let the_type = def_decl(def, &vfs, the_type);
286 let docs = def_docs(def, &vfs);
287 let context = None;
288
289 create_tooltip(the_type, doc_url, context, docs)
290 }
291
tooltip_field_or_variant( ctx: &InitActionContext, def: &Def, doc_url: Option<String>, ) -> Vec<MarkedString>292 fn tooltip_field_or_variant(
293 ctx: &InitActionContext,
294 def: &Def,
295 doc_url: Option<String>,
296 ) -> Vec<MarkedString> {
297 debug!("tooltip_field_or_variant: {}", def.name);
298
299 let vfs = &ctx.vfs;
300
301 let the_type = def.value.trim().into();
302 let docs = def_docs(def, &vfs);
303 let context = None;
304
305 create_tooltip(the_type, doc_url, context, docs)
306 }
307
tooltip_struct_enum_union_trait( ctx: &InitActionContext, def: &Def, doc_url: Option<String>, ) -> Vec<MarkedString>308 fn tooltip_struct_enum_union_trait(
309 ctx: &InitActionContext,
310 def: &Def,
311 doc_url: Option<String>,
312 ) -> Vec<MarkedString> {
313 debug!("tooltip_struct_enum_union_trait: {}", def.name);
314
315 let vfs = &ctx.vfs;
316 let fmt_config = ctx.fmt_config();
317 // We hover often, so use the in-process one to speed things up.
318 let fmt = Rustfmt::Internal;
319
320 // Fallback in case source extration fails.
321 let the_type = || match def.kind {
322 DefKind::Struct => format!("struct {}", def.name),
323 DefKind::Enum => format!("enum {}", def.name),
324 DefKind::Union => format!("union {}", def.name),
325 DefKind::Trait => format!("trait {}", def.value),
326 _ => def.value.trim().to_string(),
327 };
328
329 let decl = def_decl(def, &vfs, the_type);
330
331 let the_type = format_object(fmt, &fmt_config, decl);
332 let docs = def_docs(def, &vfs);
333 let context = None;
334
335 create_tooltip(the_type, doc_url, context, docs)
336 }
337
tooltip_mod(ctx: &InitActionContext, def: &Def, doc_url: Option<String>) -> Vec<MarkedString>338 fn tooltip_mod(ctx: &InitActionContext, def: &Def, doc_url: Option<String>) -> Vec<MarkedString> {
339 debug!("tooltip_mod: name: {}", def.name);
340
341 let vfs = &ctx.vfs;
342
343 let the_type = def.value.trim();
344 let the_type = the_type.replace("\\\\", "/");
345 let the_type = the_type.replace("\\", "/");
346
347 let mod_path = if let Some(dir) = ctx.current_project.file_name() {
348 if Path::new(&the_type).starts_with(dir) {
349 the_type.chars().skip(dir.len() + 1).collect()
350 } else {
351 the_type
352 }
353 } else {
354 the_type
355 };
356
357 let docs = def_docs(def, &vfs);
358 let context = None;
359
360 create_tooltip(mod_path, doc_url, context, docs)
361 }
362
363 fn tooltip_function_method(
364 ctx: &InitActionContext,
365 def: &Def,
366 doc_url: Option<String>,
367 ) -> Vec<MarkedString> {
368 debug!("tooltip_function_method: {}", def.name);
369
370 let vfs = &ctx.vfs;
371 let fmt_config = ctx.fmt_config();
372 // We hover often, so use the in-process one to speed things up.
373 let fmt = Rustfmt::Internal;
374
375 let the_type = || {
376 def.value
377 .trim()
378 .replacen("fn ", &format!("fn {}", def.name), 1)
379 .replace("> (", ">(")
380 .replace("->(", "-> (")
381 };
382
383 let decl = def_decl(def, &vfs, the_type);
384
385 let the_type = format_method(fmt, &fmt_config, decl);
386 let docs = def_docs(def, &vfs);
387 let context = None;
388
389 create_tooltip(the_type, doc_url, context, docs)
390 }
391
392 fn tooltip_local_variable_decl(
393 _ctx: &InitActionContext,
394 def: &Def,
395 doc_url: Option<String>,
396 ) -> Vec<MarkedString> {
397 debug!("tooltip_local_variable_decl: {}", def.name);
398
399 let the_type = def.value.trim().into();
400 let docs = None;
401 let context = None;
402
403 create_tooltip(the_type, doc_url, context, docs)
404 }
405
406 fn tooltip_function_arg_usage(
407 _ctx: &InitActionContext,
408 def: &Def,
409 doc_url: Option<String>,
410 ) -> Vec<MarkedString> {
411 debug!("tooltip_function_arg_usage: {}", def.name);
412
413 let the_type = def.value.trim().into();
414 let docs = None;
415 let context = None;
416
417 create_tooltip(the_type, doc_url, context, docs)
418 }
419
420 fn tooltip_function_signature_arg(
421 _ctx: &InitActionContext,
422 def: &Def,
423 doc_url: Option<String>,
424 ) -> Vec<MarkedString> {
425 debug!("tooltip_function_signature_arg: {}", def.name);
426
427 let the_type = def.value.trim().into();
428 let docs = None;
429 let context = None;
430
431 create_tooltip(the_type, doc_url, context, docs)
432 }
433
434 fn tooltip_static_const_decl(
435 ctx: &InitActionContext,
436 def: &Def,
437 doc_url: Option<String>,
438 ) -> Vec<MarkedString> {
439 debug!("tooltip_static_const_decl: {}", def.name);
440
441 let vfs = &ctx.vfs;
442
443 let the_type = def_decl(def, &vfs, || def.value.trim().into());
444 let docs = def_docs(def, &vfs);
445 let context = None;
446
447 create_tooltip(the_type, doc_url, context, docs)
448 }
449
450 fn empty_to_none(s: String) -> Option<String> {
451 if s.trim().is_empty() {
452 None
453 } else {
454 Some(s)
455 }
456 }
457
458 /// Extracts and processes source documentation for the give `def`.
459 fn def_docs(def: &Def, vfs: &Vfs) -> Option<String> {
460 let save_analysis_docs = || empty_to_none(def.docs.trim().into());
461 extract_and_process_docs(&vfs, def.span.file.as_ref(), def.span.range.row_start)
462 .or_else(save_analysis_docs)
463 .filter(|docs| !docs.trim().is_empty())
464 }
465
466 /// Returns the type or function declaration from source. If source
467 /// extraction fails, the result of `the_type` is used as a fallback.
468 fn def_decl<F>(def: &Def, vfs: &Vfs, the_type: F) -> String
469 where
470 F: FnOnce() -> String,
471 {
472 extract_decl(vfs, &def.span.file, def.span.range.row_start)
473 .map(|lines| lines.join("\n"))
474 .ok()
475 .or_else(|| Some(the_type()))
476 .unwrap()
477 }
478
479 /// Creates a tooltip using the function, type or other declaration and
480 /// optional doc URL, context, or markdown documentation. No additional
481 /// processing or formatting is performed.
482 fn create_tooltip(
483 the_type: String,
484 doc_url: Option<String>,
485 context: Option<String>,
486 docs: Option<String>,
487 ) -> Vec<MarkedString> {
488 let mut tooltip = vec![];
489 let rust = "rust".to_string();
490 if !the_type.trim().is_empty() {
491 tooltip.push(MarkedString::from_language_code(rust.clone(), the_type));
492 }
493 if let Some(doc_url) = doc_url {
494 tooltip.push(MarkedString::from_markdown(doc_url));
495 }
496 if let Some(context) = context {
497 tooltip.push(MarkedString::from_language_code(rust, context));
498 }
499 if let Some(docs) = docs {
500 tooltip.push(MarkedString::from_markdown(docs));
501 }
502 tooltip
503 }
504
505 /// Collapses parent directory references inside of paths.
506 ///
507 /// # Example
508 ///
509 /// ```ignore
510 /// # use std::path::PathBuf;
511 ///
512 /// let path = PathBuf::from("libstd/../liballoc/string.rs");
513 /// let collapsed = collapse_parents(path);
514 /// let expected = PathBuf::from("liballoc/string.rs");
515 /// assert_eq!(expected, collapsed);
516 /// ```
517 fn collapse_parents(path: PathBuf) -> PathBuf {
518 use std::path::Component;
519 let mut components = Vec::new();
520 let mut skip;
521 let mut skip_prev = false;
522 for comp in path.components().rev() {
523 if comp == Component::ParentDir {
524 skip = true;
525 } else {
526 skip = false;
527 }
528 if !skip && !skip_prev {
529 components.insert(0, comp);
530 }
531 skip_prev = skip;
532 }
533
534 components.iter().fold(PathBuf::new(), |mut path, comp| {
535 path.push(comp);
536 path
537 })
538 }
539
540 /// Converts a racer `Match` to a save-analysis `Def`. Returns
541 /// `None` if the coordinates are not available on the match.
542 fn racer_match_to_def(ctx: &InitActionContext, m: &racer::Match) -> Option<Def> {
543 use racer::MatchType;
544 fn to_def_kind(kind: &MatchType) -> DefKind {
545 match kind {
546 MatchType::Struct(_) => DefKind::Struct,
547 MatchType::Union(_) => DefKind::Union,
548 MatchType::Module => DefKind::Mod,
549 MatchType::MatchArm => DefKind::Local,
550 MatchType::Function | MatchType::Method(_) => DefKind::Function,
551 MatchType::Crate => DefKind::Mod,
552 MatchType::Let(_)
553 | MatchType::IfLet(_)
554 | MatchType::WhileLet(_)
555 | MatchType::For(_) => DefKind::Local,
556 MatchType::StructField => DefKind::Field,
557 MatchType::Enum(_) => DefKind::Enum,
558 MatchType::EnumVariant(_) => DefKind::StructVariant,
559 MatchType::Type | MatchType::TypeParameter(_) | MatchType::AssocType => DefKind::Type,
560 MatchType::FnArg(_) => DefKind::Local,
561 MatchType::Trait => DefKind::Trait,
562 MatchType::Const => DefKind::Const,
563 MatchType::Static => DefKind::Static,
564 MatchType::Macro => DefKind::Macro,
565 MatchType::Builtin(_) => DefKind::Macro,
566 MatchType::UseAlias(m) => match m.mtype {
567 MatchType::UseAlias(_) => unreachable!("Nested use aliases"),
568 _ => to_def_kind(&m.mtype),
569 },
570 }
571 }
572 let kind = to_def_kind(&m.mtype);
573
574 let contextstr = if kind == DefKind::Mod {
575 use std::env;
576
577 let home = home::home_dir().unwrap_or_default();
578 let cargo_home =
579 env::var("CARGO_HOME").map(PathBuf::from).unwrap_or_else(|_| home.join(".cargo"));
580 let cargo_registry_src =
581 cargo_home.join("registry").join("src").join("github.com-1ecc6299db9ec823");
582 let rust_src_path = racer::get_rust_src_path().ok();
583
584 let contextstr = m.contextstr.replacen("\\\\?\\", "", 1);
585 let contextstr_path = PathBuf::from(&contextstr);
586 let contextstr_path = collapse_parents(contextstr_path);
587
588 // Attempt to tidy up the module path
589 rust_src_path
590 .and_then(|rust_src_path| {
591 // Make the path relative to Rust src root
592 contextstr_path.strip_prefix(rust_src_path).ok().map(ToOwned::to_owned)
593 })
594 .or_else(|| {
595 // Make the path relative to the package root cached in Cargo registry
596 contextstr_path.strip_prefix(cargo_registry_src).ok().map(ToOwned::to_owned)
597 })
598 .or_else(|| {
599 // Make the path relative to the root of the project
600 contextstr_path.strip_prefix(&ctx.current_project).ok().map(ToOwned::to_owned)
601 })
602 .and_then(|path| path.to_str().map(ToOwned::to_owned))
603 .unwrap_or_else(|| contextstr.to_string())
604 } else {
605 m.contextstr.trim_end_matches('{').trim().to_string()
606 };
607
608 let filepath = m.filepath.clone();
609 let matchstr = m.matchstr.clone();
610 let matchstr_len = matchstr.len() as u32;
611 let docs = m.docs.trim().to_string();
612 m.coords.map(|coords| {
613 assert!(
614 coords.row.0 > 0,
615 "racer_match_to_def: racer returned `0` for a 1-based row: {:?}",
616 m
617 );
618 let (row, col1) = requests::from_racer_coord(coords);
619 let col2 = Column::new_zero_indexed(col1.0 + matchstr_len);
620 let row = Row::new_zero_indexed(row.0 - 1);
621 let span = Span::new(row, row, col1, col2, filepath);
622 let def = Def {
623 kind,
624 span,
625 name: matchstr,
626 value: contextstr,
627 qualname: "".to_string(),
628 distro_crate: false,
629 parent: None,
630 docs,
631 };
632 trace!(
633 "racer_match_to_def: Def {{ kind: {:?}, span: {:?}, name: {:?}, \
634 value: {:?}, qualname: {:?}, distro_crate: {:?}, \
635 parent: {:?}, docs.is_empty: {:?} }}",
636 def.kind,
637 def.span,
638 def.name,
639 def.value,
640 def.qualname,
641 def.distro_crate,
642 def.parent,
643 def.docs.is_empty()
644 );
645 def
646 })
647 }
648
649 /// Uses racer to synthesize a `Def` for the given `span`. If no appropriate
650 /// match is found with coordinates, `None` is returned.
651 fn racer_def(ctx: &InitActionContext, span: &Span<ZeroIndexed>) -> Option<Def> {
652 let file_path = &span.file;
653
654 if !file_path.as_path().exists() {
655 error!("racer_def: skipping non-existant file: {:?}", file_path);
656 return None;
657 }
658
659 let name = ctx.vfs.load_line(file_path.as_path(), span.range.row_start).ok().and_then(|line| {
660 let col_start = span.range.col_start.0 as usize;
661 let col_end = span.range.col_end.0 as usize;
662 line.get(col_start..col_end).map(ToOwned::to_owned)
663 });
664
665 debug!("racer_def: name: {:?}", name);
666
667 let results = ::std::panic::catch_unwind(move || {
668 let cache = ctx.racer_cache();
669 let session = ctx.racer_session(&cache);
670 let row = span.range.row_end.one_indexed();
671 let coord = requests::racer_coord(row, span.range.col_end);
672 let location = racer::Location::Coords(coord);
673 trace!("racer_def: file_path: {:?}, location: {:?}", file_path, location);
674 let racer_match = racer::find_definition(file_path, location, &session);
675 trace!("racer_def: match: {:?}", racer_match);
676 racer_match
677 // Avoid creating tooltip text that is exactly the item being hovered over.
678 .filter(|m| name.as_ref().map(|name| name != &m.contextstr).unwrap_or(true))
679 .and_then(|m| racer_match_to_def(ctx, &m))
680 });
681
682 let results = results.map_err(|_| {
683 error!("racer_def: racer panicked");
684 });
685
686 results.unwrap_or(None)
687 }
688
689 /// Formats a struct, enum, union, or trait. The original type is returned
690 /// in the event of an error.
691 fn format_object(rustfmt: Rustfmt, fmt_config: &FmtConfig, the_type: String) -> String {
692 debug!("format_object: {}", the_type);
693 let mut config = fmt_config.get_rustfmt_config().clone();
694 config.set().newline_style(NewlineStyle::Unix);
695 let trimmed = the_type.trim();
696
697 // Normalize the ending for rustfmt.
698 let object = if trimmed.ends_with(')') {
699 format!("{};", trimmed)
700 } else if trimmed.ends_with('}') || trimmed.ends_with(';') {
701 trimmed.to_string()
702 } else if trimmed.ends_with('{') {
703 let trimmed = trimmed.trim_end_matches('{').to_string();
704 format!("{}{{}}", trimmed)
705 } else {
706 format!("{}{{}}", trimmed)
707 };
708
709 let formatted = match rustfmt.format(object.clone(), config) {
710 Ok(lines) => match lines.rfind('{') {
711 Some(pos) => lines[0..pos].into(),
712 None => lines,
713 },
714 Err(e) => {
715 error!("format_object: error: {:?}, input: {:?}", e, object);
716 trimmed.to_string()
717 }
718 };
719
720 // If it's a tuple, remove the trailing ';' and hide non-pub components
721 // for pub types.
722 let result = if formatted.trim().ends_with(';') {
723 let mut decl = formatted.trim().trim_end_matches(';');
724 if let (Some(pos), true) = (decl.rfind('('), decl.ends_with(')')) {
725 let mut hidden_count = 0;
726 let tuple_parts = decl[pos + 1..decl.len() - 1]
727 .split(',')
728 .map(|part| {
729 let part = part.trim();
730 if decl.starts_with("pub") && !part.starts_with("pub") {
731 hidden_count += 1;
732 "_".to_string()
733 } else {
734 part.to_string()
735 }
736 })
737 .collect::<Vec<String>>();
738 decl = &decl[0..pos];
739 if hidden_count != tuple_parts.len() {
740 format!("{}({})", decl, tuple_parts.join(", "))
741 } else {
742 decl.to_string()
743 }
744 } else {
745 // Not a tuple.
746 decl.into()
747 }
748 } else {
749 // Not a tuple or unit struct.
750 formatted
751 };
752
753 result.trim().into()
754 }
755
756 /// Formats a method or function. The original type is returned
757 /// in the event of an error.
758 fn format_method(rustfmt: Rustfmt, fmt_config: &FmtConfig, the_type: String) -> String {
759 trace!("format_method: {}", the_type);
760 let the_type = the_type.trim().trim_end_matches(';').to_string();
761
762 let mut config = fmt_config.get_rustfmt_config().clone();
763 config.set().newline_style(NewlineStyle::Unix);
764 let tab_spaces = config.tab_spaces();
765
766 let method = format!("impl Dummy {{ {} {{ unimplemented!() }} }}", the_type);
767
768 let result = match rustfmt.format(method.clone(), config) {
769 Ok(mut lines) => {
770 if let Some(front_pos) = lines.find('{') {
771 lines = lines[front_pos..].chars().skip(1).collect();
772 }
773 if let Some(back_pos) = lines.rfind('{') {
774 lines = lines[0..back_pos].into();
775 }
776 lines
777 .lines()
778 .filter(|line| line.trim() != "")
779 .map(|line| {
780 let mut spaces = tab_spaces + 1;
781 let should_trim = |c: char| {
782 spaces = spaces.saturating_sub(1);
783 spaces > 0 && c.is_whitespace()
784 };
785 let line = line.trim_start_matches(should_trim);
786 format!("{}\n", line)
787 })
788 .collect()
789 }
790 Err(e) => {
791 error!("format_method: error: {:?}, input: {:?}", e, method);
792 the_type
793 }
794 };
795
796 result.trim().into()
797 }
798
799 /// Builds a hover tooltip composed of the function signature or type declaration, doc URL
800 /// (if available in the save-analysis), source extracted documentation, and code context
801 /// for local variables.
802 pub fn tooltip(
803 ctx: &InitActionContext,
804 params: &TextDocumentPositionParams,
805 ) -> Result<Tooltip, ResponseError> {
806 let analysis = &ctx.analysis;
807
808 let hover_file_path = parse_file_path!(¶ms.text_document.uri, "hover")?;
809 let hover_span = ctx.convert_pos_to_span(hover_file_path, params.position);
810 let hover_span_doc = analysis.docs(&hover_span).unwrap_or_else(|_| String::new());
811 let hover_span_typ = analysis.show_type(&hover_span).unwrap_or_else(|_| String::new());
812 let hover_span_def = analysis.id(&hover_span).and_then(|id| analysis.get_def(id));
813
814 trace!("tooltip: span: {:?}", hover_span);
815 trace!("tooltip: span_doc: {:?}", hover_span_doc);
816 trace!("tooltip: span_typ: {:?}", hover_span_typ);
817 trace!("tooltip: span_def: {:?}", hover_span_def);
818
819 let racer_fallback_enabled = ctx.config.lock().unwrap().racer_completion;
820
821 // Fallback to racer if the def was not available and racer is enabled.
822 let hover_span_def = hover_span_def.or_else(|e| {
823 debug!("tooltip: racer_fallback_enabled: {}", racer_fallback_enabled);
824 if racer_fallback_enabled {
825 debug!("tooltip: span_def is empty, attempting with racer");
826 racer_def(&ctx, &hover_span).ok_or_else(|| {
827 debug!("tooltip: racer returned an empty result");
828 e
829 })
830 } else {
831 Err(e)
832 }
833 });
834
835 let doc_url = analysis.doc_url(&hover_span).ok();
836
837 let contents = if let Ok(def) = hover_span_def {
838 if def.kind == DefKind::Local && def.span == hover_span && def.qualname.contains('$') {
839 tooltip_local_variable_decl(&ctx, &def, doc_url)
840 } else if def.kind == DefKind::Local
841 && def.span != hover_span
842 && !def.qualname.contains('$')
843 {
844 tooltip_function_arg_usage(&ctx, &def, doc_url)
845 } else if def.kind == DefKind::Local && def.span != hover_span && def.qualname.contains('$')
846 {
847 tooltip_local_variable_usage(&ctx, &def, doc_url)
848 } else if def.kind == DefKind::Local && def.span == hover_span {
849 tooltip_function_signature_arg(&ctx, &def, doc_url)
850 } else {
851 match def.kind {
852 DefKind::TupleVariant | DefKind::StructVariant | DefKind::Field => {
853 tooltip_field_or_variant(&ctx, &def, doc_url)
854 }
855 DefKind::Enum | DefKind::Union | DefKind::Struct | DefKind::Trait => {
856 tooltip_struct_enum_union_trait(&ctx, &def, doc_url)
857 }
858 DefKind::Function | DefKind::Method | DefKind::ForeignFunction => {
859 tooltip_function_method(&ctx, &def, doc_url)
860 }
861 DefKind::Mod => tooltip_mod(&ctx, &def, doc_url),
862 DefKind::Static | DefKind::ForeignStatic | DefKind::Const => {
863 tooltip_static_const_decl(&ctx, &def, doc_url)
864 }
865 DefKind::Type => tooltip_type(&ctx, &def, doc_url),
866 _ => {
867 debug!(
868 "tooltip: ignoring def: \
869 name: {:?}, \
870 kind: {:?}, \
871 value: {:?}, \
872 qualname: {:?}, \
873 parent: {:?}",
874 def.name, def.kind, def.value, def.qualname, def.parent
875 );
876
877 Vec::default()
878 }
879 }
880 }
881 } else {
882 debug!("tooltip: def is empty");
883 Vec::default()
884 };
885 debug!("tooltip: contents.len: {}", contents.len());
886 Ok(Tooltip { contents, range: hover_span.range })
887 }
888
889 #[cfg(test)]
890 #[allow(clippy::expect_fun_call)]
891 pub mod test {
892 use super::*;
893 use crate::actions::format::Rustfmt;
894
895 pub fn fixtures_dir() -> &'static Path {
896 Path::new(env!("FIXTURES_DIR"))
897 }
898
899 /// Strips indentation from string literals by examining
900 /// the indent of the first non-empty line. Preceding
901 /// and trailing whitespace is also removed.
902 fn noindent(text: &str) -> String {
903 let indent = text
904 .lines()
905 .filter(|line| !line.trim().is_empty())
906 .peekable()
907 .peek()
908 .map(|first_non_empty_line| {
909 first_non_empty_line
910 .chars()
911 .scan(0, |_, ch| if ch.is_whitespace() { Some(1) } else { None })
912 .fuse()
913 .sum()
914 })
915 .unwrap_or(0);
916
917 text.lines()
918 .map(|line| line.chars().skip(indent).collect::<String>())
919 .collect::<Vec<String>>()
920 .join("\n")
921 .trim()
922 .to_string()
923 }
924
925 #[test]
926 fn test_noindent() {
927 let lines = noindent(
928 "
929
930 Hello, world ! ! !
931 The next line
932 Indented line
933 Last line
934
935 ",
936 );
937 assert_eq!("Hello, world ! ! !\nThe next line\n Indented line\nLast line", &lines);
938
939 let lines = noindent(
940 "
941
942 Hello, world ! ! !
943 The next line
944 Indented line
945 Last line
946
947 ",
948 );
949 assert_eq!("Hello, world ! ! !\nThe next line\n Indented line\nLast line", &lines);
950 }
951
952 #[test]
953 fn test_process_docs_rust_blocks() {
954 let docs = &noindent("
955 Brief one liner.
956
957 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vitae ex
958 vel mi egestas semper in non dolor. Proin ut arcu at odio hendrerit consequat.
959
960 # Examples
961
962 Donec ullamcorper risus quis massa sollicitudin, id faucibus nibh bibendum.
963
964 ## Hidden code lines and proceeding whitespace is removed and meta attributes are preserved
965
966 ```
967 # extern crate foo;
968
969 use foo::bar;
970
971 #[derive(Debug)]
972 struct Baz(u32);
973
974 let baz = Baz(1);
975 ```
976
977 ## Rust code block attributes are converted to 'rust'
978
979 ```compile_fail,E0123
980 let foo = \"compile_fail\"
981 ```
982
983 ```no_run
984 let foo = \"no_run\";
985 ```
986
987 ```ignore
988 let foo = \"ignore\";
989 ```
990
991 ```should_panic
992 let foo = \"should_panic\";
993 ```
994
995 ```should_panic,ignore,no_run,compile_fail
996 let foo = \"should_panic,ignore,no_run,compile_fail\";
997 ```
998
999 ## Inner comments and indentation is preserved
1000
1001 ```
1002 /// inner doc comment
1003 fn foobar() {
1004 // inner comment
1005 let indent = 1;
1006 }
1007 ```
1008
1009 ## Module attributes are preserved
1010
1011 ```
1012 #![allow(dead_code, unused_imports)]
1013 ```
1014 ");
1015
1016 let expected = noindent("
1017 Brief one liner.
1018
1019 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus vitae ex
1020 vel mi egestas semper in non dolor. Proin ut arcu at odio hendrerit consequat.
1021
1022 # Examples
1023
1024 Donec ullamcorper risus quis massa sollicitudin, id faucibus nibh bibendum.
1025
1026 ## Hidden code lines and proceeding whitespace is removed and meta attributes are preserved
1027
1028 ```rust
1029 use foo::bar;
1030
1031 #[derive(Debug)]
1032 struct Baz(u32);
1033
1034 let baz = Baz(1);
1035 ```
1036
1037 ## Rust code block attributes are converted to 'rust'
1038
1039 ```rust
1040 let foo = \"compile_fail\"
1041 ```
1042
1043 ```rust
1044 let foo = \"no_run\";
1045 ```
1046
1047 ```rust
1048 let foo = \"ignore\";
1049 ```
1050
1051 ```rust
1052 let foo = \"should_panic\";
1053 ```
1054
1055 ```rust
1056 let foo = \"should_panic,ignore,no_run,compile_fail\";
1057 ```
1058
1059 ## Inner comments and indentation is preserved
1060
1061 ```rust
1062 /// inner doc comment
1063 fn foobar() {
1064 // inner comment
1065 let indent = 1;
1066 }
1067 ```
1068
1069 ## Module attributes are preserved
1070
1071 ```rust
1072 #![allow(dead_code, unused_imports)]
1073 ```
1074 ");
1075
1076 let actual = process_docs(docs);
1077 assert_eq!(expected, actual);
1078 }
1079
1080 #[test]
test_process_docs_bash_block()1081 fn test_process_docs_bash_block() {
1082 let expected = noindent(
1083 "
1084 Brief one liner.
1085
1086 ```bash
1087 # non rust-block comment lines are preserved
1088 ls -la
1089 ```
1090 ",
1091 );
1092
1093 let actual = process_docs(&expected);
1094 assert_eq!(expected, actual);
1095 }
1096
1097 #[test]
test_process_docs_racer_returns_extra_slashes()1098 fn test_process_docs_racer_returns_extra_slashes() {
1099 let docs = noindent(
1100 "
1101 ////////////////////////////////////////////////////////////////////////////////
1102
1103 Spawns a new thread, returning a [`JoinHandle`] for it.
1104
1105 The join handle will implicitly *detach* the child thread upon being
1106 dropped. In this case, the child thread may outlive the parent (unless
1107 ",
1108 );
1109
1110 let expected = noindent(
1111 "
1112 Spawns a new thread, returning a [`JoinHandle`] for it.
1113
1114 The join handle will implicitly *detach* the child thread upon being
1115 dropped. In this case, the child thread may outlive the parent (unless
1116 ",
1117 );
1118
1119 let actual = process_docs(&docs);
1120 assert_eq!(expected, actual);
1121 }
1122
1123 #[test]
test_format_method()1124 fn test_format_method() {
1125 let fmt = Rustfmt::Internal;
1126 let config = &FmtConfig::default();
1127
1128 let input = "fn foo() -> ()";
1129 let result = format_method(fmt.clone(), config, input.into());
1130 assert_eq!(input, &result, "function explicit void return");
1131
1132 let input = "fn foo()";
1133 let expected = "fn foo()";
1134 let result = format_method(fmt.clone(), config, input.into());
1135 assert_eq!(expected, &result, "function");
1136
1137 let input = "fn foo() -> Thing";
1138 let expected = "fn foo() -> Thing";
1139 let result = format_method(fmt.clone(), config, input.into());
1140 assert_eq!(expected, &result, "function with return");
1141
1142 let input = "fn foo(&self);";
1143 let expected = "fn foo(&self)";
1144 let result = format_method(fmt.clone(), config, input.into());
1145 assert_eq!(expected, &result, "method");
1146
1147 let input = "fn foo<T>(t: T) where T: Copy";
1148 let expected = noindent(
1149 "
1150 fn foo<T>(t: T)
1151 where
1152 T: Copy,
1153 ",
1154 );
1155 let result = format_method(fmt.clone(), config, input.into());
1156 assert_eq!(expected, result, "function with generic parameters");
1157
1158 let input = "fn foo<T>(&self, t: T) where T: Copy";
1159 let expected = noindent(
1160 "
1161 fn foo<T>(&self, t: T)
1162 where
1163 T: Copy,
1164 ",
1165 );
1166 let result = format_method(fmt.clone(), config, input.into());
1167 assert_eq!(expected, result, "method with type parameters");
1168
1169 let input = noindent(
1170 " fn foo<T>(
1171 &self,
1172 t: T)
1173 where
1174 T: Copy
1175
1176 ",
1177 );
1178 let expected = noindent(
1179 "
1180 fn foo<T>(&self, t: T)
1181 where
1182 T: Copy,
1183 ",
1184 );
1185 let result = format_method(fmt.clone(), config, input);
1186 assert_eq!(expected, result, "method with type parameters; corrected spacing");
1187
1188 let input = "fn really_really_really_really_long_name<T>(foo_thing: String, bar_thing: Thing, baz_thing: Vec<T>, foo_other: u32, bar_other: i32) -> Thing";
1189 let expected = noindent(
1190 "
1191 fn really_really_really_really_long_name<T>(
1192 foo_thing: String,
1193 bar_thing: Thing,
1194 baz_thing: Vec<T>,
1195 foo_other: u32,
1196 bar_other: i32,
1197 ) -> Thing
1198 ",
1199 );
1200 let result = format_method(fmt.clone(), config, input.into());
1201 assert_eq!(expected, result, "long function signature");
1202
1203 let input = "fn really_really_really_really_long_name(&self, foo_thing: String, bar_thing: Thing, baz_thing: Vec<T>, foo_other: u32, bar_other: i32) -> Thing";
1204 let expected = noindent(
1205 "
1206 fn really_really_really_really_long_name(
1207 &self,
1208 foo_thing: String,
1209 bar_thing: Thing,
1210 baz_thing: Vec<T>,
1211 foo_other: u32,
1212 bar_other: i32,
1213 ) -> Thing
1214 ",
1215 );
1216 let result = format_method(fmt.clone(), config, input.into());
1217 assert_eq!(expected, result, "long method signature with generic");
1218
1219 let input = noindent(
1220 "
1221 fn matrix_something(
1222 _a_matrix: [[f32; 4]; 4],
1223 _b_matrix: [[f32; 4]; 4],
1224 _c_matrix: [[f32; 4]; 4],
1225 _d_matrix: [[f32; 4]; 4],
1226 )
1227 ",
1228 );
1229 let expected = noindent(
1230 "
1231 fn matrix_something(
1232 _a_matrix: [[f32; 4]; 4],
1233 _b_matrix: [[f32; 4]; 4],
1234 _c_matrix: [[f32; 4]; 4],
1235 _d_matrix: [[f32; 4]; 4],
1236 )
1237 ",
1238 );
1239 let result = format_method(fmt, config, input);
1240 assert_eq!(expected, result, "function with multiline args");
1241 }
1242
1243 #[test]
test_extract_decl()1244 fn test_extract_decl() {
1245 let vfs = Vfs::new();
1246 let file = fixtures_dir().join("hover/src/test_extract_decl.rs");
1247
1248 let expected = "pub fn foo() -> Foo<u32>";
1249 let row_start = Row::new_zero_indexed(0);
1250 let actual = extract_decl(&vfs, &file, row_start).expect("function declaration").join("\n");
1251 assert_eq!(expected, actual);
1252
1253 let expected = "pub struct Foo<T>";
1254 let row_start = Row::new_zero_indexed(5);
1255 let actual = extract_decl(&vfs, &file, row_start).expect("struct declaration").join("\n");
1256 assert_eq!(expected, actual);
1257
1258 let expected = "pub enum Bar";
1259 let row_start = Row::new_zero_indexed(10);
1260 let actual = extract_decl(&vfs, &file, row_start).expect("enum declaration").join("\n");
1261 assert_eq!(expected, actual);
1262
1263 let expected = "pub struct NewType(pub u32, f32)";
1264 let row_start = Row::new_zero_indexed(15);
1265 let actual = extract_decl(&vfs, &file, row_start).expect("tuple declaration").join("\n");
1266 assert_eq!(expected, actual);
1267
1268 let expected = "pub fn new() -> NewType";
1269 let row_start = Row::new_zero_indexed(18);
1270 let actual =
1271 extract_decl(&vfs, &file, row_start).expect("struct function declaration").join("\n");
1272 assert_eq!(expected, actual);
1273
1274 let expected = "pub fn bar<T: Copy + Add>(&self, the_really_long_name_string: String, the_really_long_name_foo: Foo<T>) -> Vec<(String, Foo<T>)>";
1275 let row_start = Row::new_zero_indexed(22);
1276 let actual = extract_decl(&vfs, &file, row_start)
1277 .expect("long struct method declaration with generics")
1278 .join("\n");
1279 assert_eq!(expected, actual);
1280
1281 let expected = "pub trait Baz<T> where T: Copy";
1282 let row_start = Row::new_zero_indexed(27);
1283 let actual = extract_decl(&vfs, &file, row_start).expect("enum declaration").join("\n");
1284 assert_eq!(expected, actual);
1285
1286 let expected = "fn make_copy(&self) -> Self";
1287 let row_start = Row::new_zero_indexed(28);
1288 let actual =
1289 extract_decl(&vfs, &file, row_start).expect("trait method declaration").join("\n");
1290 assert_eq!(expected, actual);
1291
1292 let expected = "fn make_copy(&self) -> Self";
1293 let row_start = Row::new_zero_indexed(32);
1294 let actual =
1295 extract_decl(&vfs, &file, row_start).expect("trait method implementation").join("\n");
1296 assert_eq!(expected, actual);
1297
1298 let expected = noindent(
1299 "
1300 pub trait Qeh<T, U>
1301 where T: Copy,
1302 U: Clone
1303 ",
1304 );
1305 let row_start = Row::new_zero_indexed(37);
1306 let actual =
1307 extract_decl(&vfs, &file, row_start).expect("trait declaration multiline").join("\n");
1308 assert_eq!(expected, actual);
1309
1310 let expected = noindent(
1311 "
1312 pub fn multiple_lines(
1313 s: String,
1314 i: i32
1315 )
1316 ",
1317 );
1318 let row_start = Row::new_zero_indexed(43);
1319 let actual = extract_decl(&vfs, &file, row_start)
1320 .expect("function declaration multiline")
1321 .join("\n");
1322 assert_eq!(expected, actual);
1323 }
1324
1325 #[test]
test_format_object()1326 fn test_format_object() {
1327 let fmt = Rustfmt::Internal;
1328 let config = &FmtConfig::default();
1329
1330 let input = "pub struct Box<T: ?Sized>(Unique<T>);";
1331 let result = format_object(fmt.clone(), config, input.into());
1332 assert_eq!(
1333 "pub struct Box<T: ?Sized>", &result,
1334 "tuple struct with all private fields has hidden components"
1335 );
1336
1337 let input = "pub struct Thing(pub u32);";
1338 let result = format_object(fmt.clone(), config, input.into());
1339 assert_eq!(
1340 "pub struct Thing(pub u32)", &result,
1341 "tuple struct with trailing ';' from racer"
1342 );
1343
1344 let input = "pub struct Thing(pub u32)";
1345 let result = format_object(fmt.clone(), config, input.into());
1346 assert_eq!("pub struct Thing(pub u32)", &result, "pub tuple struct");
1347
1348 let input = "pub struct Thing(pub u32, i32)";
1349 let result = format_object(fmt.clone(), config, input.into());
1350 assert_eq!(
1351 "pub struct Thing(pub u32, _)", &result,
1352 "non-pub components of pub tuples should be hidden"
1353 );
1354
1355 let input = "struct Thing(u32, i32)";
1356 let result = format_object(fmt.clone(), config, input.into());
1357 assert_eq!(
1358 "struct Thing(u32, i32)", &result,
1359 "private tuple struct may show private components"
1360 );
1361
1362 let input = "pub struct Thing<T: Copy>";
1363 let result = format_object(fmt.clone(), config, input.into());
1364 assert_eq!("pub struct Thing<T: Copy>", &result, "pub struct");
1365
1366 let input = "pub struct Thing<T: Copy> {";
1367 let result = format_object(fmt.clone(), config, input.into());
1368 assert_eq!(
1369 "pub struct Thing<T: Copy>", &result,
1370 "pub struct with trailing '{{' from racer"
1371 );
1372
1373 let input = "pub struct Thing { x: i32 }";
1374 let result = format_object(fmt.clone(), config, input.into());
1375 assert_eq!("pub struct Thing", &result, "pub struct with body");
1376
1377 let input = "pub enum Foobar { Foo, Bar }";
1378 let result = format_object(fmt.clone(), config, input.into());
1379 assert_eq!("pub enum Foobar", &result, "pub enum with body");
1380
1381 let input = "pub trait Thing<T, U> where T: Copy + Sized, U: Clone";
1382 let expected = noindent(
1383 "
1384 pub trait Thing<T, U>
1385 where
1386 T: Copy + Sized,
1387 U: Clone,
1388 ",
1389 );
1390 let result = format_object(fmt, config, input.into());
1391 assert_eq!(expected, result, "trait with where clause");
1392 }
1393
1394 #[test]
test_extract_decl_multiline_empty_function()1395 fn test_extract_decl_multiline_empty_function() {
1396 let vfs = Vfs::new();
1397 let file = fixtures_dir().join("hover/src/test_extract_decl_multiline_empty_function.rs");
1398
1399 let expected = noindent(
1400 "
1401 fn matrix_something(
1402 _a_matrix: [[f32; 4]; 4],
1403 _b_matrix: [[f32; 4]; 4],
1404 _c_matrix: [[f32; 4]; 4],
1405 _d_matrix: [[f32; 4]; 4],
1406 )
1407 ",
1408 );
1409 let row_start = Row::new_zero_indexed(1);
1410 let actual = extract_decl(&vfs, &file, row_start)
1411 .expect("the empty body should not be extracted")
1412 .join("\n");
1413 assert_eq!(expected, actual);
1414 }
1415
1416 #[test]
test_extract_docs_module_docs_with_attribute()1417 fn test_extract_docs_module_docs_with_attribute() {
1418 let vfs = Vfs::new();
1419 let file = fixtures_dir().join("hover/src/test_extract_docs_module_docs_with_attribute.rs");
1420 let row_start = Row::new_zero_indexed(0);
1421 let actual = extract_docs(&vfs, &file, row_start)
1422 .expect(&format!("failed to extract docs: {:?}", file))
1423 .join("\n");
1424
1425 let expected = noindent(
1426 "
1427 Begin module docs
1428
1429 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas
1430 tincidunt tristique maximus. Sed venenatis urna vel sagittis tempus.
1431 In hac habitasse platea dictumst.
1432
1433 End module docs.
1434 ",
1435 );
1436
1437 assert_eq!(expected, actual, "module docs without a copyright header");
1438 }
1439
1440 #[test]
test_extract_docs_module_docs_no_copyright()1441 fn test_extract_docs_module_docs_no_copyright() {
1442 let vfs = Vfs::new();
1443 let file = fixtures_dir().join("hover/src/test_extract_docs_module_docs_no_copyright.rs");
1444 let row_start = Row::new_zero_indexed(0);
1445 let actual = extract_docs(&vfs, &file, row_start)
1446 .expect(&format!("failed to extract docs: {:?}", file))
1447 .join("\n");
1448
1449 let expected = noindent(
1450 "
1451 Begin module docs
1452
1453 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas
1454 tincidunt tristique maximus. Sed venenatis urna vel sagittis tempus.
1455 In hac habitasse platea dictumst.
1456
1457 End module docs.
1458 ",
1459 );
1460
1461 assert_eq!(expected, actual, "module docs without a copyright header");
1462 }
1463
1464 #[test]
test_extract_docs_comment_block()1465 fn test_extract_docs_comment_block() {
1466 let vfs = Vfs::new();
1467 let file = fixtures_dir().join("hover/src/test_extract_docs_comment_block.rs");
1468 let row_start = Row::new_zero_indexed(11);
1469 let actual = extract_docs(&vfs, &file, row_start)
1470 .expect(&format!("failed to extract docs: {:?}", file))
1471 .join("\n");
1472
1473 let expected = noindent(
1474 "
1475 The standard library often has comment header blocks that should not be
1476 included.
1477
1478 Nam efficitur dapibus lectus consequat porta. Pellentesque augue metus,
1479 vestibulum nec massa at, aliquet consequat ex.
1480
1481 End of spawn docs
1482 ",
1483 );
1484
1485 assert_eq!(expected, actual);
1486 }
1487
1488 #[test]
test_extract_docs_empty_line_before_decl()1489 fn test_extract_docs_empty_line_before_decl() {
1490 let vfs = Vfs::new();
1491 let file = fixtures_dir().join("hover/src/test_extract_docs_empty_line_before_decl.rs");
1492 let row_start = Row::new_zero_indexed(8);
1493 let actual = extract_docs(&vfs, &file, row_start)
1494 .expect(&format!("failed to extract docs: {:?}", file))
1495 .join("\n");
1496
1497 let expected = noindent(
1498 "
1499 Begin empty before decl
1500
1501 Cras malesuada mattis massa quis ornare. Suspendisse in ex maximus,
1502 iaculis ante non, ultricies nulla. Nam ultrices convallis ex, vel
1503 lacinia est rhoncus sed. Nullam sollicitudin finibus ex at placerat.
1504
1505 End empty line before decl.
1506 ",
1507 );
1508
1509 assert_eq!(expected, actual);
1510 }
1511
1512 #[test]
test_extract_docs_module_docs()1513 fn test_extract_docs_module_docs() {
1514 let vfs = Vfs::new();
1515 let file = fixtures_dir().join("hover/src/test_extract_docs_module_docs.rs");
1516
1517 let row_start = Row::new_zero_indexed(0);
1518 let actual = extract_docs(&vfs, &file, row_start)
1519 .expect(&format!("failed to extract docs: {:?}", file))
1520 .join("\n");
1521
1522 let expected = noindent(
1523 "
1524 Begin module docs
1525
1526 Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas
1527 tincidunt tristique maximus. Sed venenatis urna vel sagittis tempus.
1528 In hac habitasse platea dictumst.
1529
1530 End module docs.
1531 ",
1532 );
1533
1534 assert_eq!(expected, actual);
1535
1536 let row_start = Row::new_zero_indexed(11);
1537 let actual = extract_docs(&vfs, &file, row_start)
1538 .expect(&format!("failed to extract docs: {:?}", file))
1539 .join("\n");
1540
1541 let expected = noindent(
1542 "
1543 Begin first item docs
1544
1545 The first item docs should not pick up the module docs.
1546 ",
1547 );
1548
1549 assert_eq!(expected, actual);
1550 }
1551
1552 #[test]
test_extract_docs_attributes()1553 fn test_extract_docs_attributes() {
1554 let vfs = Vfs::new();
1555 let file = fixtures_dir().join("hover/src/test_extract_docs_attributes.rs");
1556
1557 let row_start = Row::new_zero_indexed(11);
1558 let actual = extract_docs(&vfs, &file, row_start)
1559 .expect(&format!("failed to extract docs: {:?}", file))
1560 .join("\n");
1561
1562 let expected = noindent(
1563 "
1564 Begin multiline attribute
1565
1566 Cras malesuada mattis massa quis ornare. Suspendisse in ex maximus,
1567 iaculis ante non, ultricies nulla. Nam ultrices convallis ex, vel
1568 lacinia est rhoncus sed. Nullam sollicitudin finibus ex at placerat.
1569
1570 End multiline attribute
1571 ",
1572 );
1573
1574 assert_eq!(expected, actual);
1575
1576 let row_start = Row::new_zero_indexed(22);
1577 let actual = extract_docs(&vfs, &file, row_start)
1578 .expect(&format!("failed to extract docs: {:?}", file))
1579 .join("\n");
1580
1581 let expected = noindent(
1582 "
1583 Begin single line attribute
1584
1585 Cras malesuada mattis massa quis ornare. Suspendisse in ex maximus,
1586 iaculis ante non, ultricies nulla. Nam ultrices convallis ex, vel
1587 lacinia est rhoncus sed. Nullam sollicitudin finibus ex at placerat.
1588
1589 End single line attribute.
1590 ",
1591 );
1592
1593 assert_eq!(expected, actual);
1594 }
1595
1596 #[test]
test_extract_docs_comment_first_line()1597 fn test_extract_docs_comment_first_line() {
1598 let vfs = Vfs::new();
1599 let file = fixtures_dir().join("hover/src/test_extract_docs_comment_first_line.rs");
1600
1601 let row_start = Row::new_zero_indexed(1);
1602 let actual = extract_docs(&vfs, &file, row_start)
1603 .expect(&format!("failed to extract docs: {:?}", file))
1604 .join("\n");
1605
1606 let expected = "First line comment";
1607
1608 assert_eq!(expected, actual);
1609 }
1610 }
1611