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
+ });
+});