diff --git a/README.md b/README.md index add81dc82..3c72a14ee 100644 --- a/README.md +++ b/README.md @@ -267,8 +267,8 @@ Prettier ships with a handful of customizable format options, usable in both the | **Trailing Commas** - Print trailing commas wherever possible.

Valid options:
- `"none"` - no trailing commas
- `"es5"` - trailing commas where valid in ES5 (objects, arrays, etc)
- `"all"` - trailing commas wherever possible (function arguments). This requires node 8 or a [transform](https://babeljs.io/docs/plugins/syntax-trailing-function-commas/). | `"none"` | --trailing-comma | trailingComma: "" | | **Bracket Spacing** - Print spaces between brackets in object literals.

Valid options:
- `true` - Example: `{ foo: bar }`
- `false` - Example: `{foo: bar}` | `true` | `--no-bracket-spacing` | `bracketSpacing: ` | | **JSX Brackets on Same Line** - Put the `>` of a multi-line JSX element at the end of the last line instead of being alone on the next line | `false` | `--jsx-bracket-same-line` | `jsxBracketSameLine: ` | -| **Range Start** - Format code starting at a given character offset. The range will extend backwards to the start of the line. | `0` | `--range-start ` | `rangeStart: ` | -| **Range End** - Format code ending at a given character offset (exclusive). The range will extend forwards to the end of the line. | `Infinity` | `--range-end ` | `rangeEnd: ` | +| **Range Start** - Format code starting at a given character offset. The range will extend backwards to the start of the first line containing the selected statement. | `0` | `--range-start ` | `rangeStart: ` | +| **Range End** - Format code ending at a given character offset (exclusive). The range will extend forwards to the end of the selected statement. | `Infinity` | `--range-end ` | `rangeEnd: ` | | **Parser** - Specify which parser to use. Both parsers support the same set of JavaScript features (including Flow). You shouldn't have to change this setting. | `babylon` | --parser | parser: "" | ### Excluding code from formatting diff --git a/bin/prettier.js b/bin/prettier.js index f9479c9f5..36ca4907c 100755 --- a/bin/prettier.js +++ b/bin/prettier.js @@ -232,8 +232,12 @@ if (argv["help"] || (!filepatterns.length && !stdin)) { " --trailing-comma \n" + " Print trailing commas wherever possible. Defaults to none.\n" + " --parser Specify which parse to use. Defaults to babylon.\n" + - " --range-start Format code starting at a given character offset. The range will extend backwards to the start of the line. Defaults to 0.\n" + - " --range-end Format code ending at a given character offset (exclusive). The range will extend forwards to the end of the line. Defaults to Infinity.\n" + + " --range-start Format code starting at a given character offset.\n" + + " The range will extend backwards to the start of the first line containing the selected statement.\n" + + " Defaults to 0.\n" + + " --range-end Format code ending at a given character offset (exclusive).\n" + + " The range will extend forwards to the end of the selected statement.\n" + + " Defaults to Infinity.\n" + " --no-color Do not colorize error messages.\n" + " --version or -v Print Prettier version.\n" + "\n" diff --git a/index.js b/index.js index fc851f5e9..a17da9591 100644 --- a/index.js +++ b/index.js @@ -3,7 +3,7 @@ const comments = require("./src/comments"); const version = require("./package.json").version; const printAstToDoc = require("./src/printer").printAstToDoc; -const getAlignmentSize = require("./src/util").getAlignmentSize; +const util = require("./src/util"); const printDocToString = require("./src/doc-printer").printDocToString; const normalizeOptions = require("./src/options").normalize; const parser = require("./src/parser"); @@ -52,41 +52,159 @@ function ensureAllCommentsPrinted(astComments) { function format(text, opts, addAlignmentSize) { addAlignmentSize = addAlignmentSize || 0; - const formattedRangeOnly = formatRange(text, opts); + const ast = parser.parse(text, opts); + + const formattedRangeOnly = formatRange(text, opts, ast); if (formattedRangeOnly) { return formattedRangeOnly; } - const ast = parser.parse(text, opts); const astComments = attachComments(text, ast, opts); const doc = printAstToDoc(ast, opts, addAlignmentSize); opts.newLine = guessLineEnding(text); const str = printDocToString(doc, opts); ensureAllCommentsPrinted(astComments); - // Remove extra leading newline as well as the added indentation after last newline + // Remove extra leading indentation as well as the added indentation after last newline if (addAlignmentSize > 0) { - return str.slice(opts.newLine.length).trimRight() + opts.newLine; + return str.trim() + opts.newLine; } return str; } -function formatRange(text, opts) { - // Use `Math.min` since `lastIndexOf` returns 0 when `rangeStart` is 0 - const rangeStart = Math.min( - opts.rangeStart, - text.lastIndexOf("\n", opts.rangeStart) + 1 - ); - // Use `text.length - 1` as the maximum since `indexOf` returns -1 if `fromIndex >= text.length` - const fromIndex = Math.min(opts.rangeEnd, text.length - 1); - const nextNewLineIndex = text.indexOf("\n", fromIndex); - const rangeEnd = (nextNewLineIndex < 0 ? fromIndex : nextNewLineIndex) + 1; // Add one to make rangeEnd exclusive +function findSiblingAncestors(startNodeAndParents, endNodeAndParents) { + let resultStartNode = startNodeAndParents.node; + let resultEndNode = endNodeAndParents.node; - if (0 < rangeStart || rangeEnd < text.length) { - const rangeString = text.substring(rangeStart, rangeEnd); - const alignmentSize = getAlignmentSize( - rangeString.slice(0, rangeString.search(/[^ \t]/)), - opts.tabWidth + for (const endParent of endNodeAndParents.parentNodes) { + if (util.locStart(endParent) >= util.locStart(startNodeAndParents.node)) { + resultEndNode = endParent; + } else { + break; + } + } + + for (const startParent of startNodeAndParents.parentNodes) { + if (util.locEnd(startParent) <= util.locEnd(endNodeAndParents.node)) { + resultStartNode = startParent; + } else { + break; + } + } + + return { + startNode: resultStartNode, + endNode: resultEndNode + }; +} + +function findNodeAtOffset(node, offset, parentNodes) { + parentNodes = parentNodes || []; + const start = util.locStart(node); + const end = util.locEnd(node); + if (start <= offset && offset <= end) { + for (const childNode of comments.getSortedChildNodes(node)) { + const childResult = findNodeAtOffset( + childNode, + offset, + [node].concat(parentNodes) + ); + if (childResult) { + return childResult; + } + } + + if (isSourceElement(node)) { + return { + node: node, + parentNodes: parentNodes + }; + } + } +} + +// See https://www.ecma-international.org/ecma-262/5.1/#sec-A.5 +function isSourceElement(node) { + if (node == null) { + return false; + } + switch (node.type) { + case "FunctionDeclaration": + case "BlockStatement": + case "BreakStatement": + case "ContinueStatement": + case "DebuggerStatement": + case "DoWhileStatement": + case "EmptyStatement": + case "ExpressionStatement": + case "ForInStatement": + case "ForStatement": + case "IfStatement": + case "LabeledStatement": + case "ReturnStatement": + case "SwitchStatement": + case "ThrowStatement": + case "TryStatement": + case "VariableDeclaration": + case "WhileStatement": + case "WithStatement": + return true; + } + return false; +} + +function calculateRange(text, opts, ast) { + // Contract the range so that it has non-whitespace characters at its endpoints. + // This ensures we can format a range that doesn't end on a node. + const rangeStringOrig = text.slice(opts.rangeStart, opts.rangeEnd); + const startNonWhitespace = Math.max( + opts.rangeStart + rangeStringOrig.search(/\S/), + opts.rangeStart + ); + let endNonWhitespace; + for ( + endNonWhitespace = opts.rangeEnd; + endNonWhitespace > opts.rangeStart; + --endNonWhitespace + ) { + if (text[endNonWhitespace - 1].match(/\S/)) { + break; + } + } + + const startNodeAndParents = findNodeAtOffset(ast, startNonWhitespace); + const endNodeAndParents = findNodeAtOffset(ast, endNonWhitespace); + const siblingAncestors = findSiblingAncestors( + startNodeAndParents, + endNodeAndParents + ); + const startNode = siblingAncestors.startNode; + const endNode = siblingAncestors.endNode; + const rangeStart = Math.min(util.locStart(startNode), util.locStart(endNode)); + const rangeEnd = Math.max(util.locEnd(startNode), util.locEnd(endNode)); + + return { + rangeStart: rangeStart, + rangeEnd: rangeEnd + }; +} + +function formatRange(text, opts, ast) { + if (0 < opts.rangeStart || opts.rangeEnd < text.length) { + const range = calculateRange(text, opts, ast); + const rangeStart = range.rangeStart; + const rangeEnd = range.rangeEnd; + const rangeString = text.slice(rangeStart, rangeEnd); + + // Try to extend the range backwards to the beginning of the line. + // This is so we can detect indentation correctly and restore it. + // Use `Math.min` since `lastIndexOf` returns 0 when `rangeStart` is 0 + const rangeStart2 = Math.min( + rangeStart, + text.lastIndexOf("\n", rangeStart) + 1 ); + const indentString = text.slice(rangeStart2, rangeStart); + + const alignmentSize = util.getAlignmentSize(indentString, opts.tabWidth); const rangeFormatted = format( rangeString, @@ -98,7 +216,11 @@ function formatRange(text, opts) { alignmentSize ); - return text.slice(0, rangeStart) + rangeFormatted + text.slice(rangeEnd); + // Since the range contracts to avoid trailing whitespace, + // we need to remove the newline that was inserted by the `format` call. + const rangeTrimmed = rangeFormatted.trimRight(); + + return text.slice(0, rangeStart) + rangeTrimmed + text.slice(rangeEnd); } } diff --git a/src/comments.js b/src/comments.js index 993bfe6d5..cf1a27d9a 100644 --- a/src/comments.js +++ b/src/comments.js @@ -939,4 +939,9 @@ function printComments(path, print, options, needsSemi) { return concat(leadingParts.concat(trailingParts)); } -module.exports = { attach, printComments, printDanglingComments }; +module.exports = { + attach, + printComments, + printDanglingComments, + getSortedChildNodes +}; diff --git a/tests/range/__snapshots__/jsfmt.spec.js.snap b/tests/range/__snapshots__/jsfmt.spec.js.snap index 3feb81f45..f52c4746e 100644 --- a/tests/range/__snapshots__/jsfmt.spec.js.snap +++ b/tests/range/__snapshots__/jsfmt.spec.js.snap @@ -1,12 +1,166 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`different-levels.js 1`] = ` +call(1,2,3) +call(1,2,3) +function f() { + call(1,2,3) +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +call(1,2,3) +call(1, 2, 3); +function f() { + call(1, 2, 3); +} + +`; + +exports[`function-declaration.js 1`] = ` +function ugly ( {a=1, b = 2 } ) {} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +function ugly({ a = 1, b = 2 }) {} + +`; + +exports[`ignore-indentation.js 1`] = ` +function ugly ( {a=1, b = 2 } ) { + function ugly ( {a=1, b = 2 } ) { + function ugly ( {a=1, b = 2 } ) { + \`multiline template string + with too much indentation\` + } + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +function ugly ( {a=1, b = 2 } ) { + function ugly ( {a=1, b = 2 } ) { + function ugly ( {a=1, b = 2 } ) { + \`multiline template string + with too much indentation\`; + } + } +} + +`; + +exports[`multiple-statements.js 1`] = ` +call( + 1, 2,3 +); + +call( + 1, 2,3 +); + +call( + 1, 2,3 +); + +call( + 1, 2,3 +); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +call( + 1, 2,3 +); + +call(1, 2, 3); + +call(1, 2, 3); + +call( + 1, 2,3 +); + +`; + +exports[`multiple-statements2.js 1`] = ` +call( + 1, 2,3 +); + +call( + 1, 2,3 +); + +call( + 1, 2,3 +); + +call( + 1, 2,3 +); +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +call( + 1, 2,3 +); + +call(1, 2, 3); + +call(1, 2, 3); + +call( + 1, 2,3 +); + +`; + +exports[`nested.js 1`] = ` +try { + if (condition) { + body + } +} +catch (err) {} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +try { + if (condition) { + body; + } +} catch (err) {} + +`; + +exports[`nested2.js 1`] = ` +try { + if (condition) { + body + } +} +catch (err) {} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +try { + if (condition) { + body; + } +} +catch (err) {} + +`; + +exports[`nested3.js 1`] = ` +try { +1;if (condition) { + body + } +} +catch (err) {} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +try { +1;if (condition) { + body; + } +} +catch (err) {} + +`; + exports[`range.js 1`] = ` function ugly ( {a=1, b = 2 } ) { function ugly ( {a=1, b = 2 } ) { function ugly ( {a=1, b = 2 } ) { \`multiline template string with too much indentation\` - // The [165, 246) range selects the above two lines, including the second newline } } } @@ -16,9 +170,17 @@ function ugly ( {a=1, b = 2 } ) { function ugly ( {a=1, b = 2 } ) { \`multiline template string with too much indentation\`; - // The [165, 246) range selects the above two lines, including the second newline } } } `; + +exports[`try-catch.js 1`] = ` +try {} +catch (err) {} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +try { +} catch (err) {} + +`; diff --git a/tests/range/different-levels.js b/tests/range/different-levels.js new file mode 100644 index 000000000..614a3feee --- /dev/null +++ b/tests/range/different-levels.js @@ -0,0 +1,5 @@ +call(1,2,3) +call(1,<<>>2,3) +function f() { + call(1,<<>>2,3) +} diff --git a/tests/range/function-declaration.js b/tests/range/function-declaration.js new file mode 100644 index 000000000..fff8ab9bd --- /dev/null +++ b/tests/range/function-declaration.js @@ -0,0 +1 @@ +function ugly ( <<>>{a=1, b = 2 }<<>> ) {} diff --git a/tests/range/ignore-indentation.js b/tests/range/ignore-indentation.js new file mode 100644 index 000000000..7a4a8f41d --- /dev/null +++ b/tests/range/ignore-indentation.js @@ -0,0 +1,8 @@ +function ugly ( {a=1, b = 2 } ) { + function ugly ( {a=1, b = 2 } ) { + function ugly ( {a=1, b = 2 } ) { + `multiline template string + with <<>>too<<>> much indentation` + } + } +} diff --git a/tests/range/jsfmt.spec.js b/tests/range/jsfmt.spec.js index 2d80d1068..989047bcc 100644 --- a/tests/range/jsfmt.spec.js +++ b/tests/range/jsfmt.spec.js @@ -1 +1 @@ -run_spec(__dirname, { rangeStart: 180, rangeEnd: 220 }); +run_spec(__dirname); diff --git a/tests/range/multiple-statements.js b/tests/range/multiple-statements.js new file mode 100644 index 000000000..f0bf75567 --- /dev/null +++ b/tests/range/multiple-statements.js @@ -0,0 +1,15 @@ +call( + 1, 2,3 +); + +call( + 1, <<>>2,3 +); + +call( + 1, <<>>2,3 +); + +call( + 1, 2,3 +); diff --git a/tests/range/multiple-statements2.js b/tests/range/multiple-statements2.js new file mode 100644 index 000000000..e0ef5e2ba --- /dev/null +++ b/tests/range/multiple-statements2.js @@ -0,0 +1,15 @@ +call( + 1, 2,3 +); +<<>> +call( + 1, 2,3 +); + +call( + 1, 2,3 +); +<<>> +call( + 1, 2,3 +); diff --git a/tests/range/nested.js b/tests/range/nested.js new file mode 100644 index 000000000..79f1d387f --- /dev/null +++ b/tests/range/nested.js @@ -0,0 +1,6 @@ +<<>>try { + if (condition) { + body + }<<>> +} +catch (err) {} diff --git a/tests/range/nested2.js b/tests/range/nested2.js new file mode 100644 index 000000000..04289c876 --- /dev/null +++ b/tests/range/nested2.js @@ -0,0 +1,6 @@ +try { + i<<>>f (condition) { + body + }<<>> +} +catch (err) {} diff --git a/tests/range/nested3.js b/tests/range/nested3.js new file mode 100644 index 000000000..ac77cf4a3 --- /dev/null +++ b/tests/range/nested3.js @@ -0,0 +1,6 @@ +try { +1;i<<>>f (condition) { + body + }<<>> +} +catch (err) {} diff --git a/tests/range/range.js b/tests/range/range.js index ed1eb1bc6..3ca1fbfc5 100644 --- a/tests/range/range.js +++ b/tests/range/range.js @@ -2,8 +2,7 @@ function ugly ( {a=1, b = 2 } ) { function ugly ( {a=1, b = 2 } ) { function ugly ( {a=1, b = 2 } ) { `multiline template string - with too much indentation` - // The [165, 246) range selects the above two lines, including the second newline + with <<>>too<<>> much indentation` } } } diff --git a/tests/range/try-catch.js b/tests/range/try-catch.js new file mode 100644 index 000000000..b1c8311c4 --- /dev/null +++ b/tests/range/try-catch.js @@ -0,0 +1,2 @@ +try {} +<<>>c<<>>atch (err) {} diff --git a/tests_config/run_spec.js b/tests_config/run_spec.js index 8b919cae1..f5835f954 100644 --- a/tests_config/run_spec.js +++ b/tests_config/run_spec.js @@ -17,9 +17,23 @@ function run_spec(dirname, options, additionalParsers) { const extension = extname(filename); if (/^\.[jt]sx?$/.test(extension) && filename !== "jsfmt.spec.js") { const path = dirname + "/" + filename; - const mergedOptions = mergeDefaultOptions(options || {}); + let rangeStart = 0; + let rangeEnd = Infinity; + const source = read(path) + .replace(/\r\n/g, "\n") + .replace("<<>>", (match, offset) => { + rangeStart = offset; + return ""; + }) + .replace("<<>>", (match, offset) => { + rangeEnd = offset; + return ""; + }); - const source = read(path).replace(/\r\n/g, "\n"); + const mergedOptions = Object.assign(mergeDefaultOptions(options || {}), { + rangeStart: rangeStart, + rangeEnd: rangeEnd + }); const output = prettyprint(source, path, mergedOptions); test(`${mergedOptions.parser} - ${parser.parser}-verify`, () => { expect(raw(source + "~".repeat(80) + "\n" + output)).toMatchSnapshot( @@ -41,7 +55,6 @@ function run_spec(dirname, options, additionalParsers) { }); if (AST_COMPARE) { - const source = read(dirname + "/" + filename); const ast = parse(source, mergedOptions); const astMassaged = massageAST(ast); let ppastMassaged;