prettierx
Version:
prettierX - a less opinionated fork of the Prettier code formatter
366 lines (301 loc) • 9.89 kB
JavaScript
;
const diff = require("diff");
const {
printer: { printDocToString },
debug: { printDocToDebug },
} = require("../document");
const { getAlignmentSize } = require("../common/util");
const {
guessEndOfLine,
convertEndOfLineToChars,
countEndOfLineChars,
normalizeEndOfLine,
} = require("../common/end-of-line");
const normalizeOptions = require("./options").normalize;
const massageAST = require("./massage-ast");
const comments = require("./comments");
const parser = require("./parser");
const printAstToDoc = require("./ast-to-doc");
const rangeUtil = require("./range-util");
const BOM = "\uFEFF";
const CURSOR = Symbol("cursor");
function attachComments(text, ast, opts) {
const astComments = ast.comments;
if (astComments) {
delete ast.comments;
comments.attach(astComments, ast, text, opts);
}
opts[Symbol.for("comments")] = astComments || [];
opts[Symbol.for("tokens")] = ast.tokens || [];
opts.originalText = text;
return astComments;
}
function coreFormat(originalText, opts, addAlignmentSize = 0) {
if (!originalText || originalText.trim().length === 0) {
return { formatted: "", cursorOffset: -1, comments: [] };
}
const { ast, text } = parser.parse(originalText, opts);
if (opts.cursorOffset >= 0) {
const nodeResult = rangeUtil.findNodeAtOffset(ast, opts.cursorOffset, opts);
if (nodeResult && nodeResult.node) {
opts.cursorNode = nodeResult.node;
}
}
const astComments = attachComments(text, ast, opts);
const doc = printAstToDoc(ast, opts, addAlignmentSize);
const result = printDocToString(doc, opts);
comments.ensureAllCommentsPrinted(astComments);
// Remove extra leading indentation as well as the added indentation after last newline
if (addAlignmentSize > 0) {
const trimmed = result.formatted.trim();
if (result.cursorNodeStart !== undefined) {
result.cursorNodeStart -= result.formatted.indexOf(trimmed);
}
result.formatted = trimmed + convertEndOfLineToChars(opts.endOfLine);
}
if (opts.cursorOffset >= 0) {
let oldCursorNodeStart;
let oldCursorNodeText;
let cursorOffsetRelativeToOldCursorNode;
let newCursorNodeStart;
let newCursorNodeText;
if (opts.cursorNode && result.cursorNodeText) {
oldCursorNodeStart = opts.locStart(opts.cursorNode);
oldCursorNodeText = text.slice(
oldCursorNodeStart,
opts.locEnd(opts.cursorNode)
);
cursorOffsetRelativeToOldCursorNode =
opts.cursorOffset - oldCursorNodeStart;
newCursorNodeStart = result.cursorNodeStart;
newCursorNodeText = result.cursorNodeText;
} else {
oldCursorNodeStart = 0;
oldCursorNodeText = text;
cursorOffsetRelativeToOldCursorNode = opts.cursorOffset;
newCursorNodeStart = 0;
newCursorNodeText = result.formatted;
}
if (oldCursorNodeText === newCursorNodeText) {
return {
formatted: result.formatted,
cursorOffset: newCursorNodeStart + cursorOffsetRelativeToOldCursorNode,
comments: astComments,
};
}
// diff old and new cursor node texts, with a special cursor
// symbol inserted to find out where it moves to
const oldCursorNodeCharArray = oldCursorNodeText.split("");
oldCursorNodeCharArray.splice(
cursorOffsetRelativeToOldCursorNode,
0,
CURSOR
);
const newCursorNodeCharArray = newCursorNodeText.split("");
const cursorNodeDiff = diff.diffArrays(
oldCursorNodeCharArray,
newCursorNodeCharArray
);
let cursorOffset = newCursorNodeStart;
for (const entry of cursorNodeDiff) {
if (entry.removed) {
if (entry.value.includes(CURSOR)) {
break;
}
} else {
cursorOffset += entry.count;
}
}
return { formatted: result.formatted, cursorOffset, comments: astComments };
}
return {
formatted: result.formatted,
cursorOffset: -1,
comments: astComments,
};
}
function formatRange(originalText, opts) {
const { ast, text } = parser.parse(originalText, opts);
const { rangeStart, rangeEnd } = rangeUtil.calculateRange(text, opts, ast);
const rangeString = text.slice(rangeStart, rangeEnd);
// Try to extend the range backwards to the beginning of the line.
// This is so we can detect indentation correctly and restore it.
// Use `Math.min` since `lastIndexOf` returns 0 when `rangeStart` is 0
const rangeStart2 = Math.min(
rangeStart,
text.lastIndexOf("\n", rangeStart) + 1
);
const indentString = text.slice(rangeStart2, rangeStart).match(/^\s*/)[0];
const alignmentSize = getAlignmentSize(indentString, opts.tabWidth);
const rangeResult = coreFormat(
rangeString,
{
...opts,
rangeStart: 0,
rangeEnd: Number.POSITIVE_INFINITY,
// Track the cursor offset only if it's within our range
cursorOffset:
opts.cursorOffset > rangeStart && opts.cursorOffset <= rangeEnd
? opts.cursorOffset - rangeStart
: -1,
// Always use `lf` to format, we'll replace it later
endOfLine: "lf",
},
alignmentSize
);
// Since the range contracts to avoid trailing whitespace,
// we need to remove the newline that was inserted by the `format` call.
const rangeTrimmed = rangeResult.formatted.trimEnd();
let { cursorOffset } = opts;
if (cursorOffset > rangeEnd) {
// handle the case where the cursor was past the end of the range
cursorOffset += rangeTrimmed.length - rangeString.length;
} else if (rangeResult.cursorOffset >= 0) {
// handle the case where the cursor was in the range
cursorOffset = rangeResult.cursorOffset + rangeStart;
}
// keep the cursor as it was if it was before the start of the range
let formatted =
text.slice(0, rangeStart) + rangeTrimmed + text.slice(rangeEnd);
if (opts.endOfLine !== "lf") {
const eol = convertEndOfLineToChars(opts.endOfLine);
if (cursorOffset >= 0 && eol === "\r\n") {
cursorOffset += countEndOfLineChars(
formatted.slice(0, cursorOffset),
"\n"
);
}
formatted = formatted.replace(/\n/g, eol);
}
return { formatted, cursorOffset, comments: rangeResult.comments };
}
function ensureIndexInText(text, index, defaultValue) {
if (
typeof index !== "number" ||
Number.isNaN(index) ||
index < 0 ||
index > text.length
) {
return defaultValue;
}
return index;
}
function normalizeIndexes(text, options) {
let { cursorOffset, rangeStart, rangeEnd } = options;
cursorOffset = ensureIndexInText(text, cursorOffset, -1);
rangeStart = ensureIndexInText(text, rangeStart, 0);
rangeEnd = ensureIndexInText(text, rangeEnd, text.length);
return { ...options, cursorOffset, rangeStart, rangeEnd };
}
function normalizeInputAndOptions(text, options) {
let { cursorOffset, rangeStart, rangeEnd, endOfLine } = normalizeIndexes(
text,
options
);
const hasBOM = text.charAt(0) === BOM;
if (hasBOM) {
text = text.slice(1);
cursorOffset--;
rangeStart--;
rangeEnd--;
}
if (endOfLine === "auto") {
endOfLine = guessEndOfLine(text);
}
// get rid of CR/CRLF parsing
if (text.includes("\r")) {
const countCrlfBefore = (index) =>
countEndOfLineChars(text.slice(0, Math.max(index, 0)), "\r\n");
cursorOffset -= countCrlfBefore(cursorOffset);
rangeStart -= countCrlfBefore(rangeStart);
rangeEnd -= countCrlfBefore(rangeEnd);
text = normalizeEndOfLine(text);
}
return {
hasBOM,
text,
options: normalizeIndexes(text, {
...options,
cursorOffset,
rangeStart,
rangeEnd,
endOfLine,
}),
};
}
function hasPragma(text, options) {
const selectedParser = parser.resolveParser(options);
return !selectedParser.hasPragma || selectedParser.hasPragma(text);
}
function formatWithCursor(originalText, originalOptions) {
let { hasBOM, text, options } = normalizeInputAndOptions(
originalText,
normalizeOptions(originalOptions)
);
if (
(options.rangeStart >= options.rangeEnd && text !== "") ||
(options.requirePragma && !hasPragma(text, options))
) {
return {
formatted: originalText,
cursorOffset: originalOptions.cursorOffset,
comments: [],
};
}
let result;
if (options.rangeStart > 0 || options.rangeEnd < text.length) {
result = formatRange(text, options);
} else {
if (
!options.requirePragma &&
options.insertPragma &&
options.printer.insertPragma &&
!hasPragma(text, options)
) {
text = options.printer.insertPragma(text);
}
result = coreFormat(text, options);
}
if (hasBOM) {
result.formatted = BOM + result.formatted;
if (result.cursorOffset >= 0) {
result.cursorOffset++;
}
}
return result;
}
module.exports = {
formatWithCursor,
parse(originalText, originalOptions, massage) {
const { text, options } = normalizeInputAndOptions(
originalText,
normalizeOptions(originalOptions)
);
const parsed = parser.parse(text, options);
if (massage) {
parsed.ast = massageAST(parsed.ast, options);
}
return parsed;
},
formatAST(ast, options) {
options = normalizeOptions(options);
const doc = printAstToDoc(ast, options);
return printDocToString(doc, options);
},
// Doesn't handle shebang for now
formatDoc(doc, options) {
return formatWithCursor(printDocToDebug(doc), {
...options,
parser: "__js_expression",
}).formatted;
},
printToDoc(originalText, options) {
options = normalizeOptions(options);
const { ast, text } = parser.parse(originalText, options);
attachComments(text, ast, options);
return printAstToDoc(ast, options);
},
printDocToString(doc, options) {
return printDocToString(doc, normalizeOptions(options));
},
};