diff --git a/src/doc-builders.js b/src/doc-builders.js index c54e41beeb80..837e0d6d3a9e 100644 --- a/src/doc-builders.js +++ b/src/doc-builders.js @@ -54,6 +54,12 @@ function conditionalGroup(states, opts) { ); } +function fill(parts) { + parts.forEach(assertDoc); + + return { type: "fill", parts }; +} + function ifBreak(breakContents, flatContents) { if (breakContents) { assertDoc(breakContents); @@ -103,6 +109,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 d78101d42a54..d248d5fa246e 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; @@ -40,7 +47,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) { @@ -80,8 +87,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) { @@ -220,6 +236,86 @@ 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 content 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 content items fit on the same line without + // breaking + // -> output the first content item and the whitespace "flat". + // * Only the first content item fits on the line without breaking + // -> output the first content item "flat" and the whitespace with + // "break". + // * Neither content item fits on the line without breaking + // -> output the first content item and the whitespace with "break". + case "fill": { + let rem = width - pos; + + const parts = doc.parts; + if (parts.length === 0) { + break; + } + + const content = parts[0]; + const contentFlatCmd = [ind, MODE_FLAT, content]; + const contentBreakCmd = [ind, MODE_BREAK, content]; + const contentFits = fits(contentFlatCmd, [], width - rem, true) + + if (parts.length === 1) { + if (contentFits) { + cmds.push(contentFlatCmd); + } else { + cmds.push(contentBreakCmd); + } + break; + } + + const whitespace = parts[1]; + const whitespaceFlatCmd = [ind, MODE_FLAT, whitespace] + const whitespaceBreakCmd = [ind, MODE_BREAK, whitespace] + + if (parts.length === 2) { + if (contentFits) { + cmds.push(whitespaceFlatCmd); + cmds.push(contentFlatCmd); + } else { + cmds.push(whitespaceBreakCmd); + cmds.push(contentBreakCmd); + } + break; + } + + const remaining = parts.slice(2); + const remainingCmd = [ind, mode, fill(remaining)]; + + const secondContent = parts[2]; + const firstAndSecondContentFlatCmd = [ind, MODE_FLAT, concat([content, whitespace, secondContent])]; + const firstAndSecondContentFits = fits(firstAndSecondContentFlatCmd, [], rem, true); + + if (firstAndSecondContentFits) { + cmds.push(remainingCmd) + cmds.push(whitespaceFlatCmd); + cmds.push(contentFlatCmd); + } else if (contentFits) { + cmds.push(remainingCmd) + cmds.push(whitespaceBreakCmd); + cmds.push(contentFlatCmd); + } else { + cmds.push(remainingCmd); + cmds.push(whitespaceBreakCmd); + cmds.push(contentBreakCmd); + } + break; + } case "if-break": if (mode === MODE_BREAK) { if (doc.breakContents) { @@ -288,7 +384,7 @@ function printDocToString(doc, options) { out[out.length - 1] = out[out.length - 1].replace( /[^\S\n]*$/, "" - ); + ); } } 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 522282643cd0..96c72579c63b 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; @@ -3436,8 +3437,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); @@ -3448,27 +3449,38 @@ function printJSXChildren(path, options, print, jsxWhitespace) { return; } - const beginSpace = /^\s+/.test(line); + if (textLine.length === 0) { + return; + } + + const beginSpace = /^\s+/.test(textLine); if (beginSpace) { children.push(jsxWhitespace); + } else { 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); + } else { + children.push(softline); } }); - - if (!isLineNext(util.getLast(children))) { - children.push(softline); - } } else if (/\n/.test(value)) { children.push(hardline); @@ -3481,15 +3493,22 @@ function printJSXChildren(path, options, print, jsxWhitespace) { // eg; one or more spaces separating two elements for (let i = 0; i < value.length; ++i) { children.push(jsxWhitespace); + // Because fill expects alternating content and whitespace parts + // we need to include an empty content part between each JSX + // whitespace. + if (i + 1 < value.length) { + children.push(''); + } } - 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 elements that are not + // literals let next = n.children[i + 1]; - if (!(next && /^\s*\n/.test(next.value))) { + const followedByJSXElement = next && !namedTypes.Literal.check(next); + if (followedByJSXElement) { children.push(softline); } } @@ -3546,9 +3565,9 @@ function printJSXElement(path, options, print) { // Record any breaks. Should never go from true to false, only false to true. let forcedBreak = willBreak(openingLines); - const jsxWhitespace = options.singleQuote - ? ifBreak("{' '}", " ") - : ifBreak('{" "}', " "); + const rawJsxWhitespace = options.singleQuote ? "{' '}" : '{" "}'; + const jsxWhitespace = ifBreak(concat([softline, rawJsxWhitespace, softline]), " ") + const children = printJSXChildren(path, options, print, jsxWhitespace); // Trim trailing lines, recording if there was a hardline @@ -3579,56 +3598,37 @@ function printJSXElement(path, options, print) { children.unshift(hardline); } - // Group by line, recording if there was a hardline. - let groups = [[]]; // Initialize the first line's group + // Tweak how we format children if outputting this element over multiple lines. + // Also detect whether we will force this element to output over multiple lines. + let multilineChildren = []; children.forEach((child, i) => { - // leading and trailing JSX whitespace don't go into a group + + // Ensure that we display leading, trailing, and solitary whitespace as + // `{" "}` when outputting this element over multiple lines. if (child === jsxWhitespace) { - if (i === 0) { - groups.unshift(child); + if (children.length === 1) { + multilineChildren.push(rawJsxWhitespace); + return; + } else if (i === 0) { + multilineChildren.push(concat([rawJsxWhitespace, hardline])); return; } else if (i === children.length - 1) { - groups.push(child); + multilineChildren.push(concat([hardline, rawJsxWhitespace])); return; } } - let prev = children[i - 1]; - if (prev && willBreak(prev)) { - forcedBreak = true; + multilineChildren.push(child); - // On a new line, so create a new group and put this element in it. - groups.push([child]); - } else { - // Not on a newline, so add this element to the current group. - util.getLast(groups).push(child); - } - - // Ensure we record hardline of last element. - if (!forcedBreak && i === children.length - 1) { - if (willBreak(child)) forcedBreak = true; + if (willBreak(child)) { + forcedBreak = true; } }); - const childrenGroupedByLine = [ - hardline, - // Conditional groups suppress break propagation; we want to output - // hard lines without breaking up the entire jsx element. - // Note that leading and trailing JSX Whitespace don't go into a group. - concat( - groups.map( - contents => - (Array.isArray(contents) - ? conditionalGroup([concat(contents)]) - : contents) - ) - ) - ]; - const multiLineElem = group( concat([ openingLines, - indent(group(concat(childrenGroupedByLine), { shouldBreak: true })), + indent(concat([hardline, fill(multilineChildren)])), hardline, closingLines ]) @@ -3639,7 +3639,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 = ( - {" "}// error, y: number but foo expects string in App.react + {" "} + // error, y: number but foo expects string in App.react Some text. ); diff --git a/tests/jsx-significant-space/__snapshots__/jsfmt.spec.js.snap b/tests/jsx-significant-space/__snapshots__/jsfmt.spec.js.snap index e1bb3b167212..b207e51f1f15 100644 --- a/tests/jsx-significant-space/__snapshots__/jsfmt.spec.js.snap +++ b/tests/jsx-significant-space/__snapshots__/jsfmt.spec.js.snap @@ -146,13 +146,15 @@ nest_plz = ( regression_not_transformed_1 = ( - {" "} + {" "} + ); regression_not_transformed_2 = ( - {" "} + + {" "} ); @@ -178,22 +180,15 @@ similar_3 = ( not_broken_end = (
- long text long text long text long text long text long text long text long text - {" "} - url - {" "} - long text long text + long text long text long text long text long text long text long text long + text url long text long text
); not_broken_begin = (
-
- {" "} - long text long text long text long text long text long text long text long text - url - {" "} - long text long text +
long text long text long text long text long text long text long text + long texturl long text long text
); diff --git a/tests/jsx-text-wrap/__snapshots__/jsfmt.spec.js.snap b/tests/jsx-text-wrap/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000000..b1aaaa14798e --- /dev/null +++ b/tests/jsx-text-wrap/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,244 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`test.js 1`] = ` +// Wrapping text +x = +
+ Some text that would need to wrap on to a new line in order to display correctly and nicely +
+ +// Wrapping tags +x = +
+ f f f f f f +
+ +// Wrapping tags +x = +
+ ffffff +
+ +// Wrapping tags +x = +
+ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f +
+ +// Wrapping tags +x = +
+ f +
+ +x = +
+ before
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at mollis lorem.
after +
+ +x = +
+ before{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}{stuff}{stuff}after{stuff}after +
+ +x = +
+ before {stuff} after {stuff} after {stuff} after {stuff} after {stuff} after {stuff} {stuff} {stuff} after {stuff} after +
+ +x = +
+ Please state your name and occupation for the board of school directors. +
+ +function DiffOverview(props) { + const { source, target, since } = props; + return ( +
+
+

+ This diff overview is computed against the current list of records in + this collection and the list it contained on {humanDate(since)}. +

+

+ Note: last_modified and schema record metadata + are omitted for easier review. +

+
+ +
+ ); +} + +x = Starting at minute {graphActivity.startTime}, running for {graphActivity.length} to minute {graphActivity.startTime + graphActivity.length} + +x = +
+ First second third +
Something
+
+ +x = +
+
+ First +
+ Second +
+ Third +
+
+ +x = +
+ First
+ Second +
Third +
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Wrapping text +x = ( +
+ Some text that would need to wrap on to a new line in order to display + correctly and nicely +
+); + +// Wrapping tags +x = ( +
+ f f f f + {" "} + f f +
+); + +// Wrapping tags +x = ( +
+ ffff + ff +
+); + +// Wrapping tags +x = ( +
+ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + {" "} + f +
+); + +// Wrapping tags +x = ( +
+ + {" "} + f +
+); + +x = ( +
+ before +
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at + mollis lorem. +
+ after +
+); + +x = ( +
+ before{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff} + {stuff}{stuff}after{stuff}after +
+); + +x = ( +
+ before {stuff} after {stuff} after {stuff} after {stuff} after {stuff} after + {" "} + {stuff} {stuff} {stuff} after {stuff} after +
+); + +x = ( +
+ Please state your name and occupation for the board of + {" "} + school directors. +
+); + +function DiffOverview(props) { + const { source, target, since } = props; + return ( +
+
+

+ This diff overview is computed against the current list of records in + this collection and the list it contained on {humanDate(since)} + . +

+

+ Note: last_modified and schema record + metadata + are omitted for easier review. +

+
+ +
+ ); +} + +x = ( + + + Starting at minute {graphActivity.startTime}, running for + {" "} + {graphActivity.length} to minute + {" "} + {graphActivity.startTime + graphActivity.length} + + +); + +x = ( +
+ First second third +
+ Something +
+
+); + +x = ( +
+
+ First +
+ Second +
+ Third +
+
+); + +x = ( +
+ First + {" "} +
+ Second +
+ {" "} + Third +
+); + +`; diff --git a/tests/jsx-text-wrap/jsfmt.spec.js b/tests/jsx-text-wrap/jsfmt.spec.js new file mode 100644 index 000000000000..7580dfab0b75 --- /dev/null +++ b/tests/jsx-text-wrap/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, null, ["typescript"]); diff --git a/tests/jsx-text-wrap/test.js b/tests/jsx-text-wrap/test.js new file mode 100644 index 000000000000..5a526643d48c --- /dev/null +++ b/tests/jsx-text-wrap/test.js @@ -0,0 +1,95 @@ +// Wrapping text +x = +
+ Some text that would need to wrap on to a new line in order to display correctly and nicely +
+ +// Wrapping tags +x = +
+ f f f f f f +
+ +// Wrapping tags +x = +
+ ffffff +
+ +// Wrapping tags +x = +
+ + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa f +
+ +// Wrapping tags +x = +
+ f +
+ +x = +
+ before
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Curabitur at mollis lorem.
after +
+ +x = +
+ before{stuff}after{stuff}after{stuff}after{stuff}after{stuff}after{stuff}{stuff}{stuff}after{stuff}after +
+ +x = +
+ before {stuff} after {stuff} after {stuff} after {stuff} after {stuff} after {stuff} {stuff} {stuff} after {stuff} after +
+ +x = +
+ Please state your name and occupation for the board of school directors. +
+ +function DiffOverview(props) { + const { source, target, since } = props; + return ( +
+
+

+ This diff overview is computed against the current list of records in + this collection and the list it contained on {humanDate(since)}. +

+

+ Note: last_modified and schema record metadata + are omitted for easier review. +

+
+ +
+ ); +} + +x = Starting at minute {graphActivity.startTime}, running for {graphActivity.length} to minute {graphActivity.startTime + graphActivity.length} + +x = +
+ First second third +
Something
+
+ +x = +
+
+ First +
+ Second +
+ Third +
+
+ +x = +
+ First
+ Second +
Third +
diff --git a/tests/jsx_escape/__snapshots__/jsfmt.spec.js.snap b/tests/jsx_escape/__snapshots__/jsfmt.spec.js.snap index 36657874bf0d..368e10f1ed26 100644 --- a/tests/jsx_escape/__snapshots__/jsfmt.spec.js.snap +++ b/tests/jsx_escape/__snapshots__/jsfmt.spec.js.snap @@ -18,6 +18,7 @@ exports[`nbsp.js 1`] = ` many_nbsp =
   
single_nbsp =
 
many_raw_nbsp =
   
+many_raw_spaces =
amp = foo & bar @@ -26,6 +27,7 @@ raw_amp = foo & bar many_nbsp =
   
; single_nbsp =
 
; many_raw_nbsp =
; +many_raw_spaces =
; amp = foo & bar; raw_amp = foo & bar; @@ -36,6 +38,7 @@ exports[`nbsp.js 2`] = ` many_nbsp =
   
single_nbsp =
 
many_raw_nbsp =
   
+many_raw_spaces =
amp = foo & bar @@ -44,6 +47,7 @@ raw_amp = foo & bar many_nbsp =
   
; single_nbsp =
 
; many_raw_nbsp =
; +many_raw_spaces =
; amp = foo & bar; raw_amp = foo & bar; diff --git a/tests/jsx_escape/nbsp.js b/tests/jsx_escape/nbsp.js index 87f6989a1cb2..fe3afc604734 100644 --- a/tests/jsx_escape/nbsp.js +++ b/tests/jsx_escape/nbsp.js @@ -1,6 +1,7 @@ many_nbsp =
   
single_nbsp =
 
many_raw_nbsp =
   
+many_raw_spaces =
amp = foo & bar diff --git a/tests/typescript/conformance/expressions/functionCalls/__snapshots__/jsfmt.spec.js.snap b/tests/typescript/conformance/expressions/functionCalls/__snapshots__/jsfmt.spec.js.snap index 16502cb83212..a71ea878ac0e 100644 --- a/tests/typescript/conformance/expressions/functionCalls/__snapshots__/jsfmt.spec.js.snap +++ b/tests/typescript/conformance/expressions/functionCalls/__snapshots__/jsfmt.spec.js.snap @@ -56,7 +56,7 @@ class D extends C { // @target: ES6 interface X { - foo(x: number, y: number, ...z: string[]) + foo(x: number, y: number, ...z: string[]); } function foo(x: number, y: number, ...z: string[]) {}