diff --git a/commands.md b/commands.md index 6020150c..ea73d56f 100644 --- a/commands.md +++ b/commands.md @@ -237,6 +237,14 @@ declare function dedentToRoot(doc: Doc): Doc; This will dedent the current indentation to the root marked by `markAsRoot`. +### trim + +```ts +declare var trim: Doc; +``` + +This will trim any whitespace or tab character on the current line. This is used for preprocessor directives. + ### cursor ```ts diff --git a/src/doc/doc-builders.js b/src/doc/doc-builders.js index 08acf143..6965fc58 100644 --- a/src/doc/doc-builders.js +++ b/src/doc/doc-builders.js @@ -113,6 +113,7 @@ function lineSuffix(contents) { const lineSuffixBoundary = { type: "line-suffix-boundary" }; const breakParent = { type: "break-parent" }; +const trim = { type: "trim" }; const line = { type: "line" }; const softline = { type: "line", soft: true }; const hardline = concat([{ type: "line", hard: true }, breakParent]); @@ -167,6 +168,7 @@ module.exports = { cursor, breakParent, ifBreak, + trim, indent, align, addAlignmentToDoc, diff --git a/src/doc/doc-debug.js b/src/doc/doc-debug.js index 0c9875b6..51d57e61 100644 --- a/src/doc/doc-debug.js +++ b/src/doc/doc-debug.js @@ -59,6 +59,10 @@ function printDoc(doc) { return "breakParent"; } + if (doc.type === "trim") { + return "trim"; + } + if (doc.type === "concat") { return "[" + doc.parts.map(printDoc).join(", ") + "]"; } diff --git a/src/doc/doc-printer.js b/src/doc/doc-printer.js index 0ed9b0f8..5d2e511b 100644 --- a/src/doc/doc-printer.js +++ b/src/doc/doc-printer.js @@ -109,9 +109,37 @@ function generateInd(ind, newPart, options) { } } +function trim(out) { + if (out.length === 0) { + return 0; + } + + let trimCount = 0; + + // Trim whitespace at the end of line + while ( + out.length > 0 && + typeof out[out.length - 1] === "string" && + out[out.length - 1].match(/^[ \t]*$/) + ) { + trimCount += out.pop().length; + } + + if (out.length && typeof out[out.length - 1] === "string") { + const trimmed = out[out.length - 1].replace(/[ \t]*$/, ""); + trimCount += out[out.length - 1].length - trimmed.length; + out[out.length - 1] = trimmed; + } + + return trimCount; +} + function fits(next, restCommands, width, options, mustBeFlat) { let restIdx = restCommands.length; const cmds = [next]; + // `out` is only used for width counting because `trim` requires to look + // backwards for space characters. + const out = []; while (width >= 0) { if (cmds.length === 0) { if (restIdx === 0) { @@ -130,6 +158,8 @@ function fits(next, restCommands, width, options, mustBeFlat) { const doc = x[2]; if (typeof doc === "string") { + out.push(doc); + width -= getStringWidth(doc); } else { switch (doc.type) { @@ -146,6 +176,10 @@ function fits(next, restCommands, width, options, mustBeFlat) { case "align": cmds.push([makeAlign(ind, doc.n, options), mode, doc.contents]); + break; + case "trim": + width += trim(out); + break; case "group": if (mustBeFlat && doc.break) { @@ -184,6 +218,8 @@ function fits(next, restCommands, width, options, mustBeFlat) { case MODE_FLAT: if (!doc.hard) { if (!doc.soft) { + out.push(" "); + width -= 1; } @@ -244,6 +280,10 @@ function printDocToString(doc, options) { case "align": cmds.push([makeAlign(ind, doc.n, options), mode, doc.contents]); + break; + case "trim": + pos -= trim(out); + break; case "group": switch (mode) { @@ -471,24 +511,7 @@ function printDocToString(doc, options) { pos = 0; } } else { - if (out.length > 0) { - // Trim whitespace at the end of line - while ( - out.length > 0 && - typeof out[out.length - 1] === "string" && - out[out.length - 1].match(/^[ \t]*$/) - ) { - out.pop(); - } - - if (out.length && typeof out[out.length - 1] === "string") { - out[out.length - 1] = out[out.length - 1].replace( - /[ \t]*$/, - "" - ); - } - } - + pos -= trim(out); out.push(newLine + ind.value); pos = ind.length; } diff --git a/tests_integration/__tests__/doc-mark-as-root.js b/tests_integration/__tests__/doc-mark-as-root.js new file mode 100644 index 00000000..7b1dbc78 --- /dev/null +++ b/tests_integration/__tests__/doc-mark-as-root.js @@ -0,0 +1,38 @@ +"use strict"; + +const prettier = require("prettier/local"); +const docPrinter = prettier.doc.printer; +const docBuilders = prettier.doc.builders; + +const printDocToString = docPrinter.printDocToString; +const concat = docBuilders.concat; +const hardline = docBuilders.hardline; +const literalline = docBuilders.literalline; +const trim = docBuilders.trim; +const indent = docBuilders.indent; +const markAsRoot = docBuilders.markAsRoot; + +describe("markAsRoot", () => { + test.each([ + [ + "with hardline will insert a newline with current indentation", + concat([indent(markAsRoot(indent(hardline))), "123"]), + "\n 123" + ], + [ + "with literalline will insert a newline with root indentation", + concat([indent(markAsRoot(indent(literalline))), "123"]), + "\n 123" + ], + [ + "followed by trim will trims up to the the first column, ignoring indented root", + concat([indent(markAsRoot(indent(literalline))), trim, "123"]), + "\n123" + ] + ])("%s", (_, doc, expected) => { + const result = printDocToString(doc, { printWidth: 80, tabWidth: 2 }); + + expect(result).toBeDefined(); + expect(result.formatted).toEqual(expected); + }); +}); diff --git a/tests_integration/__tests__/doc-trim.js b/tests_integration/__tests__/doc-trim.js new file mode 100644 index 00000000..aa5fdeb8 --- /dev/null +++ b/tests_integration/__tests__/doc-trim.js @@ -0,0 +1,59 @@ +"use strict"; + +const prettier = require("prettier/local"); +const docPrinter = prettier.doc.printer; +const docBuilders = prettier.doc.builders; + +const printDocToString = docPrinter.printDocToString; +const concat = docBuilders.concat; +const line = docBuilders.line; +const trim = docBuilders.trim; +const group = docBuilders.group; +const indent = docBuilders.indent; + +// These tests don't use `runPrettier` because `trim` is not used by any +// bundled parser (only third-party plugins). + +describe("trim", () => { + test.each([ + ["trims the current line", group(concat(["hello ", trim])), "hello"], + [ + "trims existing indentation", + group( + concat([ + "function()", + line, + "{", + indent( + concat([ + line, + group(concat([trim, "#if DEBUG"])), + line, + "alert(42);", + line, + group(concat([trim, "#endif"])) + ]) + ), + line, + "}" + ]) + ), + `function() +{ +#if DEBUG + alert(42); +#endif +}` + ], + [ + "ignores trimmed characters when fitting the line", + group(concat(["hello ", " ", trim, line, "world!"])), + "hello world!" + ] + ])("%s", (_, doc, expected) => { + const result = printDocToString(doc, { printWidth: 12, tabWidth: 2 }); + + expect(result).toBeDefined(); + expect(result.formatted).toEqual(expected); + }); +});