diff --git a/src/doc-builders.js b/src/doc-builders.js
index 0f1526be8175..820ccf49ea0b 100644
--- a/src/doc-builders.js
+++ b/src/doc-builders.js
@@ -57,6 +57,12 @@ function conditionalGroup(states, opts) {
);
}
+function fill(parts) {
+ parts.forEach(assertDoc);
+
+ return { type: "fill", parts };
+}
+
function ifBreak(breakContents, flatContents) {
if (breakContents) {
assertDoc(breakContents);
@@ -106,6 +112,7 @@ module.exports = {
literalline,
group,
conditionalGroup,
+ fill,
lineSuffix,
lineSuffixBoundary,
breakParent,
diff --git a/src/doc-debug.js b/src/doc-debug.js
index df3dd2794d8a..e0732d2a54d1 100644
--- a/src/doc-debug.js
+++ b/src/doc-debug.js
@@ -101,6 +101,13 @@ function printDoc(doc) {
);
}
+ if (doc.type === "fill") {
+ return ("fill") +
+ "(" +
+ doc.parts.map(printDoc).join(", ") +
+ ")";
+ }
+
if (doc.type === "line-suffix") {
return "lineSuffix(" + printDoc(doc.contents) + ")";
}
diff --git a/src/doc-printer.js b/src/doc-printer.js
index 7e5acad251c7..7550bf5268d3 100644
--- a/src/doc-printer.js
+++ b/src/doc-printer.js
@@ -1,5 +1,12 @@
"use strict";
+const docBuilders = require("./doc-builders");
+const concat = docBuilders.concat;
+const line = docBuilders.line;
+const softline = docBuilders.softline;
+const fill = docBuilders.fill;
+const ifBreak = docBuilders.ifBreak;
+
const MODE_BREAK = 1;
const MODE_FLAT = 2;
@@ -30,7 +37,7 @@ function makeAlign(ind, n) {
};
}
-function fits(next, restCommands, width) {
+function fits(next, restCommands, width, mustBeFlat) {
let restIdx = restCommands.length;
const cmds = [next];
while (width >= 0) {
@@ -70,8 +77,17 @@ function fits(next, restCommands, width) {
break;
case "group":
+ if (mustBeFlat && doc.break) {
+ return false;
+ }
cmds.push([ind, doc.break ? MODE_BREAK : mode, doc.contents]);
+ break;
+ case "fill":
+ for (var i = doc.parts.length - 1; i >= 0; i--) {
+ cmds.push([ind, mode, doc.parts[i]]);
+ }
+
break;
case "if-break":
if (mode === MODE_BREAK) {
@@ -210,6 +226,76 @@ function printDocToString(doc, options) {
break;
}
break;
+
+ // Fills each line with as much code as possible before moving to a new
+ // line with the same indentation.
+ //
+ // Expects doc.parts to be an array of alternating code and
+ // whitespace. The whitespace contains the linebreaks.
+ //
+ // For example:
+ // ["I", line, "love", line, "monkeys"]
+ // or
+ // [{ type: group, ... }, softline, { type: group, ... }]
+ //
+ // It uses this parts structure to handle three main layout cases:
+ // * The first two non-whitespace items fit on the same line without
+ // breaking -> output both items and the whitespace "flat".
+ // * Only the first item fits on the line without breaking -> output the
+ // first item "flat" and the whitespace with "break".
+ // * Neither item fits on the line without breaking -> output the first
+ // item and the whitespace with "break".
+ case "fill":
+ let rem = width - pos;
+
+ const parts = doc.parts;
+ if (parts.length === 0) {
+ break;
+ }
+
+ const first = parts[0];
+ const firstCmd = [ind, MODE_FLAT, first];
+ if (parts.length === 1) {
+ if (fits(firstCmd, cmds, width - rem, true)) {
+ cmds.push(firstCmd);
+ } else {
+ cmds.push([ind, mode, first]);
+ }
+ break;
+ }
+
+ const split = parts[1];
+ if (parts.length === 2) {
+ if (fits(firstCmd, cmds, width - rem, true)) {
+ cmds.push([ind, MODE_FLAT, split]);
+ cmds.push(firstCmd);
+ } else {
+ cmds.push([ind, mode, split]);
+ cmds.push([ind, mode, first]);
+ }
+ break;
+ }
+
+ const second = parts[2];
+ const remaining = parts.slice(2);
+ const remainingCmd = [ind, MODE_BREAK, fill(remaining)];
+
+ const firstAndSecondCmd = [ind, MODE_FLAT, concat([first, split, second])];
+
+ if (fits(firstAndSecondCmd, cmds, rem, true)) {
+ cmds.push(remainingCmd)
+ cmds.push([ind, MODE_FLAT, split]);
+ cmds.push(firstCmd);
+ } else if (fits(firstCmd, cmds, width - rem, true)) {
+ cmds.push(remainingCmd)
+ cmds.push([ind, MODE_BREAK, split]);
+ cmds.push(firstCmd);
+ } else {
+ cmds.push(remainingCmd);
+ cmds.push([ind, MODE_BREAK, split]);
+ cmds.push([ind, MODE_BREAK, first]);
+ }
+ break;
case "if-break":
if (mode === MODE_BREAK) {
if (doc.breakContents) {
diff --git a/src/doc-utils.js b/src/doc-utils.js
index 94b88ce9778f..656db6fab0ca 100644
--- a/src/doc-utils.js
+++ b/src/doc-utils.js
@@ -10,7 +10,7 @@ function traverseDoc(doc, onEnter, onExit, shouldTraverseConditionalGroups) {
}
if (shouldRecurse) {
- if (doc.type === "concat") {
+ if (doc.type === "concat" || doc.type === "fill") {
for (var i = 0; i < doc.parts.length; i++) {
traverseDocRec(doc.parts[i]);
}
@@ -43,7 +43,7 @@ function traverseDoc(doc, onEnter, onExit, shouldTraverseConditionalGroups) {
function mapDoc(doc, func) {
doc = func(doc);
- if (doc.type === "concat") {
+ if (doc.type === "concat" || doc.type === "fill") {
return Object.assign({}, doc, {
parts: doc.parts.map(d => mapDoc(d, func))
});
diff --git a/src/printer.js b/src/printer.js
index ba5dfa5b9ade..64ee25bb2d69 100644
--- a/src/printer.js
+++ b/src/printer.js
@@ -17,6 +17,7 @@ var group = docBuilders.group;
var indent = docBuilders.indent;
var align = docBuilders.align;
var conditionalGroup = docBuilders.conditionalGroup;
+var fill = docBuilders.fill;
var ifBreak = docBuilders.ifBreak;
var breakParent = docBuilders.breakParent;
var lineSuffixBoundary = docBuilders.lineSuffixBoundary;
@@ -2915,8 +2916,8 @@ function printJSXChildren(path, options, print, jsxWhitespace) {
if (/\S/.test(value)) {
// treat each line of text as its own entity
- value.split(/(\r?\n\s*)/).forEach(line => {
- const newlines = line.match(/\n/g);
+ value.split(/(\r?\n\s*)/).forEach(textLine => {
+ const newlines = textLine.match(/\n/g);
if (newlines) {
children.push(hardline);
@@ -2927,27 +2928,30 @@ function printJSXChildren(path, options, print, jsxWhitespace) {
return;
}
- const beginSpace = /^\s+/.test(line);
+ const beginSpace = /^\s+/.test(textLine);
if (beginSpace) {
children.push(jsxWhitespace);
- children.push(softline);
}
- const stripped = line.replace(/^\s+|\s+$/g, "");
+ const stripped = textLine.replace(/^\s+|\s+$/g, "");
if (stripped) {
- children.push(stripped);
+
+ // Split text into words separated by "line"s.
+ stripped.split(/(\s+)/).forEach(word => {
+ const space = /\s+/.test(word);
+ if (space) {
+ children.push(line);
+ } else {
+ children.push(word);
+ }
+ });
}
- const endSpace = /\s+$/.test(line);
+ const endSpace = /\s+$/.test(textLine);
if (endSpace) {
- children.push(softline);
children.push(jsxWhitespace);
}
});
-
- if (!isLineNext(util.getLast(children))) {
- children.push(softline);
- }
} else if (/\n/.test(value)) {
children.push(hardline);
@@ -2959,14 +2963,15 @@ function printJSXChildren(path, options, print, jsxWhitespace) {
// whitespace-only without newlines,
// eg; a single space separating two elements
children.push(jsxWhitespace);
- children.push(softline);
}
} else {
children.push(print(childPath));
- // add a line unless it's followed by a JSX newline
+ // add a softline where we have two adjacent JSX elements without
+ // any text or whitespace between them.
let next = n.children[i + 1];
- if (!(next && /^\s*\n/.test(next.value))) {
+ const followedByJSXElement = next && next.type === 'JSXElement';
+ if (followedByJSXElement) {
children.push(softline);
}
}
@@ -3024,8 +3029,8 @@ function printJSXElement(path, options, print) {
let forcedBreak = willBreak(openingLines);
const jsxWhitespace = options.singleQuote
- ? ifBreak("{' '}", " ")
- : ifBreak('{" "}', " ");
+ ? ifBreak(concat(["{' '}", softline]), " ")
+ : ifBreak(concat(['{" "}', softline]), " ");
const children = printJSXChildren(path, options, print, jsxWhitespace);
// Trim trailing lines, recording if there was a hardline
@@ -3096,7 +3101,7 @@ function printJSXElement(path, options, print) {
groups.map(
contents =>
(Array.isArray(contents)
- ? conditionalGroup([concat(contents)])
+ ? conditionalGroup([fill(contents)])
: contents)
)
)
@@ -3116,7 +3121,7 @@ function printJSXElement(path, options, print) {
}
return conditionalGroup([
- group(concat([openingLines, concat(children), closingLines])),
+ group(concat([openingLines, fill(children), closingLines])),
multiLineElem
]);
}
diff --git a/tests/flow/more_react/__snapshots__/jsfmt.spec.js.snap b/tests/flow/more_react/__snapshots__/jsfmt.spec.js.snap
index 6afb885f1877..dc7116992e32 100644
--- a/tests/flow/more_react/__snapshots__/jsfmt.spec.js.snap
+++ b/tests/flow/more_react/__snapshots__/jsfmt.spec.js.snap
@@ -164,7 +164,8 @@ var App = require("App.react");
var app = (