From fbd0dc5c022e1be8c79be3458e55e6e38fce8e0c Mon Sep 17 00:00:00 2001 From: Lorenzo Manacorda Date: Fri, 15 Sep 2023 23:03:27 +0200 Subject: [PATCH] Fix indentation of structured multi-line comments Closes #75 --- src/commonmark.rs | 45 ++++++++++++++++-- src/main.rs | 66 +++++++++++++++++++++++---- src/snapshots/nixdoc__multi_line.snap | 18 ++++++++ test/multi-line.nix | 12 +++++ 4 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 src/snapshots/nixdoc__multi_line.snap create mode 100644 test/multi-line.nix diff --git a/src/commonmark.rs b/src/commonmark.rs index 9197130..bb23373 100644 --- a/src/commonmark.rs +++ b/src/commonmark.rs @@ -41,29 +41,44 @@ pub enum Argument { impl Argument { /// Write CommonMark structure for a single function argument. + /// We use the definition list extension, which prepends each argument with `: `. + /// For pattern arguments, we create a nested definition list. fn format_argument(self) -> String { match self { - // Write a flat argument entry. + // Write a flat argument entry, e.g. `id = x: x` + // + // `x` + // : Function argument Argument::Flat(arg) => { format!( - "`{}`\n\n: {}\n\n", + "`{}`\n\n{}\n\n", arg.name, - arg.doc.unwrap_or("Function argument".into()).trim() + handle_indentation(arg.doc.unwrap_or("Function argument".into()).trim()) ) } // Write a pattern argument entry and its individual - // parameters as a nested structure. + // parameters as a nested structure, e.g.: + // + // `foo = { a }: a` + // + // structured function argument + // : `a` + // : Function argument Argument::Pattern(pattern_args) => { let mut inner = String::new(); for pattern_arg in pattern_args { inner += &Argument::Flat(pattern_arg).format_argument(); } + // let indented = handle_indentation(&inner); let indented = textwrap::indent(&inner, " "); - // drop leading indentation, the `: ` serves this function already + format!( "structured function argument\n\n: {}", + // drop leading indentation on the first line, the `: ` serves this function + // already. + // The `:` creates another definition list of which `indented` is the term. indented.trim_start() ) } @@ -71,6 +86,26 @@ impl Argument { } } +/// Indent doc-comments so that the first line always starts with `: `, and the text of the other +/// lines is aligned with the first: +/// +/// : first line +/// second line (optional) +fn handle_indentation(raw: &str) -> String { + let result: String = match raw.split_once('\n') { + Some((first, rest)) => { + format!( + "{}\n{}", + textwrap::indent(first, ": "), + textwrap::indent(rest, " ") + ) + } + None => textwrap::indent(raw, ": "), + }; + + result +} + /// Represents a single manual section describing a library function. #[derive(Clone, Debug)] pub struct ManualEntry { diff --git a/src/main.rs b/src/main.rs index 1e6f667..3148251 100644 --- a/src/main.rs +++ b/src/main.rs @@ -100,7 +100,7 @@ impl DocItem { /// Retrieve documentation comments. fn retrieve_doc_comment(node: &SyntaxNode, allow_line_comments: bool) -> Option { // if the current node has a doc comment it'll be immediately preceded by that comment, - // or there will be a whitespace token and *then* the comment tokens before it. we merge + // or there will be a whitespace token and *then* the comment tokens before it. We merge // multiple line comments into one large comment if they are on adjacent lines for // documentation simplicity. let mut token = node.first_token()?.prev_token()?; @@ -124,7 +124,7 @@ fn retrieve_doc_comment(node: &SyntaxNode, allow_line_comments: bool) -> Option< } if token.text().starts_with("/*") { - return Some(Comment::cast(token)?.text().to_string()); + return handle_indentation(Comment::cast(token)?.text()); } // backtrack to the start of the doc comment, allowing only adjacent line comments. @@ -173,10 +173,11 @@ fn retrieve_doc_item(node: &AttrpathValue) -> Option { }) } -/// Dedent everything but the first line, whose indentation gets fully removed all the time +/// Ensure all lines in a multi-line doc-comments have the same indentation. /// -/// A doc comment like this in Nix: +/// Consider such a doc comment: /// +/// ```nix /// { /// /* foo is /// the value: @@ -184,16 +185,35 @@ fn retrieve_doc_item(node: &AttrpathValue) -> Option { /// */ /// foo = 10; /// } +/// ``` /// -/// The parser turns this into "foo is\n the value:\n 10\n" where the first -/// line has no leading indentation, but the rest do +/// The parser turns this into: /// -/// To align all lines to the same indentation, while preserving the -/// formatting, we dedent all but the first line, while stripping any potential -/// indentation from the first line. +/// ``` +/// foo is +/// the value: +/// 10 +/// ``` +/// +/// +/// where the first line has no leading indentation, and all other lines have preserved their +/// original indentation. +/// +/// What we want instead is: +/// +/// ``` +/// foo is +/// the value: +/// 10 +/// ``` +/// +/// i.e. we want the whole thing to be dedented. To achieve this, we remove all leading whitespace +/// from the first line, and remove all common whitespace from the rest of the string. fn handle_indentation(raw: &str) -> Option { let result: String = match raw.split_once('\n') { - Some((first, rest)) => format!("{}\n{}", first.trim(), dedent(rest)), + Some((first, rest)) => { + format!("{}\n{}", first.trim_start(), dedent(rest)) + } None => raw.into(), }; @@ -248,13 +268,16 @@ fn collect_lambda_args(mut lambda: Lambda) -> Vec { loop { match lambda.param().unwrap() { + // a variable, e.g. `id = x: x` Param::IdentParam(id) => { args.push(Argument::Flat(SingleArg { name: id.to_string(), doc: retrieve_doc_comment(id.syntax(), true), })); } + // an attribute set, e.g. `foo = { a }: a` Param::Pattern(pat) => { + // collect doc-comments for each attribute in the set let pattern_vec: Vec<_> = pat .pat_entries() .map(|entry| SingleArg { @@ -297,6 +320,9 @@ fn collect_entry_information(entry: AttrpathValue) -> Option { } } +// a binding is an assignment, which can take place in an attrset +// - as attributes +// - as inherits fn collect_bindings( node: &SyntaxNode, category: &str, @@ -334,6 +360,8 @@ fn collect_bindings( vec![] } +// Main entrypoint for collection +// TODO: document fn collect_entries(root: rnix::Root, category: &str) -> Vec { // we will look into the top-level let and its body for function docs. // we only need a single level of scope for this. @@ -471,3 +499,21 @@ fn test_line_comments() { insta::assert_snapshot!(output); } + +#[test] +fn test_multi_line() { + let mut output = Vec::new(); + let src = fs::read_to_string("test/multi-line.nix").unwrap(); + let nix = rnix::Root::parse(&src).ok().expect("failed to parse input"); + let category = "let"; + + for entry in collect_entries(nix, category) { + entry + .write_section(&Default::default(), &mut output) + .expect("Failed to write section") + } + + let output = String::from_utf8(output).expect("not utf8"); + + insta::assert_snapshot!(output); +} diff --git a/src/snapshots/nixdoc__multi_line.snap b/src/snapshots/nixdoc__multi_line.snap new file mode 100644 index 0000000..bebcd66 --- /dev/null +++ b/src/snapshots/nixdoc__multi_line.snap @@ -0,0 +1,18 @@ +--- +source: src/main.rs +expression: output +--- +## `lib.let.f` {#function-library-lib.let.f} + +Some description + +structured function argument + +: `x` + + : First line + + Second line + + + diff --git a/test/multi-line.nix b/test/multi-line.nix new file mode 100644 index 0000000..4247677 --- /dev/null +++ b/test/multi-line.nix @@ -0,0 +1,12 @@ +{ + /* Some description */ + f = + { + /* First line + + Second line + */ + x + , + }: x; +}