diff --git a/src/document/printer.js b/src/document/printer.js index 004498080..32f9590f7 100644 --- a/src/document/printer.js +++ b/src/document/printer.js @@ -32,7 +32,7 @@ const MODE_FLAT = Symbol("MODE_FLAT"); const CURSOR_PLACEHOLDER = Symbol("cursor"); -const IS_MUTABLE_FILL = Symbol("IS_MUTABLE_FILL"); +const DOC_FILL_PRINTED_LENGTH = Symbol("DOC_FILL_PRINTED_LENGTH"); function rootIndent() { return { value: "", length: 0, queue: [] }; @@ -463,12 +463,15 @@ function printDocToString(doc, options) { case DOC_TYPE_FILL: { const rem = width - pos; + const offset = doc[DOC_FILL_PRINTED_LENGTH] ?? 0; const { parts } = doc; - if (parts.length === 0) { + const length = parts.length - offset; + if (length === 0) { break; } - const [content, whitespace] = parts; + const content = parts[offset + 0]; + const whitespace = parts[offset + 1]; /** @type {Command} */ const contentFlatCmd = { ind, mode: MODE_FLAT, doc: content }; /** @type {Command} */ @@ -482,7 +485,7 @@ function printDocToString(doc, options) { true, ); - if (parts.length === 1) { + if (length === 1) { if (contentFits) { cmds.push(contentFlatCmd); } else { @@ -496,7 +499,7 @@ function printDocToString(doc, options) { /** @type {Command} */ const whitespaceBreakCmd = { ind, mode: MODE_BREAK, doc: whitespace }; - if (parts.length === 2) { + if (length === 2) { if (contentFits) { cmds.push(whitespaceFlatCmd, contentFlatCmd); } else { @@ -505,26 +508,14 @@ function printDocToString(doc, options) { break; } - const secondContent = parts[2]; - - // At this point we've handled the first pair (context, separator) - // and will create a new *mutable* fill doc for the rest of the content. - // Copying all the elements to a new array would make this algorithm quadratic, - // which is unusable for large arrays (e.g. large texts in JSX). - // https://github.com/prettier/prettier/issues/3263#issuecomment-344275152 - let remainingDoc = doc; - if (doc[IS_MUTABLE_FILL]) { - parts.splice(0, 2); - } else { - remainingDoc = { - ...doc, - parts: parts.slice(2), - [IS_MUTABLE_FILL]: true, - }; - } + const secondContent = parts[offset + 2]; /** @type {Command} */ - const remainingCmd = { ind, mode, doc: remainingDoc }; + const remainingCmd = { + ind, + mode, + doc: { ...doc, [DOC_FILL_PRINTED_LENGTH]: offset + 2 }, + }; /** @type {Command} */ const firstAndSecondContentFlatCmd = { diff --git a/tests/unit/doc-printer.js b/tests/unit/doc-printer.js index bf4dbf81d..97a94f387 100644 --- a/tests/unit/doc-printer.js +++ b/tests/unit/doc-printer.js @@ -40,6 +40,31 @@ test("`printDocToString` should not manipulate docs", () => { const lines = formatted.split("\n"); expect(lines.length).toBeGreaterThan(1000); - expect(endTime - startTime).toBeLessThan(1000); + expect(endTime - startTime).toBeLessThan(100); + } +}); + +describe("`printDocToString` has linear time complexity at most to print fill()", () => { + const baseSize = 3_000; + const relativeMargin = 0.3; + const baseTime = time(makeFill(baseSize)); + test.each([10_000, 20_000, 40_000])("numWords=%d", (numWords) => { + const doc = makeFill(numWords); + const ellapsed = time(doc); + const ratio = numWords / baseSize; + expect(ellapsed).toBeLessThan(baseTime * ratio * (1 + relativeMargin)); + }); + + function makeFill(numWords) { + const WORD = "word"; + const parts = Array.from({ length: numWords }, () => WORD); + return fill(join(line, parts)); + } + + function time(doc) { + const printOptions = { printWidth: 40, tabWidth: 2 }; + const start = performance.now(); + printDocToString(doc, printOptions); + return performance.now() - start; } });