diff --git a/README.md b/README.md index 08138d7b..8db8f057 100644 --- a/README.md +++ b/README.md @@ -243,7 +243,7 @@ exit 1 ### API -The API has two functions, exported as `format` and `check`. `format` usage is as follows: +The API has three functions, exported as `format`, `check`, and `formatWithCursor`. `format` usage is as follows: ```js const prettier = require("prettier"); @@ -255,6 +255,16 @@ prettier.format(source, options); `check` checks to see if the file has been formatted with Prettier given those options and returns a Boolean. This is similar to the `--list-different` parameter in the CLI and is useful for running Prettier in CI scenarios. +`formatWithCursor` both formats the code, and translates a cursor position from unformatted code to formatted code. +This is useful for editor integrations, to prevent the cursor from moving when code is formatted. For example: + +```js +const prettier = require("prettier"); + +prettier.formatWithCursor(" 1", { cursorOffset: 2 }); +// -> { formatted: '1;\n', cursorOffset: 1 } +``` + ### Options Prettier ships with a handful of customizable format options, usable in both the CLI and API. @@ -269,8 +279,9 @@ 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 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: ` | +| **Cursor Offset** - Specify where the cursor is. This option only works with `prettier.formatWithCursor`, and cannot be used with `rangeStart` and `rangeEnd`. | `-1` | `--cursor-offset ` | `cursorOffset: ` | +| **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. This option cannot be used with `cursorOffset`. | `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. This option cannot be used with `cursorOffset`. | `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: "" | | **Filepath** - Specify the input filepath this will be used to do parser inference.

Example:
`cat foo \| prettier --stdin-filepath foo.css`
will default to use `postcss` parser | | `--stdin-filepath` | `filepath: ` | diff --git a/bin/prettier.js b/bin/prettier.js index 920fc318..a66245dc 100755 --- a/bin/prettier.js +++ b/bin/prettier.js @@ -40,6 +40,7 @@ const argv = minimist(process.argv.slice(2), { "tab-width", "parser", "trailing-comma", + "cursor-offset", "range-start", "range-end", "stdin-filepath" @@ -153,6 +154,7 @@ function getTrailingComma() { } const options = { + cursorOffset: getIntOption("cursor-offset"), rangeStart: getIntOption("range-start"), rangeEnd: getIntOption("range-end"), useTabs: argv["use-tabs"], @@ -202,7 +204,7 @@ function format(input, opt) { return; } - return prettier.format(input, opt); + return prettier.formatWithCursor(input, opt); } function handleError(filename, e) { @@ -247,11 +249,15 @@ 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" + + " --cursor-offset Print (to stderr) where a cursor at the given position would move to after formatting.\n" + + " This option cannot be used with --range-start and --range-end\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" + + " This option cannot be used with --cursor-offset.\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" + + " This option cannot be used with --cursor-offset.\n" + " Defaults to Infinity.\n" + " --no-color Do not colorize error messages.\n" + " --with-node-modules Process files inside `node_modules` directory.\n" + @@ -264,8 +270,7 @@ if (argv["help"] || (!filepatterns.length && !stdin)) { if (stdin) { getStdin().then(input => { try { - // Don't use `console.log` here since it adds an extra newline at the end. - process.stdout.write(format(input, options)); + writeOutput(format(input, options)); } catch (e) { handleError("stdin", e); return; @@ -302,13 +307,15 @@ if (stdin) { const start = Date.now(); + let result; let output; try { - output = format( + result = format( input, Object.assign({}, options, { filepath: filename }) ); + output = result.formatted; } catch (e) { // Add newline to split errors from filename line. process.stdout.write("\n"); @@ -351,12 +358,20 @@ if (stdin) { process.exitCode = 2; } } else if (!argv["list-different"]) { - // Don't use `console.log` here since it adds an extra newline at the end. - process.stdout.write(output); + writeOutput(result); } }); } +function writeOutput(result) { + // Don't use `console.log` here since it adds an extra newline at the end. + process.stdout.write(result.formatted); + + if (options.cursorOffset) { + process.stderr.write(result.cursorOffset + "\n"); + } +} + function eachFilename(patterns, callback) { patterns.forEach(pattern => { if (!glob.hasMagic(pattern)) { diff --git a/index.js b/index.js index 9d92028e..7db230dd 100644 --- a/index.js +++ b/index.js @@ -53,26 +53,50 @@ function ensureAllCommentsPrinted(astComments) { }); } -function format(text, opts, addAlignmentSize) { +function formatWithCursor(text, opts, addAlignmentSize) { addAlignmentSize = addAlignmentSize || 0; const ast = parser.parse(text, opts); const formattedRangeOnly = formatRange(text, opts, ast); if (formattedRangeOnly) { - return formattedRangeOnly; + return { formatted: formattedRangeOnly }; + } + + let cursorOffset; + if (opts.cursorOffset >= 0) { + const cursorNodeAndParents = findNodeAtOffset(ast, opts.cursorOffset); + const cursorNode = cursorNodeAndParents.node; + if (cursorNode) { + cursorOffset = opts.cursorOffset - util.locStart(cursorNode); + opts.cursorNode = cursorNode; + } } const astComments = attachComments(text, ast, opts); const doc = printAstToDoc(ast, opts, addAlignmentSize); opts.newLine = guessLineEnding(text); - const str = printDocToString(doc, opts); + const toStringResult = printDocToString(doc, opts); + const str = toStringResult.formatted; + const cursorOffsetResult = toStringResult.cursor; ensureAllCommentsPrinted(astComments); // Remove extra leading indentation as well as the added indentation after last newline if (addAlignmentSize > 0) { - return str.trim() + opts.newLine; + return { formatted: str.trim() + opts.newLine }; } - return str; + + if (cursorOffset !== undefined) { + return { + formatted: str, + cursorOffset: cursorOffsetResult + cursorOffset + }; + } + + return { formatted: str }; +} + +function format(text, opts, addAlignmentSize) { + return formatWithCursor(text, opts, addAlignmentSize).formatted; } function findSiblingAncestors(startNodeAndParents, endNodeAndParents) { @@ -242,6 +266,9 @@ function formatWithShebang(text, opts) { } module.exports = { + formatWithCursor: function(text, opts) { + return formatWithCursor(text, normalizeOptions(opts)); + }, format: function(text, opts) { return formatWithShebang(text, normalizeOptions(opts)); }, diff --git a/src/comments.js b/src/comments.js index 3d52f127..360a4def 100644 --- a/src/comments.js +++ b/src/comments.js @@ -8,6 +8,7 @@ const breakParent = docBuilders.breakParent; const indent = docBuilders.indent; const lineSuffix = docBuilders.lineSuffix; const join = docBuilders.join; +const cursor = docBuilders.cursor; const util = require("./util"); const childNodesCacheKey = Symbol("child-nodes"); const locStart = util.locStart; @@ -924,13 +925,20 @@ function printDanglingComments(path, options, sameIndent) { return indent(concat([hardline, join(hardline, parts)])); } +function prependCursorPlaceholder(path, options, printed) { + if (path.getNode() === options.cursorNode) { + return concat([cursor, printed]); + } + return printed; +} + function printComments(path, print, options, needsSemi) { const value = path.getValue(); const printed = print(path); const comments = value && value.comments; if (!comments || comments.length === 0) { - return printed; + return prependCursorPlaceholder(path, options, printed); } const leadingParts = []; @@ -957,7 +965,11 @@ function printComments(path, print, options, needsSemi) { } }, "comments"); - return concat(leadingParts.concat(trailingParts)); + return prependCursorPlaceholder( + path, + options, + concat(leadingParts.concat(trailingParts)) + ); } module.exports = { diff --git a/src/doc-builders.js b/src/doc-builders.js index dd4bb826..2e547584 100644 --- a/src/doc-builders.js +++ b/src/doc-builders.js @@ -85,6 +85,7 @@ const literalline = concat([ { type: "line", hard: true, literal: true }, breakParent ]); +const cursor = { type: "cursor", placeholder: Symbol() }; function join(sep, arr) { const res = []; @@ -128,6 +129,7 @@ module.exports = { fill, lineSuffix, lineSuffixBoundary, + cursor, breakParent, ifBreak, indent, diff --git a/src/doc-printer.js b/src/doc-printer.js index 1ccda077..c3d489d4 100644 --- a/src/doc-printer.js +++ b/src/doc-printer.js @@ -3,6 +3,7 @@ const docBuilders = require("./doc-builders"); const concat = docBuilders.concat; const fill = docBuilders.fill; +const cursor = docBuilders.cursor; const MODE_BREAK = 1; const MODE_FLAT = 2; @@ -156,6 +157,10 @@ function printDocToString(doc, options) { pos += doc.length; } else { switch (doc.type) { + case "cursor": + out.push(cursor.placeholder); + + break; case "concat": for (let i = doc.parts.length - 1; i >= 0; i--) { cmds.push([ind, mode, doc.parts[i]]); @@ -411,7 +416,19 @@ function printDocToString(doc, options) { } } } - return out.join(""); + + const cursorPlaceholderIndex = out.indexOf(cursor.placeholder); + if (cursorPlaceholderIndex !== -1) { + const beforeCursor = out.slice(0, cursorPlaceholderIndex).join(""); + const afterCursor = out.slice(cursorPlaceholderIndex + 1).join(""); + + return { + formatted: beforeCursor + afterCursor, + cursor: beforeCursor.length + }; + } + + return { formatted: out.join("") }; } module.exports = { printDocToString }; diff --git a/src/options.js b/src/options.js index 35318845..73d40426 100644 --- a/src/options.js +++ b/src/options.js @@ -4,6 +4,7 @@ const validate = require("jest-validate").validate; const deprecatedConfig = require("./deprecated"); const defaults = { + cursorOffset: -1, rangeStart: 0, rangeEnd: Infinity, useTabs: false, diff --git a/tests/cursor/jsfmt.spec.js b/tests/cursor/jsfmt.spec.js new file mode 100644 index 00000000..090654b1 --- /dev/null +++ b/tests/cursor/jsfmt.spec.js @@ -0,0 +1,8 @@ +const prettier = require("../.."); + +test("translates cursor correctly in basic case", () => { + expect(prettier.formatWithCursor(" 1", { cursorOffset: 2 })).toEqual({ + formatted: "1;\n", + cursorOffset: 1 + }); +});