<commit_before>"use strict";

const diff = require("diff");

const {
  printer: { printDocToString },
  debug: { printDocToDebug },
} = require("../document");
const { getAlignmentSize } = require("../common/util");
const {
  guessEndOfLine,
  convertEndOfLineToChars,
} = 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");
const PLACEHOLDERS = {
  cursorOffset: "<<<PRETTIER_CURSOR>>>",
  rangeStart: "<<<PRETTIER_RANGE_START>>>",
  rangeEnd: "<<<PRETTIER_RANGE_END>>>",
};

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(text, opts, addAlignmentSize) {
  if (!text || !text.trim().length) {
    return { formatted: "", cursorOffset: 0 };
  }

  addAlignmentSize = addAlignmentSize || 0;

  const parsed = parser.parse(text, opts);
  const { ast } = parsed;
  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 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,
      };
    }

    // 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 };
  }

  return { formatted: result.formatted };
}

function formatRange(text, opts) {
  const parsed = parser.parse(text, opts);
  const { ast } = parsed;
  text = parsed.text;

  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: 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 =
      opts.cursorOffset + (rangeTrimmed.length - rangeString.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 =
    text.slice(0, rangeStart) + rangeTrimmed + text.slice(rangeEnd);
  if (opts.endOfLine !== "lf") {
    const eol = convertEndOfLineToChars(opts.endOfLine);
    if (cursorOffset >= 0 && eol === "\r\n") {
      const before = formatted.slice(0, cursorOffset);
      const match = before.match(/\n/g);
      if (match) {
        cursorOffset += match.length;
      }
    }

    formatted = formatted.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.includes("\r")) {
    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.charAt(0) === BOM;
  if (hasUnicodeBOM) {
    text = text.slice(1);
    if (hasCursor) {
      opts.cursorOffset--;
    }
    if (hasRangeStart) {
      opts.rangeStart--;
    }
    if (hasRangeEnd) {
      opts.rangeEnd--;
    }
  }

  if (!hasCursor) {
    opts.cursorOffset = -1;
  }
  /* istanbul ignore next */<commit_msg>Improve internal range calculate (#9012)<commit_after>"use strict";

const diff = require("diff");

const {
  printer: { printDocToString },
  debug: { printDocToDebug },
} = require("../document");
const { getAlignmentSize } = require("../common/util");
const {
  guessEndOfLine,
  convertEndOfLineToChars,
} = 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(text, opts, addAlignmentSize) {
  if (!text || !text.trim().length) {
    return { formatted: "", cursorOffset: 0 };
  }

  addAlignmentSize = addAlignmentSize || 0;

  const parsed = parser.parse(text, opts);
  const { ast } = parsed;
  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 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,
      };
    }

    // 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 };
  }

  return { formatted: result.formatted };
}

function formatRange(text, opts) {
  const parsed = parser.parse(text, opts);
  const { ast } = parsed;
  text = parsed.text;

  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: 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 =
      opts.cursorOffset + (rangeTrimmed.length - rangeString.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 =
    text.slice(0, rangeStart) + rangeTrimmed + text.slice(rangeEnd);
  if (opts.endOfLine !== "lf") {
    const eol = convertEndOfLineToChars(opts.endOfLine);
    if (cursorOffset >= 0 && eol === "\r\n") {
      const before = formatted.slice(0, cursorOffset);
      const match = before.match(/\n/g);
      if (match) {
        cursorOffset += match.length;
      }
    }

    formatted = formatted.replace(/\n/g, eol);
  }

  return { formatted, cursorOffset };
}

function countCrlf(text) {
  const match = text.match(/\r\n/g);
  return match ? match.length : 0;
}

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.includes("\r")) {
    if (hasCursor) {
      opts.cursorOffset -= countCrlf(text.slice(0, opts.cursorOffset));
    }
    if (hasRangeStart) {
      opts.rangeStart -= countCrlf(text.slice(0, opts.rangeStart));
    }
    if (hasRangeEnd) {
      opts.rangeEnd -= countCrlf(text.slice(0, opts.rangeEnd));
    }

    text = text.replace(/\r\n?/g, "\n");
  }

  const hasUnicodeBOM = text.charAt(0) === BOM;
  if (hasUnicodeBOM) {
    text = text.slice(1);
    if (hasCursor) {
      opts.cursorOffset--;
    }
    if (hasRangeStart) {
      opts.rangeStart--;
    }
    if (hasRangeEnd) {
      opts.rangeEnd--;
    }
  }

  if (!hasCursor) {
    opts.cursorOffset = -1;
  }
  /* istanbul ignore next */
{'fraction_non_alphanumeric': 0.12220421393841167, 'fraction_numerical': 0.0038897893030794164, 'mean_word_length': 3.0961232076473713, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 31, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '2799652', 'n_tokens_mistral': 4839, 'n_tokens_neox': 4661, 'n_words': 1159}