Repository: jlongster/prettier (js)

Issue:
Less readable after (arg => obj.method ... on same line
Overall, I liked output from the latest release, but I did find [this code]

```js
export default function theFunction(action$, store) {
  return action$
    .ofType(THE_ACTION)
    .switchMap(action => Observable
      .webSocket({
        url: (THE_URL),
        /*
        comments
        */
    	more: stuff(),
    	evenMore: stuff({
          value1: true,
          value2: false,
          value3: false
        })
      })
      .filter(data => theFilter(data))
      .map(({ theType, ...data }) => theMap(theType, data))
      .retryWhen(errors => errors.switchMap(err => {
        return navigator.onLine
          ? Observable.timer(3000 /* ms */)
          : Observable.fromEvent(window, 'online').take(1);
      })));
}
```

turns into

```js
export default function theFunction(action$, store) {
  return action$.ofType(THE_ACTION).switchMap(action => Observable.webSocket({
    url: THE_URL,
    /*
        comments
        */
    more: stuff(),
    evenMore: stuff({
      value1: true,
      value2: false,
      value3: false
    })
  })
    .filter(data => theFilter(data))
    .map(({ theType, ...data }) => theMap(theType, data))
    .retryWhen(errors => errors.switchMap(err => {
      return navigator.onLine
        ? Observable.timer(3000 /* ms */)
        : Observable.fromEvent(window, "online").take(1);
    })));
}
```

to be far less readable (where filter now gets indented). 

https://jlongster.github.io/prettier/#%7B%22content%22%3A%22export%20default%20function%20theFunction(action%24%2C%20store)%20%7B%5Cn%20%20return%20action%24%5Cn%20%20%20%20.ofType(THE_ACTION)%5Cn%20%20%20%20.switchMap(action%20%3D%3E%20Observable%5Cn%20%20%20%20%20%20.webSocket(%7B%5Cn%20%20%20%20%20%20%20%20url%3A%20(THE_URL)%2C%5Cn%20%20%20%20%20%20%20%20%2F*%5Cn%20%20%20%20%20%20%20%20comments%5Cn%20%20%20%20%20%20%20%20*%2F%5Cn%20%20%20%20%5Ctmore%3A%20stuff()%2C%5Cn%20%20%20%20%5CtevenMore%3A%20stuff(%7B%5Cn%20%20%20%20%20%20%20%20%20%20value1%3A%20true%2C%5Cn%20%20%20%20%20%20%20%20%20%20value2%3A%20false%2C%5Cn%20%20%20%20%20%20%20%20%20%20value3%3A%20false%5Cn%20%20%20%20%20%20%20%20%7D)%5Cn%20%20%20%20%20%20%7D)%5Cn%20%20%20%20%20%20.filter(data%20%3D%3E%20theFilter(data))%5Cn%20%20%20%20%20%20.map((%7B%20theType%2C%20...data%20%7D)%20%3D%3E%20theMap(theType%2C%20data))%5Cn%20%20%20%20%20%20.retryWhen(errors%20%3D%3E%20errors.switchMap(err%20%3D%3E%20%7B%5Cn%20%20%20%20%20%20%20%20return%20navigator.onLine%5Cn%20%20%20%20%20%20%20%20%20%20%3F%20Observable.timer(3000%20%2F*%20ms%20*%2F)%5Cn%20%20%20%20%20%20%20%20%20%20%3A%20Observable.fromEvent(window%2C%20'online').take(1)%3B%5Cn%20%20%20%20%20%20%7D)))%3B%5Cn%7D%5Cn%22%2C%22options%22%3A%7B%22printWidth%22%3A80%2C%22tabWidth%22%3A2%2C%22singleQuote%22%3Afalse%2C%22trailingComma%22%3Afalse%2C%22bracketSpacing%22%3Atrue%2C%22doc%22%3Afalse%7D%7D

Pull request description:
Soft break the first member of a chain
Before, we would always concatenate the first `.call()` if the identifier started with a capital letter, but we do want to break if the content of the call doesn't fit in one line.

Fixes #639

Patch:
diff --git a/editors/jetbrains/README.md b/editors/jetbrains/README.md
index f75eddde7..1a7ae61f4 100644
--- a/editors/jetbrains/README.md
+++ b/editors/jetbrains/README.md
@@ -14,6 +14,6 @@ If on the other hand you have `prettier` installed locally, replace the **Progra
 
 ### Configure Keymap
 
-Now when you setup **External Tool** I guess you what to add hotkey for it. Go to *File | Settings | Keymap* for Windows and Linux *WebStorm | Preferences | Keymap* and type external tool name in search box.
+Now when you setup **External Tool** I guess you want to add hotkey for it. Go to *File | Settings | Keymap* for Windows and Linux *WebStorm | Preferences | Keymap* and type external tool name in search box.
 
 See [this documentation](https://www.jetbrains.com/help/webstorm/configuring-keyboard-shortcuts.html) about configuring keyboard shortcuts.
diff --git a/src/comments.js b/src/comments.js
index d97dae762..6109ce834 100644
--- a/src/comments.js
+++ b/src/comments.js
@@ -170,12 +170,14 @@ function attach(comments, ast, text) {
         addDanglingComment(ast, comment);
       }
     } else {
-      // Otherwise, text exists both before and after the comment on
-      // the same line. If there is both a preceding and following
-      // node, use a tie-breaking algorithm to determine if it should
-      // be attached to the next or previous node. In the last case,
-      // simply attach the right node;
-      if (precedingNode && followingNode) {
+      if (handleIfStatementComments(enclosingNode, followingNode, comment)) {
+        // We're good
+      } else if (precedingNode && followingNode) {
+        // Otherwise, text exists both before and after the comment on
+        // the same line. If there is both a preceding and following
+        // node, use a tie-breaking algorithm to determine if it should
+        // be attached to the next or previous node. In the last case,
+        // simply attach the right node;
         const tieCount = tiesToBreak.length;
         if (tieCount > 0) {
           var lastTie = tiesToBreak[tieCount - 1];
diff --git a/src/doc-debug.js b/src/doc-debug.js
index efcf9d20d..e1fc15671 100644
--- a/src/doc-debug.js
+++ b/src/doc-debug.js
@@ -92,6 +92,12 @@ function printDoc(doc) {
       printDoc(doc.contents) +
       ")";
   }
+
+  if (doc.type === "line-suffix") {
+    return "lineSuffix(" + printDoc(doc.contents) + ")";
+  }
+
+  throw new Error('Unknown doc type ' + doc.type);
 }
 
 module.exports = {
diff --git a/src/printer.js b/src/printer.js
index e78ec9718..81ccc2ed5 100644
--- a/src/printer.js
+++ b/src/printer.js
@@ -684,13 +684,13 @@ function genericPrintNoParens(path, options, print) {
           parts.push(concat([": ", printedValue]));
         } else {
           parts.push(
-            concat([
+            group(concat([
               ":",
               ifBreak(" (", " "),
               indent(options.tabWidth, concat([softline, printedValue])),
               softline,
               ifBreak(")")
-            ])
+            ]))
           );
         }
       }
@@ -712,7 +712,14 @@ function genericPrintNoParens(path, options, print) {
     case "ArrayPattern":
       if (n.elements.length === 0) {
         parts.push(
-          concat(["[", comments.printDanglingComments(path, options), "]"])
+          group(
+            concat([
+              "[",
+              comments.printDanglingComments(path, options),
+              softline,
+              "]"
+            ])
+          )
         );
       } else {
         const lastElem = util.getLast(n.elements);
@@ -2224,22 +2231,23 @@ function printMemberChain(path, options, print) {
   // node is just an identifier with the name starting with a capital
   // letter or just a sequence of _$. The rationale is that they are
   // likely to be factories.
-  if (
+  const shouldMerge =
     groups[0].length === 1 &&
-      groups[0][0].node.type === "Identifier" &&
-      groups[0][0].node.name.match(/(^[A-Z])|^[_$]+$/) &&
-      groups.length >= 2
-  ) {
-    // Push all the values of groups[0] at the beginning of groups[1]
-    [].unshift.apply(groups[1], groups[0]);
-    // Remove groups[0]
-    groups.splice(0, 1);
-  }
+    groups[0][0].node.type === "Identifier" &&
+    groups[0][0].node.name.match(/(^[A-Z])|^[_$]+$/) &&
+    groups.length >= 2;
 
   function printGroup(printedGroup) {
     return concat(printedGroup.map(tuple => tuple.printed));
   }
 
+  function printIndentedGroup(groups, lineType) {
+    return indent(
+      options.tabWidth,
+      group(concat([lineType, join(lineType, groups.map(printGroup))]))
+    );
+  }
+
   const printedGroups = groups.map(printGroup);
 
   const oneLine = concat(printedGroups);
@@ -2252,10 +2260,8 @@ function printMemberChain(path, options, print) {
 
   const expanded = concat([
     printGroup(groups[0]),
-    indent(
-      options.tabWidth,
-      group(concat([hardline, join(hardline, groups.slice(1).map(printGroup))]))
-    )
+    shouldMerge ? printIndentedGroup(groups.slice(1, 2), softline) : "",
+    printIndentedGroup(groups.slice(shouldMerge ? 2 : 1), hardline)
   ]);
 
   // If any group but the last one has a hard line, we want to force expand


Test patch:
diff --git a/tests/comments/__snapshots__/jsfmt.spec.js.snap b/tests/comments/__snapshots__/jsfmt.spec.js.snap
index 063b75f2d..b6c036234 100644
--- a/tests/comments/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/comments/__snapshots__/jsfmt.spec.js.snap
@@ -74,6 +74,32 @@ declare class Foo extends Qux<string> {/* dangling */}
 "
 `;
 
+exports[`test dangling_array.js 1`] = `
+"expect(() => {}).toTriggerReadyStateChanges([
+  // Nothing.
+]);
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+expect(() => {}).toTriggerReadyStateChanges(
+  [
+    // Nothing.
+  ]
+);
+"
+`;
+
+exports[`test dangling_array.js 2`] = `
+"expect(() => {}).toTriggerReadyStateChanges([
+  // Nothing.
+]);
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+expect(() => {}).toTriggerReadyStateChanges(
+  [
+    // Nothing.
+  ]
+);
+"
+`;
+
 exports[`test first-line.js 1`] = `
 "a // comment
 b
diff --git a/tests/comments/dangling_array.js b/tests/comments/dangling_array.js
new file mode 100644
index 000000000..599897d57
--- /dev/null
+++ b/tests/comments/dangling_array.js
@@ -0,0 +1,3 @@
+expect(() => {}).toTriggerReadyStateChanges([
+  // Nothing.
+]);
diff --git a/tests/if_comments/__snapshots__/jsfmt.spec.js.snap b/tests/if_comments/__snapshots__/jsfmt.spec.js.snap
new file mode 100644
index 000000000..569510378
--- /dev/null
+++ b/tests/if_comments/__snapshots__/jsfmt.spec.js.snap
@@ -0,0 +1,73 @@
+exports[`test if_comments.js 1`] = `
+"async function f() {
+  if (untrackedChoice === 0) /* Cancel */ {
+    return null;
+  } else if (untrackedChoice === 1) /* Add */ {
+    await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) /* Allow Untracked */ {
+    allowUntracked = true;
+  }
+}
+
+async function f() {
+  if (untrackedChoice === 0)
+    /* Cancel */ {
+      return null;
+    }
+  else if (untrackedChoice === 1)
+    /* Add */ {
+      await repository.addAll(Array.from(untrackedChanges.keys()));
+      shouldAmend = true;
+    }
+  else if (untrackedChoice === 2)
+    /* Allow Untracked */ {
+      allowUntracked = true;
+    }
+}
+
+async function f() {
+  if (untrackedChoice === 0) {
+    /* Cancel */ return null;
+  } else if (untrackedChoice === 1) {
+    /* Add */ await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) {
+    /* Allow Untracked */ allowUntracked = true;
+  }
+}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+async function f() {
+  if (untrackedChoice === 0) {
+    /* Cancel */ return null;
+  } else if (untrackedChoice === 1) {
+    /* Add */ await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) {
+    /* Allow Untracked */ allowUntracked = true;
+  }
+}
+
+async function f() {
+  if (untrackedChoice === 0) {
+    /* Cancel */ return null;
+  } else if (untrackedChoice === 1) {
+    /* Add */ await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) {
+    /* Allow Untracked */ allowUntracked = true;
+  }
+}
+
+async function f() {
+  if (untrackedChoice === 0) {
+    /* Cancel */ return null;
+  } else if (untrackedChoice === 1) {
+    /* Add */ await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) {
+    /* Allow Untracked */ allowUntracked = true;
+  }
+}
+"
+`;
diff --git a/tests/if_comments/if_comments.js b/tests/if_comments/if_comments.js
new file mode 100644
index 000000000..2b3552999
--- /dev/null
+++ b/tests/if_comments/if_comments.js
@@ -0,0 +1,37 @@
+async function f() {
+  if (untrackedChoice === 0) /* Cancel */ {
+    return null;
+  } else if (untrackedChoice === 1) /* Add */ {
+    await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) /* Allow Untracked */ {
+    allowUntracked = true;
+  }
+}
+
+async function f() {
+  if (untrackedChoice === 0)
+    /* Cancel */ {
+      return null;
+    }
+  else if (untrackedChoice === 1)
+    /* Add */ {
+      await repository.addAll(Array.from(untrackedChanges.keys()));
+      shouldAmend = true;
+    }
+  else if (untrackedChoice === 2)
+    /* Allow Untracked */ {
+      allowUntracked = true;
+    }
+}
+
+async function f() {
+  if (untrackedChoice === 0) {
+    /* Cancel */ return null;
+  } else if (untrackedChoice === 1) {
+    /* Add */ await repository.addAll(Array.from(untrackedChanges.keys()));
+    shouldAmend = true;
+  } else if (untrackedChoice === 2) {
+    /* Allow Untracked */ allowUntracked = true;
+  }
+}
diff --git a/tests/if_comments/jsfmt.spec.js b/tests/if_comments/jsfmt.spec.js
new file mode 100644
index 000000000..989047bcc
--- /dev/null
+++ b/tests/if_comments/jsfmt.spec.js
@@ -0,0 +1,1 @@
+run_spec(__dirname);
diff --git a/tests/method-chain/__snapshots__/jsfmt.spec.js.snap b/tests/method-chain/__snapshots__/jsfmt.spec.js.snap
index 86427b531..daa999035 100644
--- a/tests/method-chain/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/method-chain/__snapshots__/jsfmt.spec.js.snap
@@ -115,6 +115,41 @@ it(\"should group messages with same created time\", () => {
 "
 `;
 
+exports[`test first_long.js 1`] = `
+"export default function theFunction(action$, store) {
+  return action$.ofType(THE_ACTION).switchMap(action => Observable
+    .webSocket({
+      url: THE_URL,
+      more: stuff(),
+      evenMore: stuff({
+        value1: true,
+        value2: false,
+        value3: false
+      })
+    })
+    .filter(data => theFilter(data))
+    .map(({ theType, ...data }) => theMap(theType, data))
+    .retryWhen(errors => errors));
+}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+export default function theFunction(action$, store) {
+  return action$.ofType(THE_ACTION).switchMap(action => Observable
+    .webSocket({
+      url: THE_URL,
+      more: stuff(),
+      evenMore: stuff({
+        value1: true,
+        value2: false,
+        value3: false
+      })
+    })
+    .filter(data => theFilter(data))
+    .map(({ theType, ...data }) => theMap(theType, data))
+    .retryWhen(errors => errors));
+}
+"
+`;
+
 exports[`test multiple-members.js 1`] = `
 "if (testConfig.ENABLE_ONLINE_TESTS === \"true\") {
   describe(\"POST /users/me/pet\", function() {
diff --git a/tests/method-chain/first_long.js b/tests/method-chain/first_long.js
new file mode 100644
index 000000000..ced9a2035
--- /dev/null
+++ b/tests/method-chain/first_long.js
@@ -0,0 +1,15 @@
+export default function theFunction(action$, store) {
+  return action$.ofType(THE_ACTION).switchMap(action => Observable
+    .webSocket({
+      url: THE_URL,
+      more: stuff(),
+      evenMore: stuff({
+        value1: true,
+        value2: false,
+        value3: false
+      })
+    })
+    .filter(data => theFilter(data))
+    .map(({ theType, ...data }) => theMap(theType, data))
+    .retryWhen(errors => errors));
+}
diff --git a/tests/object-prop-break-in/__snapshots__/jsfmt.spec.js.snap b/tests/object-prop-break-in/__snapshots__/jsfmt.spec.js.snap
index 1bc4417f6..cb925d852 100644
--- a/tests/object-prop-break-in/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/object-prop-break-in/__snapshots__/jsfmt.spec.js.snap
@@ -1,3 +1,20 @@
+exports[`test comment.js 1`] = `
+"function foo() {
+  return {
+    // this comment causes the problem
+    bar: baz() + 1
+  };
+}
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+function foo() {
+  return {
+    // this comment causes the problem
+    bar: baz() + 1
+  };
+}
+"
+`;
+
 exports[`test test.js 1`] = `
 "const a = classnames({
   \"some-prop\": this.state.longLongLongLongLongLongLongLongLongTooLongProp
diff --git a/tests/object-prop-break-in/comment.js b/tests/object-prop-break-in/comment.js
new file mode 100644
index 000000000..470a85819
--- /dev/null
+++ b/tests/object-prop-break-in/comment.js
@@ -0,0 +1,6 @@
+function foo() {
+  return {
+    // this comment causes the problem
+    bar: baz() + 1
+  };
+}