Skip to content

Commit

Permalink
Fix indentation of structured multi-line comments
Browse files Browse the repository at this point in the history
Closes #75
  • Loading branch information
asymmetric committed Sep 16, 2023
1 parent d4f0a1d commit fbd0dc5
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 15 deletions.
45 changes: 40 additions & 5 deletions src/commonmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,36 +41,71 @@ 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()
)
}
}
}
}

/// 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 {
Expand Down
66 changes: 56 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ impl DocItem {
/// Retrieve documentation comments.
fn retrieve_doc_comment(node: &SyntaxNode, allow_line_comments: bool) -> Option<String> {
// 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()?;
Expand All @@ -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.
Expand Down Expand Up @@ -173,27 +173,47 @@ fn retrieve_doc_item(node: &AttrpathValue) -> Option<DocItem> {
})
}

/// 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:
/// 10
/// */
/// 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<String> {
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(),
};

Expand Down Expand Up @@ -248,13 +268,16 @@ fn collect_lambda_args(mut lambda: Lambda) -> Vec<Argument> {

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 {
Expand Down Expand Up @@ -297,6 +320,9 @@ fn collect_entry_information(entry: AttrpathValue) -> Option<DocItem> {
}
}

// a binding is an assignment, which can take place in an attrset
// - as attributes
// - as inherits
fn collect_bindings(
node: &SyntaxNode,
category: &str,
Expand Down Expand Up @@ -334,6 +360,8 @@ fn collect_bindings(
vec![]
}

// Main entrypoint for collection
// TODO: document
fn collect_entries(root: rnix::Root, category: &str) -> Vec<ManualEntry> {
// we will look into the top-level let and its body for function docs.
// we only need a single level of scope for this.
Expand Down Expand Up @@ -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);
}
18 changes: 18 additions & 0 deletions src/snapshots/nixdoc__multi_line.snap
Original file line number Diff line number Diff line change
@@ -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



12 changes: 12 additions & 0 deletions test/multi-line.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
/* Some description */
f =
{
/* First line
Second line
*/
x
,
}: x;
}

0 comments on commit fbd0dc5

Please sign in to comment.