Repository: nix-community/nixpkgs-fmt (rust)

Issue:
remove line break after expanded let ... in
```nix
# One line, everyone's happy
let self = { }; in self
```


https://github.com/nix-community/nixpkgs-fmt/issues/65#issuecomment-507306029 suggests that we could consider `let ... in` just as an `assert ...;` or a `with ... ;` statement that does not indent the following body. In that case example 2 is inconsistent.

```nix
# input
# This should be okay if `let` is considered like a `with` or `assert` 
let self = { }; in
self
```

```nix
# nix-fmt output
# But it is expanded :-(
let
  self = {};
in
self
```

Overall, it seems that the `let ... in body` is expanded (split across multiple lines as long as it is not written on one line. I think that expanding the `let ... in` part should be independent from expanding the body itself and from decoupling the `in` from the body.

The above example is made even worse by the fact that we do not indent toplevel let bodies (https://github.com/nix-community/nixpkgs-fmt/pull/108). The `self` really looks weird and could be on the same line as the `in`.

May I propose the following patterns ?

```nix
let foo = bar; in foo
```
```nix
let foo = bar; in
foo
```
```nix
let
  foo = bar;
in {
  bar = foo;
}
```

The implicit rule is that when the let+in construct is one one line, it behaves like a `with` or `assert`, and when it is on several lines (expanded form), no line break is enforced after the `in` (which means that line breaks are only enforced for a multi-line body after a single line let+in).

Pull request:
Changing SpacingDsl behavior for let ... in node
This PR fixes #125 


Patch:
diff --git a/README.md b/README.md
index 3e0a294..04273d6 100644
--- a/README.md
+++ b/README.md
@@ -186,9 +186,3 @@ Feel free to submit your project!
 * [nixpkgs style guide](https://nixos.org/nixpkgs/manual/#sec-syntax)
 * [On Nix expression formatting](https://discourse.nixos.org/t/on-nix-expression-formatting/1521/14)
 * [[Job] Implement a `nix-fmt` formatter](https://discourse.nixos.org/t/job-implement-a-nix-fmt-formatter/2819/12)
-
-## Sponsors
-
-This work has been sponsored by [NumTide](https://numtide.com).
-
-<img src="https://numtide.com/logo.png" alt="NumTide Logo" width="80">
diff --git a/src/dsl.rs b/src/dsl.rs
index 1bed023..4810730 100644
--- a/src/dsl.rs
+++ b/src/dsl.rs
@@ -172,6 +172,10 @@ impl<'a> SpacingRuleBuilder<'a> {
     pub(crate) fn single_space(self) -> &'a mut SpacingDsl {
         self.finish(SpaceValue::Single)
     }
+    /// Enforce newline
+    pub(crate) fn newline(self) -> &'a mut SpacingDsl {
+        self.finish(SpaceValue::Newline)
+    }
     pub(crate) fn single_space_or_optional_newline(self) -> &'a mut SpacingDsl {
         self.finish(SpaceValue::SingleOptionalNewline)
     }
diff --git a/src/rules.rs b/src/rules.rs
index 3fe7b55..2aefbb3 100644
--- a/src/rules.rs
+++ b/src/rules.rs
@@ -1,15 +1,17 @@
 //! This module contains specific `super::dsl` rules for formatting nix language.
 use rnix::{
     types::{Lambda, TypedNode, With},
-    SyntaxElement, SyntaxKind,
+    NodeOrToken, SyntaxElement, SyntaxKind,
     SyntaxKind::*,
-    T,
+    SyntaxToken, T,
 };
 
 use crate::{
     dsl::{self, IndentDsl, IndentValue::*, SpacingDsl},
     pattern::p,
-    tree_utils::{has_newline, next_non_whitespace_sibling, prev_sibling},
+    tree_utils::{
+        get_parent, has_newline, next_non_whitespace_sibling, next_token_sibling, prev_sibling,
+    },
 };
 
 #[rustfmt::skip]
@@ -85,8 +87,9 @@ pub(crate) fn spacing() -> SpacingDsl {
         .inside(NODE_INHERIT_FROM).before(T![")"]).no_space()
 
         .test("let   foo = bar;in  92", "let foo = bar; in 92")
-        .inside(NODE_LET_IN).after(T![let]).single_space_or_newline()
-        .inside(NODE_LET_IN).around(T![in]).single_space_or_newline()
+        .inside(NODE_LET_IN).after(T![let]).single_space_or_optional_newline()
+        .inside(NODE_LET_IN).before(T![in]).single_space_or_optional_newline()
+        .inside(NODE_LET_IN).after(T![in]).single_space_or_newline()
 
         .test("{a?3}: a", "{ a ? 3 }: a")
         .inside(NODE_PAT_ENTRY).around(T![?]).single_space()
@@ -125,6 +128,16 @@ pub(crate) fn spacing() -> SpacingDsl {
             pattern: NODE_ROOT.into(),
             space: dsl::Space { loc: dsl::SpaceLoc::After, value: dsl::SpaceValue::Newline }
         })
+        .add_rule(dsl::SpacingRule {
+            name: None,
+            pattern: p(TOKEN_LET) & p(let_contain_linebreak_pattern),
+            space: dsl::Space { loc: dsl::SpaceLoc::After, value: dsl::SpaceValue::Newline }
+        })
+        .add_rule(dsl::SpacingRule {
+            name: None,
+            pattern: p(TOKEN_IN) & p(in_contain_linebreak_pattern),
+            space: dsl::Space { loc: dsl::SpaceLoc::After, value: dsl::SpaceValue::Newline }
+        })
         ;
 
     dsl
@@ -152,6 +165,30 @@ fn next_sibling_is_multiline_lambda_pattern(element: &SyntaxElement) -> bool {
     find(element) == Some(true)
 }
 
+// special-cased to force a linebreak after `let` when `in` is not inline
+fn let_contain_linebreak_pattern(element: &SyntaxElement) -> bool {
+    fn find(element: &SyntaxElement) -> Option<bool> {
+        let token_let = next_non_whitespace_sibling(element)?;
+        let let_pattern = next_token_sibling(&token_let)?.text().contains('\n');
+
+        Some(let_pattern)
+    }
+
+    find(element) == Some(true)
+}
+
+fn in_contain_linebreak_pattern(element: &SyntaxElement) -> bool {
+    fn find(element: &SyntaxElement) -> Option<bool> {
+        let token_in = get_parent(element)?.next_sibling_or_token()?;
+        match token_in {
+            NodeOrToken::Node(_) => None,
+            NodeOrToken::Token(it) => Some(it.text().contains(';')),
+        }
+    }
+
+    find(element) == Some(true)
+}
+
 #[rustfmt::skip]
 pub(crate) fn indentation() -> IndentDsl {
     let mut dsl = IndentDsl::default();
diff --git a/src/tree_utils.rs b/src/tree_utils.rs
index e973255..5b04c2c 100644
--- a/src/tree_utils.rs
+++ b/src/tree_utils.rs
@@ -27,6 +27,10 @@ pub(crate) fn has_newline(node: &SyntaxNode) -> bool {
     walk_tokens(node).any(|it| it.text().contains('\n'))
 }
 
+pub(crate) fn get_parent(element: &SyntaxElement) -> Option<SyntaxNode> {
+    element.parent()
+}
+
 pub(crate) fn prev_sibling(element: &SyntaxElement) -> Option<SyntaxNode> {
     successors(element.prev_sibling_or_token(), |it| it.prev_sibling_or_token()).find_map(
         |element| match element {
@@ -35,7 +39,14 @@ pub(crate) fn prev_sibling(element: &SyntaxElement) -> Option<SyntaxNode> {
         },
     )
 }
-
+pub(crate) fn next_token_sibling(element: &SyntaxElement) -> Option<SyntaxToken> {
+    successors(element.next_sibling_or_token(), |it| it.next_sibling_or_token()).find_map(
+        |element| match element {
+            NodeOrToken::Node(_) => None,
+            NodeOrToken::Token(it) => Some(it),
+        },
+    )
+}
 pub(crate) fn prev_non_whitespace_sibling(element: &SyntaxElement) -> Option<SyntaxElement> {
     successors(element.prev_sibling_or_token(), |it| it.prev_sibling_or_token())
         .find(|it| it.kind() != TOKEN_WHITESPACE)


Test patch:
diff --git a/test_data/issue-125.bad.nix b/test_data/issue-125.bad.nix
new file mode 100644
index 0000000..6b70906
--- /dev/null
+++ b/test_data/issue-125.bad.nix
@@ -0,0 +1,8 @@
+{foo,bar}:
+let
+foo = let x = y; in
+z;
+#all in one line
+bar = let x = 3; in x;
+in
+body
diff --git a/test_data/issue-125.good.nix b/test_data/issue-125.good.nix
new file mode 100644
index 0000000..15a9fce
--- /dev/null
+++ b/test_data/issue-125.good.nix
@@ -0,0 +1,9 @@
+{ foo, bar }:
+let
+  foo = let x = y; in
+    z;
+  #all in one line
+  bar = let x = 3; in
+    x;
+in
+body