Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix indentation of structured multi-line comments #81

Merged
merged 4 commits into from
Oct 8, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 32 additions & 4 deletions src/commonmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,36 +41,64 @@ 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",
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 = textwrap::indent(&inner, " ");
// drop leading indentation, the `: ` serves this function already

format!(
// The `:` creates another definition list of which `indented` is the term.
"structured function argument\n\n: {}",
// drop leading indentation on the first line, the `: ` serves this function
// already.
indented.trim_start()
)
}
}
}
}

/// Since the first line starts with `: `, indent every other line by 2 spaces, so
/// that the text aligns, to result in:
///
/// : first line
/// every other line
fn handle_indentation(raw: &str) -> String {
match raw.split_once('\n') {
Some((first, rest)) => {
format!("{}\n{}", first, textwrap::indent(rest, " "))
}
None => raw.into(),
}
}

/// Represents a single manual section describing a library function.
#[derive(Clone, Debug)]
pub struct ManualEntry {
Expand Down
72 changes: 61 additions & 11 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 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,18 +268,25 @@ 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),
doc: handle_indentation(
&retrieve_doc_comment(id.syntax(), true).unwrap_or_default(),
),
}));
}
// 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 {
name: entry.ident().unwrap().to_string(),
doc: retrieve_doc_comment(entry.syntax(), true),
doc: handle_indentation(
&retrieve_doc_comment(entry.syntax(), true).unwrap_or_default(),
),
})
.collect();

Expand Down Expand Up @@ -297,6 +324,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 +364,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 +503,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);
}
31 changes: 31 additions & 0 deletions src/snapshots/nixdoc__multi_line.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
source: src/main.rs
expression: output
---
## `lib.let.f` {#function-library-lib.let.f}

Some description

structured function argument

: `x`

: First line

Second line


## `lib.let.g` {#function-library-lib.let.g}

Some other description

structured function argument

: `x`

: First line

Second line



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

Second line
*/
x
}: x;

/* Some other description */
g =
{
/*
First line

Second line
*/
x
}: x;
}