Revision control

Copy as Markdown

// Copyright (C) 2021 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
#include "qqmldomlinewriter_p.h"
#include <QtCore/QCoreApplication>
#include <QtCore/QRegularExpression>
QT_BEGIN_NAMESPACE
namespace QQmlJS {
namespace Dom {
LineWriter::LineWriter(
const SinkF &innerSink, const QString &fileName, const LineWriterOptions &options,
int lineNr, int columnNr, int utf16Offset, const QString &currentLine)
: m_innerSinks({ innerSink }),
m_fileName(fileName),
m_lineNr(lineNr),
m_columnNr(columnNr),
m_currentColumnNr(columnNr),
m_utf16Offset(utf16Offset),
m_currentLine(currentLine),
m_options(options)
{
}
LineWriter &LineWriter::ensureNewline(int nNewline, TextAddType t)
{
int nToAdd = nNewline;
if (nToAdd <= 0)
return *this;
if (m_currentLine.trimmed().isEmpty()) {
--nToAdd;
if (m_committedEmptyLines >= unsigned(nToAdd))
return *this;
nToAdd -= m_committedEmptyLines;
}
for (int i = 0; i < nToAdd; ++i)
write(u"\n", t);
return *this;
}
LineWriter &LineWriter::ensureSpace(TextAddType t)
{
if (!m_currentLine.isEmpty() && !m_currentLine.at(m_currentLine.size() - 1).isSpace())
write(u" ", t);
return *this;
}
LineWriter &LineWriter::ensureSemicolon(TextAddType t)
{
if (!m_currentLine.isEmpty() && m_currentLine.back() != u';')
write(u";", t);
return *this;
}
LineWriter &LineWriter::ensureSpace(QStringView space, TextAddType t)
{
int tabSize = m_options.formatOptions.tabSize;
IndentInfo ind(space, tabSize);
auto cc = counter();
if (ind.nNewlines > 0)
ensureNewline(ind.nNewlines, t);
if (cc != counter() || m_currentLine.isEmpty()
|| !m_currentLine.at(m_currentLine.size() - 1).isSpace())
write(ind.trailingString, t);
else {
int len = m_currentLine.size();
int i = len;
while (i != 0 && m_currentLine.at(i - 1).isSpace())
--i;
QStringView trailingSpace = QStringView(m_currentLine).mid(i, len - i);
int trailingSpaceStartColumn =
IndentInfo(QStringView(m_currentLine).mid(0, i), tabSize, m_columnNr).column;
IndentInfo indExisting(trailingSpace, tabSize, trailingSpaceStartColumn);
if (trailingSpaceStartColumn != 0)
ind = IndentInfo(space, tabSize, trailingSpaceStartColumn);
if (i == 0) {
if (indExisting.column < ind.column) {
m_currentColumnNr += ind.trailingString.size() - trailingSpace.size();
m_currentLine.replace(
i, len - i, ind.trailingString.toString()); // invalidates most QStringViews
lineChanged();
}
} else if (indExisting.column < ind.column) { // use just spaces if not at start of a line
write(QStringLiteral(u" ").repeated(ind.column - indExisting.column), t);
}
}
return *this;
}
QString LineWriter::eolToWrite() const
{
switch (m_options.lineEndings) {
case LineWriterOptions::LineEndings::Unix:
return QStringLiteral(u"\n");
case LineWriterOptions::LineEndings::Windows:
return QStringLiteral(u"\r\n");
case LineWriterOptions::LineEndings::OldMacOs:
return QStringLiteral(u"\r");
}
Q_ASSERT(false);
return QStringLiteral(u"\n");
}
template<typename String, typename ...Args>
static QRegularExpressionMatch matchHelper(QRegularExpression &re, String &&s, Args &&...args)
{
return re.matchView(s, args...);
}
LineWriter &LineWriter::write(QStringView v, TextAddType tAdd)
{
QString eol;
// split multiple lines
static QRegularExpression eolRe(QLatin1String(
"(\r?\n|\r)")); // does not support split of \r and \n for windows style line endings
QRegularExpressionMatch m = matchHelper(eolRe, v);
if (m.hasMatch()) {
// add line by line
auto i = m.capturedStart(1);
auto iEnd = m.capturedEnd(1);
eol = eolToWrite();
// offset change (eol used vs input) cannot affect things,
// because we cannot have already opened or closed a PendingSourceLocation
if (iEnd < v.size()) {
write(v.mid(0, iEnd));
m = matchHelper(eolRe, v, iEnd);
while (m.hasMatch()) {
write(v.mid(iEnd, m.capturedEnd(1) - iEnd));
iEnd = m.capturedEnd(1);
m = matchHelper(eolRe, v, iEnd);
}
if (iEnd < v.size())
write(v.mid(iEnd, v.size() - iEnd));
return *this;
}
QStringView toAdd = v.mid(0, i);
if (!toAdd.trimmed().isEmpty())
textAddCallback(tAdd);
m_counter += i;
m_currentLine.append(toAdd);
m_currentColumnNr +=
IndentInfo(toAdd, m_options.formatOptions.tabSize, m_currentColumnNr).column;
lineChanged();
} else {
if (!v.trimmed().isEmpty())
textAddCallback(tAdd);
m_counter += v.size();
m_currentLine.append(v);
m_currentColumnNr +=
IndentInfo(v, m_options.formatOptions.tabSize, m_currentColumnNr).column;
lineChanged();
}
if (!eol.isEmpty()
|| (m_options.maxLineLength > 0 && m_currentColumnNr > m_options.maxLineLength)) {
reindentAndSplit(eol);
}
return *this;
}
void LineWriter::flush()
{
if (m_currentLine.size() > 0)
commitLine(QString());
}
void LineWriter::eof(bool shouldEnsureNewline)
{
if (shouldEnsureNewline)
ensureNewline();
reindentAndSplit(QString(), true);
}
SourceLocation LineWriter::committedLocation() const
{
return SourceLocation(m_utf16Offset, 0, m_lineNr, m_lineUtf16Offset);
}
int LineWriter::addTextAddCallback(std::function<bool(LineWriter &, TextAddType)> callback)
{
int nextId = ++m_lastCallbackId;
Q_ASSERT(nextId != 0);
if (callback)
m_textAddCallbacks.insert(nextId, callback);
return nextId;
}
int LineWriter::addNewlinesAutospacerCallback(int nLines)
{
return addTextAddCallback([nLines](LineWriter &self, TextAddType t) {
if (t == TextAddType::Normal) {
quint32 c = self.counter();
QString spacesToPreserve;
bool spaceOnly = QStringView(self.m_currentLine).trimmed().isEmpty();
if (spaceOnly && !self.m_currentLine.isEmpty())
spacesToPreserve = self.m_currentLine;
self.ensureNewline(nLines, LineWriter::TextAddType::Extra);
if (self.counter() != c && !spacesToPreserve.isEmpty())
self.write(spacesToPreserve, TextAddType::Extra);
return false;
} else {
return true;
}
});
}
void LineWriter::setLineIndent(int indentAmount)
{
int startNonSpace = 0;
while (startNonSpace < m_currentLine.size() && m_currentLine.at(startNonSpace).isSpace())
++startNonSpace;
int oldColumn = column(startNonSpace);
if (indentAmount >= 0) {
QString indent;
if (m_options.formatOptions.useTabs) {
indent = QStringLiteral(u"\t").repeated(indentAmount / m_options.formatOptions.tabSize)
+ QStringLiteral(u" ").repeated(indentAmount % m_options.formatOptions.tabSize);
} else {
indent = QStringLiteral(u" ").repeated(indentAmount);
}
if (indent != m_currentLine.mid(0, startNonSpace)) {
quint32 colChange = indentAmount - oldColumn;
m_currentColumnNr += colChange;
m_currentLine = indent + m_currentLine.mid(startNonSpace);
m_currentColumnNr = column(m_currentLine.size());
lineChanged();
}
}
}
void LineWriter::handleTrailingSpace(LineWriterOptions::TrailingSpace trailingSpace)
{
switch (trailingSpace) {
case LineWriterOptions::TrailingSpace::Preserve:
break;
case LineWriterOptions::TrailingSpace::Remove: {
int lastNonSpace = m_currentLine.size();
while (lastNonSpace > 0 && m_currentLine.at(lastNonSpace - 1).isSpace())
--lastNonSpace;
if (lastNonSpace != m_currentLine.size()) {
m_currentLine = m_currentLine.mid(0, lastNonSpace);
m_currentColumnNr =
column(m_currentLine.size()); // to be extra accurate in the potential split
lineChanged();
}
} break;
}
}
void LineWriter::reindentAndSplit(const QString &eol, bool eof)
{
// maybe write out
if (!eol.isEmpty() || eof) {
handleTrailingSpace(m_options.codeTrailingSpace);
commitLine(eol);
}
}
SourceLocation LineWriter::currentSourceLocation() const
{
return SourceLocation(m_utf16Offset + m_currentLine.size(), 0, m_lineNr,
m_lineUtf16Offset + m_currentLine.size());
}
int LineWriter::column(int index)
{
if (index > m_currentLine.size())
index = m_currentLine.size();
IndentInfo iInfo(QStringView(m_currentLine).mid(0, index), m_options.formatOptions.tabSize,
m_columnNr);
return iInfo.column;
}
void LineWriter::textAddCallback(LineWriter::TextAddType t)
{
if (m_textAddCallbacks.isEmpty())
return;
int iNow = (--m_textAddCallbacks.end()).key() + 1;
while (true) {
auto it = m_textAddCallbacks.lowerBound(iNow);
if (it == m_textAddCallbacks.begin())
break;
--it;
iNow = it.key();
if (!it.value()(*this, t))
m_textAddCallbacks.erase(it);
}
}
void LineWriter::commitLine(const QString &eol, TextAddType tType, int untilChar)
{
if (untilChar == -1)
untilChar = m_currentLine.size();
bool isSpaceOnly = QStringView(m_currentLine).mid(0, untilChar).trimmed().isEmpty();
bool isEmptyNewline = !eol.isEmpty() && isSpaceOnly;
// update position, lineNr,...
// write out
for (SinkF &sink : m_innerSinks)
sink(m_currentLine.mid(0, untilChar));
m_utf16Offset += untilChar;
if (!eol.isEmpty()) {
m_utf16Offset += eol.size();
for (SinkF &sink : m_innerSinks)
sink(eol);
++m_lineNr;
m_columnNr = 0;
m_lineUtf16Offset = 0;
} else {
m_columnNr = column(untilChar);
m_lineUtf16Offset += untilChar;
}
if (untilChar == m_currentLine.size()) {
willCommit();
m_currentLine.clear();
} else {
QString nextLine = m_currentLine.mid(untilChar);
m_currentLine = m_currentLine.mid(0, untilChar);
lineChanged();
willCommit();
m_currentLine = nextLine;
}
lineChanged();
m_currentColumnNr = column(m_currentLine.size());
TextAddType notifyType = tType;
switch (tType) {
case TextAddType::Normal:
if (eol.isEmpty())
notifyType = TextAddType::PartialCommit;
else
notifyType = TextAddType::Newline;
break;
case TextAddType::Extra:
if (eol.isEmpty())
notifyType = TextAddType::NewlineExtra;
else
notifyType = TextAddType::PartialCommit;
break;
case TextAddType::Newline:
case TextAddType::NewlineSplit:
case TextAddType::NewlineExtra:
case TextAddType::PartialCommit:
case TextAddType::Eof:
break;
}
if (isEmptyNewline)
++m_committedEmptyLines;
else if (!isSpaceOnly)
m_committedEmptyLines = 0;
// notify
textAddCallback(notifyType);
}
} // namespace Dom
} // namespace QQmlJS
QT_END_NAMESPACE
#include "moc_qqmldomlinewriter_p.cpp"