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_r119737354master
parent
a983d30663
commit
4bfeb9064d
17
README.md
17
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.<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|es5|all></code> | <code>trailingComma: "<none|es5|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|babylon></code> | <code>parser: "<flow|babylon|postcss|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>` |
|
||||
|
||||
|
|
|
@ -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)) {
|
||||
|
|
37
index.js
37
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));
|
||||
},
|
||||
|
|
|
@ -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 = {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 };
|
||||
|
|
|
@ -4,6 +4,7 @@ const validate = require("jest-validate").validate;
|
|||
const deprecatedConfig = require("./deprecated");
|
||||
|
||||
const defaults = {
|
||||
cursorOffset: -1,
|
||||
rangeStart: 0,
|
||||
rangeEnd: Infinity,
|
||||
useTabs: false,
|
||||
|
|
|
@ -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
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue