diff --git a/crates/hir-def/src/attr.rs b/crates/hir-def/src/attr.rs index 07dbafec74..c6454eb9ea 100644 --- a/crates/hir-def/src/attr.rs +++ b/crates/hir-def/src/attr.rs @@ -5,7 +5,7 @@ pub mod builtin; #[cfg(test)] mod tests; -use std::{hash::Hash, ops}; +use std::{hash::Hash, ops, slice::Iter as SliceIter}; use base_db::CrateId; use cfg::{CfgExpr, CfgOptions}; @@ -251,7 +251,6 @@ impl Attrs { } } -use std::slice::Iter as SliceIter; #[derive(Debug, Clone, PartialEq, Eq, Hash, Ord, PartialOrd)] pub enum DocAtom { /// eg. `#[doc(hidden)]` @@ -265,7 +264,6 @@ pub enum DocAtom { // Adapted from `CfgExpr` parsing code #[derive(Debug, Clone, PartialEq, Eq, Hash)] -// #[cfg_attr(test, derive(derive_arbitrary::Arbitrary))] pub enum DocExpr { Invalid, /// eg. `#[doc(hidden)]`, `#[doc(alias = "x")]` diff --git a/crates/ide-db/src/documentation.rs b/crates/ide-db/src/documentation.rs index aa8a374218..26f3cd28a2 100644 --- a/crates/ide-db/src/documentation.rs +++ b/crates/ide-db/src/documentation.rs @@ -1,3 +1,4 @@ +//! Documentation attribute related utilties. use either::Either; use hir::{ db::{DefDatabase, HirDatabase}, diff --git a/crates/ide-db/src/rust_doc.rs b/crates/ide-db/src/rust_doc.rs index e27e23867a..ab2a250289 100644 --- a/crates/ide-db/src/rust_doc.rs +++ b/crates/ide-db/src/rust_doc.rs @@ -1,5 +1,7 @@ //! Rustdoc specific doc comment handling +use crate::documentation::Documentation; + // stripped down version of https://github.com/rust-lang/rust/blob/392ba2ba1a7d6c542d2459fb8133bebf62a4a423/src/librustdoc/html/markdown.rs#L810-L933 pub fn is_rust_fence(s: &str) -> bool { let mut seen_rust_tags = false; @@ -32,3 +34,170 @@ pub fn is_rust_fence(s: &str) -> bool { !seen_other_tags || seen_rust_tags } + +const RUSTDOC_FENCES: [&str; 2] = ["```", "~~~"]; + +pub fn format_docs(src: &Documentation) -> String { + format_docs_(src.as_str()) +} + +fn format_docs_(src: &str) -> String { + let mut processed_lines = Vec::new(); + let mut in_code_block = false; + let mut is_rust = false; + + for mut line in src.lines() { + if in_code_block && is_rust && code_line_ignored_by_rustdoc(line) { + continue; + } + + if let Some(header) = RUSTDOC_FENCES.into_iter().find_map(|fence| line.strip_prefix(fence)) + { + in_code_block ^= true; + + if in_code_block { + is_rust = is_rust_fence(header); + + if is_rust { + line = "```rust"; + } + } + } + + if in_code_block { + let trimmed = line.trim_start(); + if is_rust && trimmed.starts_with("##") { + line = &trimmed[1..]; + } + } + + processed_lines.push(line); + } + processed_lines.join("\n") +} + +fn code_line_ignored_by_rustdoc(line: &str) -> bool { + let trimmed = line.trim(); + trimmed == "#" || trimmed.starts_with("# ") || trimmed.starts_with("#\t") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_format_docs_adds_rust() { + let comment = "```\nfn some_rust() {}\n```"; + assert_eq!(format_docs_(comment), "```rust\nfn some_rust() {}\n```"); + } + + #[test] + fn test_format_docs_handles_plain_text() { + let comment = "```text\nthis is plain text\n```"; + assert_eq!(format_docs_(comment), "```text\nthis is plain text\n```"); + } + + #[test] + fn test_format_docs_handles_non_rust() { + let comment = "```sh\nsupposedly shell code\n```"; + assert_eq!(format_docs_(comment), "```sh\nsupposedly shell code\n```"); + } + + #[test] + fn test_format_docs_handles_rust_alias() { + let comment = "```ignore\nlet z = 55;\n```"; + assert_eq!(format_docs_(comment), "```rust\nlet z = 55;\n```"); + } + + #[test] + fn test_format_docs_handles_complex_code_block_attrs() { + let comment = "```rust,no_run\nlet z = 55;\n```"; + assert_eq!(format_docs_(comment), "```rust\nlet z = 55;\n```"); + } + + #[test] + fn test_format_docs_handles_error_codes() { + let comment = "```compile_fail,E0641\nlet b = 0 as *const _;\n```"; + assert_eq!(format_docs_(comment), "```rust\nlet b = 0 as *const _;\n```"); + } + + #[test] + fn test_format_docs_skips_comments_in_rust_block() { + let comment = + "```rust\n # skip1\n# skip2\n#stay1\nstay2\n#\n #\n # \n #\tskip3\n\t#\t\n```"; + assert_eq!(format_docs_(comment), "```rust\n#stay1\nstay2\n```"); + } + + #[test] + fn test_format_docs_does_not_skip_lines_if_plain_text() { + let comment = + "```text\n # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t\n```"; + assert_eq!( + format_docs_(comment), + "```text\n # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t\n```", + ); + } + + #[test] + fn test_format_docs_keeps_comments_outside_of_rust_block() { + let comment = " # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t"; + assert_eq!(format_docs_(comment), comment); + } + + #[test] + fn test_format_docs_preserves_newlines() { + let comment = "this\nis\nmultiline"; + assert_eq!(format_docs_(comment), comment); + } + + #[test] + fn test_code_blocks_in_comments_marked_as_rust() { + let comment = r#"```rust +fn main(){} +``` +Some comment. +``` +let a = 1; +```"#; + + assert_eq!( + format_docs_(comment), + "```rust\nfn main(){}\n```\nSome comment.\n```rust\nlet a = 1;\n```" + ); + } + + #[test] + fn test_code_blocks_in_comments_marked_as_text() { + let comment = r#"```text +filler +text +``` +Some comment. +``` +let a = 1; +```"#; + + assert_eq!( + format_docs_(comment), + "```text\nfiller\ntext\n```\nSome comment.\n```rust\nlet a = 1;\n```" + ); + } + + #[test] + fn test_format_docs_handles_escape_double_hashes() { + let comment = r#"```rust +let s = "foo +## bar # baz"; +```"#; + + assert_eq!(format_docs_(comment), "```rust\nlet s = \"foo\n# bar # baz\";\n```"); + } + + #[test] + fn test_format_docs_handles_double_hashes_non_rust() { + let comment = r#"```markdown +## A second-level heading +```"#; + assert_eq!(format_docs_(comment), "```markdown\n## A second-level heading\n```"); + } +} diff --git a/crates/ide/src/signature_help.rs b/crates/ide/src/signature_help.rs index f57eb2ba3e..e020b52e17 100644 --- a/crates/ide/src/signature_help.rs +++ b/crates/ide/src/signature_help.rs @@ -8,7 +8,7 @@ use hir::{AssocItem, GenericParam, HirDisplay, ModuleDef, PathResolution, Semant use ide_db::{ active_parameter::{callable_for_node, generic_def_for_node}, base_db::FilePosition, - documentation::HasDocs, + documentation::{Documentation, HasDocs}, FxIndexMap, }; use stdx::format_to; @@ -27,7 +27,7 @@ use crate::RootDatabase; /// edited. #[derive(Debug)] pub struct SignatureHelp { - pub doc: Option, + pub doc: Option, pub signature: String, pub active_parameter: Option, parameters: Vec, @@ -178,7 +178,7 @@ fn signature_help_for_call( let mut fn_params = None; match callable.kind() { hir::CallableKind::Function(func) => { - res.doc = func.docs(db).map(|it| it.into()); + res.doc = func.docs(db); format_to!(res.signature, "fn {}", func.name(db).display(db)); fn_params = Some(match callable.receiver_param(db) { Some(_self) => func.params_without_self(db), @@ -186,11 +186,11 @@ fn signature_help_for_call( }); } hir::CallableKind::TupleStruct(strukt) => { - res.doc = strukt.docs(db).map(|it| it.into()); + res.doc = strukt.docs(db); format_to!(res.signature, "struct {}", strukt.name(db).display(db)); } hir::CallableKind::TupleEnumVariant(variant) => { - res.doc = variant.docs(db).map(|it| it.into()); + res.doc = variant.docs(db); format_to!( res.signature, "enum {}::{}", @@ -264,38 +264,38 @@ fn signature_help_for_generics( let db = sema.db; match generics_def { hir::GenericDef::Function(it) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "fn {}", it.name(db).display(db)); } hir::GenericDef::Adt(hir::Adt::Enum(it)) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "enum {}", it.name(db).display(db)); } hir::GenericDef::Adt(hir::Adt::Struct(it)) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "struct {}", it.name(db).display(db)); } hir::GenericDef::Adt(hir::Adt::Union(it)) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "union {}", it.name(db).display(db)); } hir::GenericDef::Trait(it) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "trait {}", it.name(db).display(db)); } hir::GenericDef::TraitAlias(it) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "trait {}", it.name(db).display(db)); } hir::GenericDef::TypeAlias(it) => { - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); format_to!(res.signature, "type {}", it.name(db).display(db)); } hir::GenericDef::Variant(it) => { // In paths, generics of an enum can be specified *after* one of its variants. // eg. `None::` // We'll use the signature of the enum, but include the docs of the variant. - res.doc = it.docs(db).map(|it| it.into()); + res.doc = it.docs(db); let enum_ = it.parent_enum(db); format_to!(res.signature, "enum {}", enum_.name(db).display(db)); generics_def = enum_.into(); diff --git a/crates/rust-analyzer/src/lib.rs b/crates/rust-analyzer/src/lib.rs index 04ac77b1f6..6c62577f69 100644 --- a/crates/rust-analyzer/src/lib.rs +++ b/crates/rust-analyzer/src/lib.rs @@ -26,7 +26,6 @@ mod dispatch; mod global_state; mod line_index; mod main_loop; -mod markdown; mod mem_docs; mod op_queue; mod reload; diff --git a/crates/rust-analyzer/src/lsp/to_proto.rs b/crates/rust-analyzer/src/lsp/to_proto.rs index daa7f5fe19..57f4602254 100644 --- a/crates/rust-analyzer/src/lsp/to_proto.rs +++ b/crates/rust-analyzer/src/lsp/to_proto.rs @@ -13,6 +13,7 @@ use ide::{ RenameError, Runnable, Severity, SignatureHelp, SnippetEdit, SourceChange, StructureNodeKind, SymbolKind, TextEdit, TextRange, TextSize, }; +use ide_db::rust_doc::format_docs; use itertools::Itertools; use serde_json::to_value; use vfs::AbsPath; @@ -105,7 +106,7 @@ pub(crate) fn diagnostic_severity(severity: Severity) -> lsp_types::DiagnosticSe } pub(crate) fn documentation(documentation: Documentation) -> lsp_types::Documentation { - let value = crate::markdown::format_docs(documentation.as_str()); + let value = format_docs(&documentation); let markup_content = lsp_types::MarkupContent { kind: lsp_types::MarkupKind::Markdown, value }; lsp_types::Documentation::MarkupContent(markup_content) } @@ -416,7 +417,7 @@ pub(crate) fn signature_help( let documentation = call_info.doc.filter(|_| config.docs).map(|doc| { lsp_types::Documentation::MarkupContent(lsp_types::MarkupContent { kind: lsp_types::MarkupKind::Markdown, - value: crate::markdown::format_docs(&doc), + value: format_docs(&doc), }) }); @@ -1531,7 +1532,7 @@ pub(crate) fn markup_content( ide::HoverDocFormat::Markdown => lsp_types::MarkupKind::Markdown, ide::HoverDocFormat::PlainText => lsp_types::MarkupKind::PlainText, }; - let value = crate::markdown::format_docs(markup.as_str()); + let value = format_docs(&Documentation::new(markup.into())); lsp_types::MarkupContent { kind, value } } diff --git a/crates/rust-analyzer/src/markdown.rs b/crates/rust-analyzer/src/markdown.rs deleted file mode 100644 index 140e16ecf1..0000000000 --- a/crates/rust-analyzer/src/markdown.rs +++ /dev/null @@ -1,167 +0,0 @@ -//! Transforms rust like doc content to markdown, replacing rustdoc fences and removing rustdoc code -//! block comments. -use ide_db::rust_doc::is_rust_fence; - -const RUSTDOC_FENCES: [&str; 2] = ["```", "~~~"]; - -// FIXME: why is this in this crate? -pub(crate) fn format_docs(src: &str) -> String { - let mut processed_lines = Vec::new(); - let mut in_code_block = false; - let mut is_rust = false; - - for mut line in src.lines() { - if in_code_block && is_rust && code_line_ignored_by_rustdoc(line) { - continue; - } - - if let Some(header) = RUSTDOC_FENCES.into_iter().find_map(|fence| line.strip_prefix(fence)) - { - in_code_block ^= true; - - if in_code_block { - is_rust = is_rust_fence(header); - - if is_rust { - line = "```rust"; - } - } - } - - if in_code_block { - let trimmed = line.trim_start(); - if is_rust && trimmed.starts_with("##") { - line = &trimmed[1..]; - } - } - - processed_lines.push(line); - } - processed_lines.join("\n") -} - -fn code_line_ignored_by_rustdoc(line: &str) -> bool { - let trimmed = line.trim(); - trimmed == "#" || trimmed.starts_with("# ") || trimmed.starts_with("#\t") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_format_docs_adds_rust() { - let comment = "```\nfn some_rust() {}\n```"; - assert_eq!(format_docs(comment), "```rust\nfn some_rust() {}\n```"); - } - - #[test] - fn test_format_docs_handles_plain_text() { - let comment = "```text\nthis is plain text\n```"; - assert_eq!(format_docs(comment), "```text\nthis is plain text\n```"); - } - - #[test] - fn test_format_docs_handles_non_rust() { - let comment = "```sh\nsupposedly shell code\n```"; - assert_eq!(format_docs(comment), "```sh\nsupposedly shell code\n```"); - } - - #[test] - fn test_format_docs_handles_rust_alias() { - let comment = "```ignore\nlet z = 55;\n```"; - assert_eq!(format_docs(comment), "```rust\nlet z = 55;\n```"); - } - - #[test] - fn test_format_docs_handles_complex_code_block_attrs() { - let comment = "```rust,no_run\nlet z = 55;\n```"; - assert_eq!(format_docs(comment), "```rust\nlet z = 55;\n```"); - } - - #[test] - fn test_format_docs_handles_error_codes() { - let comment = "```compile_fail,E0641\nlet b = 0 as *const _;\n```"; - assert_eq!(format_docs(comment), "```rust\nlet b = 0 as *const _;\n```"); - } - - #[test] - fn test_format_docs_skips_comments_in_rust_block() { - let comment = - "```rust\n # skip1\n# skip2\n#stay1\nstay2\n#\n #\n # \n #\tskip3\n\t#\t\n```"; - assert_eq!(format_docs(comment), "```rust\n#stay1\nstay2\n```"); - } - - #[test] - fn test_format_docs_does_not_skip_lines_if_plain_text() { - let comment = - "```text\n # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t\n```"; - assert_eq!( - format_docs(comment), - "```text\n # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t\n```", - ); - } - - #[test] - fn test_format_docs_keeps_comments_outside_of_rust_block() { - let comment = " # stay1\n# stay2\n#stay3\nstay4\n#\n #\n # \n #\tstay5\n\t#\t"; - assert_eq!(format_docs(comment), comment); - } - - #[test] - fn test_format_docs_preserves_newlines() { - let comment = "this\nis\nmultiline"; - assert_eq!(format_docs(comment), comment); - } - - #[test] - fn test_code_blocks_in_comments_marked_as_rust() { - let comment = r#"```rust -fn main(){} -``` -Some comment. -``` -let a = 1; -```"#; - - assert_eq!( - format_docs(comment), - "```rust\nfn main(){}\n```\nSome comment.\n```rust\nlet a = 1;\n```" - ); - } - - #[test] - fn test_code_blocks_in_comments_marked_as_text() { - let comment = r#"```text -filler -text -``` -Some comment. -``` -let a = 1; -```"#; - - assert_eq!( - format_docs(comment), - "```text\nfiller\ntext\n```\nSome comment.\n```rust\nlet a = 1;\n```" - ); - } - - #[test] - fn test_format_docs_handles_escape_double_hashes() { - let comment = r#"```rust -let s = "foo -## bar # baz"; -```"#; - - assert_eq!(format_docs(comment), "```rust\nlet s = \"foo\n# bar # baz\";\n```"); - } - - #[test] - fn test_format_docs_handles_double_hashes_non_rust() { - let comment = r#"```markdown -## A second-level heading -```"#; - assert_eq!(format_docs(comment), "```markdown\n## A second-level heading\n```"); - } -}