diff --git a/README.md b/README.md index 297bb206f..d52d57a98 100644 --- a/README.md +++ b/README.md @@ -262,6 +262,8 @@ Prettier ships with a handful of customizable format options, usable in both the | **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: ` | | **Parser** - Specify which parser to use. | `babylon` | --parser | parser: "" | | **Semicolons** - Print semicolons at the ends of statements.

Valid options:
- `true` - add a semicolon at the end of every statement
- `false` - only add semicolons at the beginning of lines that may introduce ASI failures | `true` | `--no-semi` | `semi: ` | +| **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: ` | ### Excluding code from formatting diff --git a/bin/prettier.js b/bin/prettier.js index cb77ca0c0..5f5dcd244 100755 --- a/bin/prettier.js +++ b/bin/prettier.js @@ -33,7 +33,7 @@ const argv = minimist(process.argv.slice(2), { // Deprecated in 0.0.10 "flow-parser" ], - string: ["print-width", "tab-width", "parser", "trailing-comma"], + string: ["print-width", "tab-width", "parser", "trailing-comma", "range-start", "range-end"], default: { semi: true, color: true, "bracket-spacing": true, parser: "babylon" }, alias: { help: "h", version: "v", "list-different": "l" }, unknown: param => { @@ -127,6 +127,8 @@ function getTrailingComma() { } const options = { + rangeStart: getIntOption("range-start"), + rangeEnd: getIntOption("range-end"), useTabs: argv["use-tabs"], semi: argv["semi"], printWidth: getIntOption("print-width"), @@ -217,6 +219,8 @@ 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" + " --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 e2464d1ae..fc851f5e9 100644 --- a/index.js +++ b/index.js @@ -3,6 +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 printDocToString = require("./src/doc-printer").printDocToString; const normalizeOptions = require("./src/options").normalize; const parser = require("./src/parser"); @@ -48,16 +49,59 @@ function ensureAllCommentsPrinted(astComments) { }); } -function format(text, opts) { +function format(text, opts, addAlignmentSize) { + addAlignmentSize = addAlignmentSize || 0; + + const formattedRangeOnly = formatRange(text, opts); + if (formattedRangeOnly) { + return formattedRangeOnly; + } + const ast = parser.parse(text, opts); const astComments = attachComments(text, ast, opts); - const doc = printAstToDoc(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 + if (addAlignmentSize > 0) { + return str.slice(opts.newLine.length).trimRight() + 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 + + if (0 < rangeStart || rangeEnd < text.length) { + const rangeString = text.substring(rangeStart, rangeEnd); + const alignmentSize = getAlignmentSize( + rangeString.slice(0, rangeString.search(/[^ \t]/)), + opts.tabWidth + ); + + const rangeFormatted = format( + rangeString, + Object.assign({}, opts, { + rangeStart: 0, + rangeEnd: Infinity, + printWidth: opts.printWidth - alignmentSize + }), + alignmentSize + ); + + return text.slice(0, rangeStart) + rangeFormatted + text.slice(rangeEnd); + } +} + function formatWithShebang(text, opts) { if (!text.startsWith("#!")) { return format(text, opts); diff --git a/src/doc-builders.js b/src/doc-builders.js index 471e557a0..dd4bb8268 100644 --- a/src/doc-builders.js +++ b/src/doc-builders.js @@ -100,6 +100,22 @@ function join(sep, arr) { return concat(res); } +function addAlignmentToDoc(doc, size, tabWidth) { + let aligned = doc; + if (size > 0) { + // Use indent to add tabs for all the levels of tabs we need + for (let i = 0; i < Math.floor(size / tabWidth); ++i) { + aligned = indent(aligned); + } + // Use align for all the spaces that are needed + aligned = align(size % tabWidth, aligned); + // size is absolute from 0 and not relative to the current + // indentation, so we use -Infinity to reset the indentation to 0 + aligned = align(-Infinity, aligned); + } + return aligned; +} + module.exports = { concat, join, @@ -115,5 +131,6 @@ module.exports = { breakParent, ifBreak, indent, - align + align, + addAlignmentToDoc }; diff --git a/src/options.js b/src/options.js index 9800cc3a8..e0b6d72e9 100644 --- a/src/options.js +++ b/src/options.js @@ -4,6 +4,8 @@ const validate = require("jest-validate").validate; const deprecatedConfig = require("./deprecated"); const defaults = { + rangeStart: 0, + rangeEnd: Infinity, useTabs: false, tabWidth: 2, printWidth: 80, diff --git a/src/printer.js b/src/printer.js index 99695f4b7..140059326 100644 --- a/src/printer.js +++ b/src/printer.js @@ -21,6 +21,7 @@ const fill = docBuilders.fill; const ifBreak = docBuilders.ifBreak; const breakParent = docBuilders.breakParent; const lineSuffixBoundary = docBuilders.lineSuffixBoundary; +const addAlignmentToDoc = docBuilders.addAlignmentToDoc; const docUtils = require("./doc-utils"); const willBreak = docUtils.willBreak; @@ -1670,31 +1671,14 @@ function genericPrintNoParens(path, options, print, args) { const index = value.lastIndexOf('\n'); const tabWidth = options.tabWidth; if (index !== -1) { - for (let i = index + 1; i < value.length; ++i) { - if (value[i] === '\t') { - // Tabs behave in a way that they are aligned to the nearest - // multiple of tabWidth: - // 0 -> 4, 1 -> 4, 2 -> 4, 3 -> 4 - // 4 -> 8, 5 -> 8, 6 -> 8, 7 -> 8 ... - size = size + tabWidth - size % tabWidth; - } else { - size++; - } - } + size = util.getAlignmentSize(value, tabWidth, index + 1); } - let aligned = removeLines(expressions[i]); - if (size > 0) { - // Use indent to add tabs for all the levels of tabs we need - for (let i = 0; i < Math.floor(size / tabWidth); ++i) { - aligned = indent(aligned); - } - // Use align for all the spaces that are needed - aligned = align(size % tabWidth, aligned); - // size is absolute from 0 and not relative to the current - // indentation, so we use -Infinity to reset the indentation to 0 - aligned = align(-Infinity, aligned); - } + let aligned = addAlignmentToDoc( + removeLines(expressions[i]), + size, + tabWidth + ); parts.push( "${", @@ -4281,7 +4265,9 @@ function removeLines(doc) { }); } -function printAstToDoc(ast, options) { +function printAstToDoc(ast, options, addAlignmentSize) { + addAlignmentSize = addAlignmentSize || 0; + function printGenerically(path, args) { return comments.printComments( path, @@ -4291,7 +4277,16 @@ function printAstToDoc(ast, options) { ); } - const doc = printGenerically(FastPath.from(ast)); + let doc = printGenerically(FastPath.from(ast)); + if (addAlignmentSize > 0) { + // Add a hardline to make the indents take effect + // It should be removed in index.js format() + doc = addAlignmentToDoc( + removeLines(concat([hardline, doc])), + addAlignmentSize, + options.tabWidth + ); + } docUtils.propagateBreaks(doc); return doc; } diff --git a/src/util.js b/src/util.js index 92207f884..63d804e56 100644 --- a/src/util.js +++ b/src/util.js @@ -320,6 +320,25 @@ function isBlockComment(comment) { return comment.type === "Block" || comment.type === "CommentBlock"; } +function getAlignmentSize(value, tabWidth, startIndex) { + startIndex = startIndex || 0; + + let size = 0; + for (let i = startIndex; i < value.length; ++i) { + if (value[i] === '\t') { + // Tabs behave in a way that they are aligned to the nearest + // multiple of tabWidth: + // 0 -> 4, 1 -> 4, 2 -> 4, 3 -> 4 + // 4 -> 8, 5 -> 8, 6 -> 8, 7 -> 8 ... + size = size + tabWidth - size % tabWidth; + } else { + size++; + } + } + + return size; +} + module.exports = { getPrecedence, isExportDeclaration, @@ -341,5 +360,6 @@ module.exports = { setLocEnd, startsWithNoLookaheadToken, hasBlockComments, - isBlockComment + isBlockComment, + getAlignmentSize }; diff --git a/tests/range/__snapshots__/jsfmt.spec.js.snap b/tests/range/__snapshots__/jsfmt.spec.js.snap new file mode 100644 index 000000000..3feb81f45 --- /dev/null +++ b/tests/range/__snapshots__/jsfmt.spec.js.snap @@ -0,0 +1,24 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +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 + } + } +} +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +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 + } + } +} + +`; diff --git a/tests/range/jsfmt.spec.js b/tests/range/jsfmt.spec.js new file mode 100644 index 000000000..2d80d1068 --- /dev/null +++ b/tests/range/jsfmt.spec.js @@ -0,0 +1 @@ +run_spec(__dirname, { rangeStart: 180, rangeEnd: 220 }); diff --git a/tests/range/range.js b/tests/range/range.js new file mode 100644 index 000000000..ed1eb1bc6 --- /dev/null +++ b/tests/range/range.js @@ -0,0 +1,9 @@ +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 + } + } +}