Better handling of block doc comments

This commit is contained in:
Lukas Wirth 2021-03-17 14:38:11 +01:00
parent 0fbfab3b45
commit ec824a92d0
8 changed files with 158 additions and 81 deletions

View File

@ -77,33 +77,19 @@ impl RawAttrs {
pub(crate) const EMPTY: Self = Self { entries: None }; pub(crate) const EMPTY: Self = Self { entries: None };
pub(crate) fn new(owner: &dyn AttrsOwner, hygiene: &Hygiene) -> Self { pub(crate) fn new(owner: &dyn AttrsOwner, hygiene: &Hygiene) -> Self {
let attrs: Vec<_> = collect_attrs(owner).collect(); let entries = collect_attrs(owner)
let entries = if attrs.is_empty() { .enumerate()
// Avoid heap allocation .flat_map(|(i, attr)| match attr {
None Either::Left(attr) => Attr::from_src(attr, hygiene, i as u32),
} else { Either::Right(comment) => comment.doc_comment().map(|doc| Attr {
Some( index: i as u32,
attrs input: Some(AttrInput::Literal(SmolStr::new(doc))),
.into_iter() path: ModPath::from(hir_expand::name!(doc)),
.enumerate() }),
.flat_map(|(i, attr)| match attr { })
Either::Left(attr) => Attr::from_src(attr, hygiene).map(|attr| (i, attr)), .collect::<Arc<_>>();
Either::Right(comment) => comment.doc_comment().map(|doc| {
( Self { entries: if entries.is_empty() { None } else { Some(entries) } }
i,
Attr {
index: 0,
input: Some(AttrInput::Literal(SmolStr::new(doc))),
path: ModPath::from(hir_expand::name!(doc)),
},
)
}),
})
.map(|(i, attr)| Attr { index: i as u32, ..attr })
.collect(),
)
};
Self { entries }
} }
fn from_attrs_owner(db: &dyn DefDatabase, owner: InFile<&dyn AttrsOwner>) -> Self { fn from_attrs_owner(db: &dyn DefDatabase, owner: InFile<&dyn AttrsOwner>) -> Self {
@ -162,7 +148,7 @@ pub(crate) fn filter(self, db: &dyn DefDatabase, krate: CrateId) -> Attrs {
let attr = ast::Attr::parse(&format!("#[{}]", tree)).ok()?; let attr = ast::Attr::parse(&format!("#[{}]", tree)).ok()?;
// FIXME hygiene // FIXME hygiene
let hygiene = Hygiene::new_unhygienic(); let hygiene = Hygiene::new_unhygienic();
Attr::from_src(attr, &hygiene).map(|attr| Attr { index, ..attr }) Attr::from_src(attr, &hygiene, index)
}); });
let cfg_options = &crate_graph[krate].cfg_options; let cfg_options = &crate_graph[krate].cfg_options;
@ -325,15 +311,36 @@ pub fn docs(&self) -> Option<Documentation> {
AttrInput::Literal(s) => Some(s), AttrInput::Literal(s) => Some(s),
AttrInput::TokenTree(_) => None, AttrInput::TokenTree(_) => None,
}); });
// FIXME: Replace `Itertools::intersperse` with `Iterator::intersperse[_with]` until the let indent = docs
// libstd api gets stabilized (https://github.com/rust-lang/rust/issues/79524). .clone()
let docs = Itertools::intersperse(docs, &SmolStr::new_inline("\n")) .flat_map(|s| s.lines())
.map(|it| it.as_str()) .filter(|line| !line.chars().all(|c| c.is_whitespace()))
.collect::<String>(); .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
if docs.is_empty() { .min()
.unwrap_or(0);
let mut buf = String::new();
for doc in docs {
// str::lines doesn't yield anything for the empty string
if doc.is_empty() {
buf.push('\n');
} else {
buf.extend(Itertools::intersperse(
doc.lines().map(|line| {
line.char_indices()
.nth(indent)
.map_or(line, |(offset, _)| &line[offset..])
.trim_end()
}),
"\n",
));
}
buf.push('\n');
}
buf.pop();
if buf.is_empty() {
None None
} else { } else {
Some(Documentation(docs)) Some(Documentation(buf))
} }
} }
} }
@ -407,7 +414,7 @@ pub enum AttrInput {
} }
impl Attr { impl Attr {
fn from_src(ast: ast::Attr, hygiene: &Hygiene) -> Option<Attr> { fn from_src(ast: ast::Attr, hygiene: &Hygiene, index: u32) -> Option<Attr> {
let path = ModPath::from_src(ast.path()?, hygiene)?; let path = ModPath::from_src(ast.path()?, hygiene)?;
let input = if let Some(lit) = ast.literal() { let input = if let Some(lit) = ast.literal() {
let value = match lit.kind() { let value = match lit.kind() {
@ -420,7 +427,7 @@ fn from_src(ast: ast::Attr, hygiene: &Hygiene) -> Option<Attr> {
} else { } else {
None None
}; };
Some(Attr { index: 0, path, input }) Some(Attr { index, path, input })
} }
/// Maps this lowered `Attr` back to its original syntax node. /// Maps this lowered `Attr` back to its original syntax node.
@ -508,7 +515,7 @@ pub fn exists(self) -> bool {
self.attrs().next().is_some() self.attrs().next().is_some()
} }
pub fn attrs(self) -> impl Iterator<Item = &'a Attr> { pub fn attrs(self) -> impl Iterator<Item = &'a Attr> + Clone {
let key = self.key; let key = self.key;
self.attrs self.attrs
.iter() .iter()

View File

@ -95,12 +95,10 @@ fn extract_positioned_link_from_comment(
let comment_range = comment.syntax().text_range(); let comment_range = comment.syntax().text_range();
let doc_comment = comment.doc_comment()?; let doc_comment = comment.doc_comment()?;
let def_links = extract_definitions_from_markdown(doc_comment); let def_links = extract_definitions_from_markdown(doc_comment);
let start = comment_range.start() + TextSize::from(comment.prefix().len() as u32);
let (def_link, ns, _) = def_links.iter().min_by_key(|(_, _, def_link_range)| { let (def_link, ns, _) = def_links.iter().min_by_key(|(_, _, def_link_range)| {
let matched_position = comment_range.start() + TextSize::from(def_link_range.start as u32); let matched_position = start + TextSize::from(def_link_range.start as u32);
match position.offset.checked_sub(matched_position) { position.offset.checked_sub(matched_position).unwrap_or_else(|| comment_range.end())
Some(distance) => distance,
None => comment_range.end(),
}
})?; })?;
Some((def_link.to_string(), *ns)) Some((def_link.to_string(), *ns))
} }

View File

@ -3423,6 +3423,40 @@ mod Foo
); );
} }
#[test]
fn hover_doc_block_style_indentend() {
check(
r#"
/**
foo
```rust
let x = 3;
```
*/
fn foo$0() {}
"#,
expect![[r#"
*foo*
```rust
test
```
```rust
fn foo()
```
---
foo
```rust
let x = 3;
```
"#]],
);
}
#[test] #[test]
fn hover_comments_dont_highlight_parent() { fn hover_comments_dont_highlight_parent() {
check_hover_no_result( check_hover_no_result(

View File

@ -576,6 +576,20 @@ fn should_have_runnable_1() {}
/// ``` /// ```
fn should_have_runnable_2() {} fn should_have_runnable_2() {}
/**
```rust
let z = 55;
```
*/
fn should_have_no_runnable_3() {}
/**
```rust
let z = 55;
```
*/
fn should_have_no_runnable_4() {}
/// ```no_run /// ```no_run
/// let z = 55; /// let z = 55;
/// ``` /// ```
@ -616,7 +630,7 @@ fn should_have_no_runnable_6() {}
struct StructWithRunnable(String); struct StructWithRunnable(String);
"#, "#,
&[&BIN, &DOCTEST, &DOCTEST, &DOCTEST, &DOCTEST], &[&BIN, &DOCTEST, &DOCTEST, &DOCTEST, &DOCTEST, &DOCTEST, &DOCTEST],
expect![[r#" expect![[r#"
[ [
Runnable { Runnable {
@ -682,7 +696,37 @@ fn should_have_no_runnable_6() {}
file_id: FileId( file_id: FileId(
0, 0,
), ),
full_range: 756..821, full_range: 256..320,
name: "should_have_no_runnable_3",
},
kind: DocTest {
test_id: Path(
"should_have_no_runnable_3",
),
},
cfg: None,
},
Runnable {
nav: NavigationTarget {
file_id: FileId(
0,
),
full_range: 322..398,
name: "should_have_no_runnable_4",
},
kind: DocTest {
test_id: Path(
"should_have_no_runnable_4",
),
},
cfg: None,
},
Runnable {
nav: NavigationTarget {
file_id: FileId(
0,
),
full_range: 900..965,
name: "StructWithRunnable", name: "StructWithRunnable",
}, },
kind: DocTest { kind: DocTest {

View File

@ -255,7 +255,7 @@ fn foo() {
bar.fo$0; bar.fo$0;
} }
"#, "#,
DetailAndDocumentation { detail: "fn(&self)", documentation: " Do the foo" }, DetailAndDocumentation { detail: "fn(&self)", documentation: "Do the foo" },
); );
} }

View File

@ -118,7 +118,7 @@ mod foo {}
.ok() .ok()
.unwrap(); .unwrap();
let module = file.syntax().descendants().find_map(Module::cast).unwrap(); let module = file.syntax().descendants().find_map(Module::cast).unwrap();
assert!(module.doc_comment_text().is_none()); assert!(module.doc_comments().doc_comment_text().is_none());
} }
#[test] #[test]
@ -133,7 +133,7 @@ mod foo {}
.ok() .ok()
.unwrap(); .unwrap();
let module = file.syntax().descendants().find_map(Module::cast).unwrap(); let module = file.syntax().descendants().find_map(Module::cast).unwrap();
assert_eq!("doc", module.doc_comment_text().unwrap()); assert_eq!(" doc", module.doc_comments().doc_comment_text().unwrap());
} }
#[test] #[test]
@ -148,7 +148,7 @@ mod foo {}
.ok() .ok()
.unwrap(); .unwrap();
let module = file.syntax().descendants().find_map(Module::cast).unwrap(); let module = file.syntax().descendants().find_map(Module::cast).unwrap();
assert!(module.doc_comment_text().is_none()); assert!(module.doc_comments().doc_comment_text().is_none());
} }
#[test] #[test]
@ -162,7 +162,7 @@ fn test_doc_comment_of_statics() {
.ok() .ok()
.unwrap(); .unwrap();
let st = file.syntax().descendants().find_map(Static::cast).unwrap(); let st = file.syntax().descendants().find_map(Static::cast).unwrap();
assert_eq!("Number of levels", st.doc_comment_text().unwrap()); assert_eq!(" Number of levels", st.doc_comments().doc_comment_text().unwrap());
} }
#[test] #[test]
@ -181,7 +181,10 @@ mod foo {}
.ok() .ok()
.unwrap(); .unwrap();
let module = file.syntax().descendants().find_map(Module::cast).unwrap(); let module = file.syntax().descendants().find_map(Module::cast).unwrap();
assert_eq!("doc1\n```\nfn foo() {\n // ...\n}\n```", module.doc_comment_text().unwrap()); assert_eq!(
" doc1\n ```\n fn foo() {\n // ...\n }\n ```",
module.doc_comments().doc_comment_text().unwrap()
);
} }
#[test] #[test]
@ -198,7 +201,7 @@ mod foo {}
.ok() .ok()
.unwrap(); .unwrap();
let module = file.syntax().descendants().find_map(Module::cast).unwrap(); let module = file.syntax().descendants().find_map(Module::cast).unwrap();
assert_eq!("this\nis\nmod\nfoo", module.doc_comment_text().unwrap()); assert_eq!(" this\n is\n mod\n foo", module.doc_comments().doc_comment_text().unwrap());
} }
#[test] #[test]
@ -212,7 +215,7 @@ mod foo {}
.ok() .ok()
.unwrap(); .unwrap();
let module = file.syntax().descendants().find_map(Module::cast).unwrap(); let module = file.syntax().descendants().find_map(Module::cast).unwrap();
assert_eq!("this is mod foo", module.doc_comment_text().unwrap()); assert_eq!(" this is mod foo", module.doc_comments().doc_comment_text().unwrap());
} }
#[test] #[test]
@ -226,7 +229,7 @@ mod foo {}
.ok() .ok()
.unwrap(); .unwrap();
let module = file.syntax().descendants().find_map(Module::cast).unwrap(); let module = file.syntax().descendants().find_map(Module::cast).unwrap();
assert_eq!("this is mod foo ", module.doc_comment_text().unwrap()); assert_eq!(" this is mod foo ", module.doc_comments().doc_comment_text().unwrap());
} }
#[test] #[test]
@ -245,8 +248,8 @@ mod foo {}
.unwrap(); .unwrap();
let module = file.syntax().descendants().find_map(Module::cast).unwrap(); let module = file.syntax().descendants().find_map(Module::cast).unwrap();
assert_eq!( assert_eq!(
" this\n is\n mod foo\n ", "\n this\n is\n mod foo\n ",
module.doc_comment_text().unwrap() module.doc_comments().doc_comment_text().unwrap()
); );
} }
@ -259,8 +262,8 @@ fn test_comments_preserve_trailing_whitespace() {
.unwrap(); .unwrap();
let def = file.syntax().descendants().find_map(Struct::cast).unwrap(); let def = file.syntax().descendants().find_map(Struct::cast).unwrap();
assert_eq!( assert_eq!(
"Representation of a Realm. \nIn the specification these are called Realm Records.", " Representation of a Realm. \n In the specification these are called Realm Records.",
def.doc_comment_text().unwrap() def.doc_comments().doc_comment_text().unwrap()
); );
} }
@ -276,7 +279,7 @@ mod foo {}
.ok() .ok()
.unwrap(); .unwrap();
let module = file.syntax().descendants().find_map(Module::cast).unwrap(); let module = file.syntax().descendants().find_map(Module::cast).unwrap();
assert_eq!("doc comment", module.doc_comment_text().unwrap()); assert_eq!(" doc comment", module.doc_comments().doc_comment_text().unwrap());
} }
#[test] #[test]

View File

@ -33,23 +33,20 @@ pub fn prefix(&self) -> &'static str {
prefix prefix
} }
/// Returns the textual content of a doc comment block as a single string. /// Returns the textual content of a doc comment node as a single string with prefix and suffix
/// That is, strips leading `///` (+ optional 1 character of whitespace), /// removed.
/// trailing `*/`, trailing whitespace and then joins the lines.
pub fn doc_comment(&self) -> Option<&str> { pub fn doc_comment(&self) -> Option<&str> {
let kind = self.kind(); let kind = self.kind();
match kind { match kind {
CommentKind { shape, doc: Some(_) } => { CommentKind { shape, doc: Some(_) } => {
let prefix = kind.prefix(); let prefix = kind.prefix();
let text = &self.text()[prefix.len()..]; let text = &self.text()[prefix.len()..];
let ws = text.chars().next().filter(|c| c.is_whitespace()); let text = if shape == CommentShape::Block {
let text = ws.map_or(text, |ws| &text[ws.len_utf8()..]); text.strip_suffix("*/").unwrap_or(text)
match shape { } else {
CommentShape::Block if text.ends_with("*/") => { text
Some(&text[..text.len() - "*/".len()]) };
} Some(text)
_ => Some(text),
}
} }
_ => None, _ => None,
} }

View File

@ -1,8 +1,6 @@
//! Various traits that are implemented by ast nodes. //! Various traits that are implemented by ast nodes.
//! //!
//! The implementations are usually trivial, and live in generated.rs //! The implementations are usually trivial, and live in generated.rs
use itertools::Itertools;
use crate::{ use crate::{
ast::{self, support, AstChildren, AstNode, AstToken}, ast::{self, support, AstChildren, AstNode, AstToken},
syntax_node::SyntaxElementChildren, syntax_node::SyntaxElementChildren,
@ -76,10 +74,6 @@ pub trait DocCommentsOwner: AttrsOwner {
fn doc_comments(&self) -> CommentIter { fn doc_comments(&self) -> CommentIter {
CommentIter { iter: self.syntax().children_with_tokens() } CommentIter { iter: self.syntax().children_with_tokens() }
} }
fn doc_comment_text(&self) -> Option<String> {
self.doc_comments().doc_comment_text()
}
} }
impl CommentIter { impl CommentIter {
@ -87,12 +81,12 @@ pub fn from_syntax_node(syntax_node: &ast::SyntaxNode) -> CommentIter {
CommentIter { iter: syntax_node.children_with_tokens() } CommentIter { iter: syntax_node.children_with_tokens() }
} }
/// Returns the textual content of a doc comment block as a single string. #[cfg(test)]
/// That is, strips leading `///` (+ optional 1 character of whitespace),
/// trailing `*/`, trailing whitespace and then joins the lines.
pub fn doc_comment_text(self) -> Option<String> { pub fn doc_comment_text(self) -> Option<String> {
let docs = let docs = itertools::Itertools::join(
self.filter_map(|comment| comment.doc_comment().map(ToOwned::to_owned)).join("\n"); &mut self.filter_map(|comment| comment.doc_comment().map(ToOwned::to_owned)),
"\n",
);
if docs.is_empty() { if docs.is_empty() {
None None
} else { } else {