412 lines
11 KiB
JavaScript
412 lines
11 KiB
JavaScript
"use strict";
|
|
|
|
const diff = require("diff");
|
|
|
|
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 {
|
|
guessEndOfLine,
|
|
convertEndOfLineToChars
|
|
} = require("../common/end-of-line");
|
|
const rangeUtil = require("./range-util");
|
|
const privateUtil = require("../common/util");
|
|
const {
|
|
utils: { mapDoc },
|
|
printer: { printDocToString },
|
|
debug: { printDocToDebug }
|
|
} = require("../doc");
|
|
|
|
const UTF8BOM = 0xfeff;
|
|
|
|
const CURSOR = Symbol("cursor");
|
|
const PLACEHOLDERS = {
|
|
cursorOffset: "<<<PRETTIER_CURSOR>>>",
|
|
rangeStart: "<<<PRETTIER_RANGE_START>>>",
|
|
rangeEnd: "<<<PRETTIER_RANGE_END>>>"
|
|
};
|
|
|
|
function ensureAllCommentsPrinted(astComments) {
|
|
if (!astComments) {
|
|
return;
|
|
}
|
|
|
|
for (let i = 0; i < astComments.length; ++i) {
|
|
if (astComments[i].value.trim() === "prettier-ignore") {
|
|
// If there's a prettier-ignore, we're not printing that sub-tree so we
|
|
// don't know if the comments was printed or not.
|
|
return;
|
|
}
|
|
}
|
|
|
|
astComments.forEach(comment => {
|
|
if (!comment.printed) {
|
|
throw new Error(
|
|
'Comment "' +
|
|
comment.value.trim() +
|
|
'" was not printed. Please report this error!'
|
|
);
|
|
}
|
|
delete comment.printed;
|
|
});
|
|
}
|
|
|
|
function attachComments(text, ast, opts) {
|
|
const astComments = ast.comments;
|
|
if (astComments) {
|
|
delete ast.comments;
|
|
comments.attach(astComments, ast, text, opts);
|
|
}
|
|
ast.tokens = [];
|
|
opts.originalText = opts.parser === "yaml" ? text : text.trimRight();
|
|
return astComments;
|
|
}
|
|
|
|
function coreFormat(text, opts, addAlignmentSize) {
|
|
if (!text || !text.trim().length) {
|
|
return { formatted: "", cursorOffset: 0 };
|
|
}
|
|
|
|
addAlignmentSize = addAlignmentSize || 0;
|
|
|
|
const parsed = parser.parse(text, opts);
|
|
const ast = parsed.ast;
|
|
text = parsed.text;
|
|
|
|
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 eol = convertEndOfLineToChars(opts.endOfLine);
|
|
const result = printDocToString(
|
|
opts.endOfLine === "lf"
|
|
? doc
|
|
: mapDoc(doc, currentDoc =>
|
|
typeof currentDoc === "string" && currentDoc.indexOf("\n") !== -1
|
|
? currentDoc.replace(/\n/g, eol)
|
|
: currentDoc
|
|
),
|
|
opts
|
|
);
|
|
|
|
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
|
|
};
|
|
}
|
|
|
|
// 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.indexOf(CURSOR) > -1) {
|
|
break;
|
|
}
|
|
} else {
|
|
cursorOffset += entry.count;
|
|
}
|
|
}
|
|
|
|
return { formatted: result.formatted, cursorOffset };
|
|
}
|
|
|
|
return { formatted: result.formatted };
|
|
}
|
|
|
|
function formatRange(text, opts) {
|
|
const parsed = parser.parse(text, opts);
|
|
const ast = parsed.ast;
|
|
text = parsed.text;
|
|
|
|
const range = rangeUtil.calculateRange(text, opts, ast);
|
|
const rangeStart = range.rangeStart;
|
|
const rangeEnd = range.rangeEnd;
|
|
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);
|
|
|
|
const alignmentSize = privateUtil.getAlignmentSize(
|
|
indentString,
|
|
opts.tabWidth
|
|
);
|
|
|
|
const rangeResult = coreFormat(
|
|
rangeString,
|
|
Object.assign({}, opts, {
|
|
rangeStart: 0,
|
|
rangeEnd: Infinity,
|
|
printWidth: opts.printWidth - alignmentSize,
|
|
// track the cursor offset only if it's within our range
|
|
cursorOffset:
|
|
opts.cursorOffset >= rangeStart && opts.cursorOffset < rangeEnd
|
|
? opts.cursorOffset - rangeStart
|
|
: -1
|
|
}),
|
|
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.trimRight();
|
|
const rangeLeft = text.slice(0, rangeStart);
|
|
const rangeRight = text.slice(rangeEnd);
|
|
|
|
let cursorOffset = opts.cursorOffset;
|
|
if (opts.cursorOffset >= rangeEnd) {
|
|
// handle the case where the cursor was past the end of the range
|
|
cursorOffset =
|
|
opts.cursorOffset - rangeEnd + (rangeStart + rangeTrimmed.length);
|
|
} else if (rangeResult.cursorOffset !== undefined) {
|
|
// 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;
|
|
if (opts.endOfLine === "lf") {
|
|
formatted = rangeLeft + rangeTrimmed + rangeRight;
|
|
} else {
|
|
const eol = convertEndOfLineToChars(opts.endOfLine);
|
|
if (cursorOffset >= 0) {
|
|
const parts = [rangeLeft, rangeTrimmed, rangeRight];
|
|
let partIndex = 0;
|
|
let partOffset = cursorOffset;
|
|
while (partIndex < parts.length) {
|
|
const part = parts[partIndex];
|
|
if (partOffset < part.length) {
|
|
parts[partIndex] =
|
|
parts[partIndex].slice(0, partOffset) +
|
|
PLACEHOLDERS.cursorOffset +
|
|
parts[partIndex].slice(partOffset);
|
|
break;
|
|
}
|
|
partIndex++;
|
|
partOffset -= part.length;
|
|
}
|
|
const [newRangeLeft, newRangeTrimmed, newRangeRight] = parts;
|
|
formatted = (
|
|
newRangeLeft.replace(/\n/g, eol) +
|
|
newRangeTrimmed +
|
|
newRangeRight.replace(/\n/g, eol)
|
|
).replace(PLACEHOLDERS.cursorOffset, (_, index) => {
|
|
cursorOffset = index;
|
|
return "";
|
|
});
|
|
} else {
|
|
formatted =
|
|
rangeLeft.replace(/\n/g, eol) +
|
|
rangeTrimmed +
|
|
rangeRight.replace(/\n/g, eol);
|
|
}
|
|
}
|
|
|
|
return { formatted, cursorOffset };
|
|
}
|
|
|
|
function format(text, opts) {
|
|
const selectedParser = parser.resolveParser(opts);
|
|
const hasPragma = !selectedParser.hasPragma || selectedParser.hasPragma(text);
|
|
if (opts.requirePragma && !hasPragma) {
|
|
return { formatted: text };
|
|
}
|
|
|
|
if (opts.endOfLine === "auto") {
|
|
opts.endOfLine = guessEndOfLine(text);
|
|
}
|
|
|
|
const hasCursor = opts.cursorOffset >= 0;
|
|
const hasRangeStart = opts.rangeStart > 0;
|
|
const hasRangeEnd = opts.rangeEnd < text.length;
|
|
|
|
// get rid of CR/CRLF parsing
|
|
if (text.indexOf("\r") !== -1) {
|
|
const offsetKeys = [
|
|
hasCursor && "cursorOffset",
|
|
hasRangeStart && "rangeStart",
|
|
hasRangeEnd && "rangeEnd"
|
|
]
|
|
.filter(Boolean)
|
|
.sort((aKey, bKey) => opts[aKey] - opts[bKey]);
|
|
|
|
for (let i = offsetKeys.length - 1; i >= 0; i--) {
|
|
const key = offsetKeys[i];
|
|
text =
|
|
text.slice(0, opts[key]) + PLACEHOLDERS[key] + text.slice(opts[key]);
|
|
}
|
|
|
|
text = text.replace(/\r\n?/g, "\n");
|
|
|
|
for (let i = 0; i < offsetKeys.length; i++) {
|
|
const key = offsetKeys[i];
|
|
text = text.replace(PLACEHOLDERS[key], (_, index) => {
|
|
opts[key] = index;
|
|
return "";
|
|
});
|
|
}
|
|
}
|
|
|
|
const hasUnicodeBOM = text.charCodeAt(0) === UTF8BOM;
|
|
if (hasUnicodeBOM) {
|
|
text = text.substring(1);
|
|
if (hasCursor) {
|
|
opts.cursorOffset++;
|
|
}
|
|
if (hasRangeStart) {
|
|
opts.rangeStart++;
|
|
}
|
|
if (hasRangeEnd) {
|
|
opts.rangeEnd++;
|
|
}
|
|
}
|
|
|
|
if (!hasCursor) {
|
|
opts.cursorOffset = -1;
|
|
}
|
|
if (opts.rangeStart < 0) {
|
|
opts.rangeStart = 0;
|
|
}
|
|
if (opts.rangeEnd > text.length) {
|
|
opts.rangeEnd = text.length;
|
|
}
|
|
|
|
const result =
|
|
hasRangeStart || hasRangeEnd
|
|
? formatRange(text, opts)
|
|
: coreFormat(
|
|
opts.insertPragma && opts.printer.insertPragma && !hasPragma
|
|
? opts.printer.insertPragma(text)
|
|
: text,
|
|
opts
|
|
);
|
|
|
|
if (hasUnicodeBOM) {
|
|
result.formatted = String.fromCharCode(UTF8BOM) + result.formatted;
|
|
|
|
if (hasCursor) {
|
|
result.cursorOffset++;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
module.exports = {
|
|
formatWithCursor(text, opts) {
|
|
opts = normalizeOptions(opts);
|
|
return format(text, opts);
|
|
},
|
|
|
|
parse(text, opts, massage) {
|
|
opts = normalizeOptions(opts);
|
|
if (text.indexOf("\r") !== -1) {
|
|
text = text.replace(/\r\n?/g, "\n");
|
|
}
|
|
const parsed = parser.parse(text, opts);
|
|
if (massage) {
|
|
parsed.ast = massageAST(parsed.ast, opts);
|
|
}
|
|
return parsed;
|
|
},
|
|
|
|
formatAST(ast, opts) {
|
|
opts = normalizeOptions(opts);
|
|
const doc = printAstToDoc(ast, opts);
|
|
return printDocToString(doc, opts);
|
|
},
|
|
|
|
// Doesn't handle shebang for now
|
|
formatDoc(doc, opts) {
|
|
const debug = printDocToDebug(doc);
|
|
opts = normalizeOptions(Object.assign({}, opts, { parser: "babylon" }));
|
|
return format(debug, opts).formatted;
|
|
},
|
|
|
|
printToDoc(text, opts) {
|
|
opts = normalizeOptions(opts);
|
|
const parsed = parser.parse(text, opts);
|
|
const ast = parsed.ast;
|
|
text = parsed.text;
|
|
attachComments(text, ast, opts);
|
|
return printAstToDoc(ast, opts);
|
|
},
|
|
|
|
printDocToString(doc, opts) {
|
|
return printDocToString(doc, normalizeOptions(opts));
|
|
}
|
|
};
|