Add `cursorOffset` option for cursor translation (#1637)

* Add `formatWithCursor` API with `cursorOffset` option

This addresses https://github.com/prettier/prettier/issues/93 by
adding a new option, `cursorOffset`, that tells prettier to determine
the location of the cursor after the code has been formatted. This is
accessible through the API via a new function, `formatWithCursor`, which
returns a `{formatted: string, cursorOffset: ?number}`.

Here's a usage example:

```js
require("prettier").formatWithCursor(" 1", { cursorOffset: 2 });
// -> { formatted: '1;\n', cursorOffset: 1 }
```

* Add `--cursor-offset` CLI option

It will print out the offset instead of the formatted output. This
makes it easier to test. For example:

    echo ' 1' | prettier --stdin --cursor-offset 2
    # prints 1

* Add basic test of cursor translation

* Document `cursorOffset` option and `formatWithCursor()`

* Print translated cursor offset to stderr when --cursor-offset is given

This lets us continue to print the formatted code, while also
communicating the updated cursor position.

See https://github.com/prettier/prettier/pull/1637#discussion_r119735496

* doc-print cursor placeholder in comments.printComments()

See https://github.com/prettier/prettier/pull/1637#discussion_r119735149

* Compare array index to -1 instead of >= 0 to determine element presence

See https://github.com/prettier/prettier/pull/1637#discussion_r119736623

* Return {formatted, cursor} from printDocToString() instead of mutating options

See https://github.com/prettier/prettier/pull/1637#discussion_r119737354
master
Joseph Frazier 2017-06-01 18:52:29 -04:00 committed by Christopher Chedeau
parent a983d30663
commit 4bfeb9064d
8 changed files with 110 additions and 17 deletions

View File

@ -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.<br /><br />Valid options: <br /> - `"none"` - no trailing commas <br /> - `"es5"` - trailing commas where valid in ES5 (objects, arrays, etc) <br /> - `"all"` - trailing commas wherever possible (function arguments). This requires node 8 or a [transform](https://babeljs.io/docs/plugins/syntax-trailing-function-commas/). | `"none"` | <code>--trailing-comma <none&#124;es5&#124;all></code> | <code>trailingComma: "<none&#124;es5&#124;all>"</code> |
| **Bracket Spacing** - Print spaces between brackets in object literals.<br /><br />Valid options: <br /> - `true` - Example: `{ foo: bar }` <br /> - `false` - Example: `{foo: bar}` | `true` | `--no-bracket-spacing` | `bracketSpacing: <bool>` |
| **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: <bool>` |
| **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 <int>` | `rangeStart: <int>` |
| **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 <int>` | `rangeEnd: <int>` |
| **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 <int>` | `cursorOffset: <int>` |
| **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 <int>` | `rangeStart: <int>` |
| **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 <int>` | `rangeEnd: <int>` |
| **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` | <code>--parser <flow&#124;babylon></code> | <code>parser: "<flow&#124;babylon&#124;postcss&#124;typescript>"</code> |
| **Filepath** - Specify the input filepath this will be used to do parser inference.<br /><br /> Example: <br />`cat foo \| prettier --stdin-filepath foo.css`<br /> will default to use `postcss` parser | | `--stdin-filepath` | `filepath: <string>` |

View File

@ -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 <none|es5|all>\n" +
" Print trailing commas wherever possible. Defaults to none.\n" +
" --parser <flow|babylon> Specify which parse to use. Defaults to babylon.\n" +
" --cursor-offset <int> 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 <int> 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 <int> 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)) {

View File

@ -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));
},

View File

@ -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 = {

View File

@ -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,

View File

@ -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 };

View File

@ -4,6 +4,7 @@ const validate = require("jest-validate").validate;
const deprecatedConfig = require("./deprecated");
const defaults = {
cursorOffset: -1,
rangeStart: 0,
rangeEnd: Infinity,
useTabs: false,

View File

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