| 1 | // Copyright (C) 2021 The Qt Company Ltd. |
| 2 | // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only |
| 3 | |
| 4 | #include "qqmldomlinewriter_p.h" |
| 5 | #include <QtCore/QCoreApplication> |
| 6 | #include <QtCore/QRegularExpression> |
| 7 | |
| 8 | QT_BEGIN_NAMESPACE |
| 9 | namespace QQmlJS { |
| 10 | namespace Dom { |
| 11 | |
| 12 | LineWriter::LineWriter( |
| 13 | const SinkF &innerSink, const QString &fileName, const LineWriterOptions &options, |
| 14 | int lineNr, int columnNr, int utf16Offset, const QString ¤tLine) |
| 15 | : m_innerSinks({ innerSink }), |
| 16 | m_fileName(fileName), |
| 17 | m_lineNr(lineNr), |
| 18 | m_columnNr(columnNr), |
| 19 | m_currentColumnNr(columnNr), |
| 20 | m_utf16Offset(utf16Offset), |
| 21 | m_currentLine(currentLine), |
| 22 | m_options(options) |
| 23 | { |
| 24 | } |
| 25 | |
| 26 | LineWriter &LineWriter::ensureNewline(int nNewline, TextAddType t) |
| 27 | { |
| 28 | int nToAdd = nNewline; |
| 29 | if (nToAdd <= 0) |
| 30 | return *this; |
| 31 | if (m_currentLine.trimmed().isEmpty()) { |
| 32 | --nToAdd; |
| 33 | if (m_committedEmptyLines >= unsigned(nToAdd)) |
| 34 | return *this; |
| 35 | nToAdd -= m_committedEmptyLines; |
| 36 | } |
| 37 | for (int i = 0; i < nToAdd; ++i) |
| 38 | write(v: u"\n" , tType: t); |
| 39 | return *this; |
| 40 | } |
| 41 | |
| 42 | LineWriter &LineWriter::ensureSpace(TextAddType t) |
| 43 | { |
| 44 | if (!m_currentLine.isEmpty() && !m_currentLine.at(i: m_currentLine.size() - 1).isSpace()) |
| 45 | write(v: u" " , tType: t); |
| 46 | return *this; |
| 47 | } |
| 48 | |
| 49 | LineWriter &LineWriter::ensureSemicolon(TextAddType t) |
| 50 | { |
| 51 | if (!m_currentLine.isEmpty() && m_currentLine.back() != u';') |
| 52 | write(v: u";" , tType: t); |
| 53 | return *this; |
| 54 | } |
| 55 | |
| 56 | LineWriter &LineWriter::ensureSpace(QStringView space, TextAddType t) |
| 57 | { |
| 58 | int tabSize = m_options.formatOptions.tabSize; |
| 59 | IndentInfo ind(space, tabSize); |
| 60 | auto cc = counter(); |
| 61 | if (ind.nNewlines > 0) |
| 62 | ensureNewline(nNewline: ind.nNewlines, t); |
| 63 | if (cc != counter() || m_currentLine.isEmpty() |
| 64 | || !m_currentLine.at(i: m_currentLine.size() - 1).isSpace()) |
| 65 | write(v: ind.trailingString, tType: t); |
| 66 | else { |
| 67 | int len = m_currentLine.size(); |
| 68 | int i = len; |
| 69 | while (i != 0 && m_currentLine.at(i: i - 1).isSpace()) |
| 70 | --i; |
| 71 | QStringView trailingSpace = QStringView(m_currentLine).mid(pos: i, n: len - i); |
| 72 | int trailingSpaceStartColumn = |
| 73 | IndentInfo(QStringView(m_currentLine).mid(pos: 0, n: i), tabSize, m_columnNr).column; |
| 74 | IndentInfo indExisting(trailingSpace, tabSize, trailingSpaceStartColumn); |
| 75 | if (trailingSpaceStartColumn != 0) |
| 76 | ind = IndentInfo(space, tabSize, trailingSpaceStartColumn); |
| 77 | if (i == 0) { |
| 78 | if (indExisting.column < ind.column) { |
| 79 | m_currentColumnNr += ind.trailingString.size() - trailingSpace.size(); |
| 80 | m_currentLine.replace( |
| 81 | i, len: len - i, after: ind.trailingString.toString()); // invalidates most QStringViews |
| 82 | lineChanged(); |
| 83 | } |
| 84 | } else if (indExisting.column < ind.column) { // use just spaces if not at start of a line |
| 85 | write(QStringLiteral(u" " ).repeated(times: ind.column - indExisting.column), tType: t); |
| 86 | } |
| 87 | } |
| 88 | return *this; |
| 89 | } |
| 90 | |
| 91 | QString LineWriter::eolToWrite() const |
| 92 | { |
| 93 | switch (m_options.lineEndings) { |
| 94 | case LineWriterOptions::LineEndings::Unix: |
| 95 | return QStringLiteral(u"\n" ); |
| 96 | case LineWriterOptions::LineEndings::Windows: |
| 97 | return QStringLiteral(u"\r\n" ); |
| 98 | case LineWriterOptions::LineEndings::OldMacOs: |
| 99 | return QStringLiteral(u"\r" ); |
| 100 | } |
| 101 | Q_ASSERT(false); |
| 102 | return QStringLiteral(u"\n" ); |
| 103 | } |
| 104 | |
| 105 | template<typename String, typename ...Args> |
| 106 | static QRegularExpressionMatch matchHelper(QRegularExpression &re, String &&s, Args &&...args) |
| 107 | { |
| 108 | return re.matchView(subjectView: s, offset: args...); |
| 109 | } |
| 110 | |
| 111 | LineWriter &LineWriter::write(QStringView v, TextAddType tAdd) |
| 112 | { |
| 113 | QString eol; |
| 114 | // split multiple lines |
| 115 | static QRegularExpression eolRe(QLatin1String( |
| 116 | "(\r?\n|\r)" )); // does not support split of \r and \n for windows style line endings |
| 117 | QRegularExpressionMatch m = matchHelper(re&: eolRe, s&: v); |
| 118 | if (m.hasMatch()) { |
| 119 | // add line by line |
| 120 | auto i = m.capturedStart(nth: 1); |
| 121 | auto iEnd = m.capturedEnd(nth: 1); |
| 122 | eol = eolToWrite(); |
| 123 | // offset change (eol used vs input) cannot affect things, |
| 124 | // because we cannot have already opened or closed a PendingSourceLocation |
| 125 | if (iEnd < v.size()) { |
| 126 | write(v: v.mid(pos: 0, n: iEnd)); |
| 127 | m = matchHelper(re&: eolRe, s&: v, args&: iEnd); |
| 128 | while (m.hasMatch()) { |
| 129 | write(v: v.mid(pos: iEnd, n: m.capturedEnd(nth: 1) - iEnd)); |
| 130 | iEnd = m.capturedEnd(nth: 1); |
| 131 | m = matchHelper(re&: eolRe, s&: v, args&: iEnd); |
| 132 | } |
| 133 | if (iEnd < v.size()) |
| 134 | write(v: v.mid(pos: iEnd, n: v.size() - iEnd)); |
| 135 | return *this; |
| 136 | } |
| 137 | QStringView toAdd = v.mid(pos: 0, n: i); |
| 138 | if (!toAdd.trimmed().isEmpty()) |
| 139 | textAddCallback(t: tAdd); |
| 140 | m_counter += i; |
| 141 | m_currentLine.append(v: toAdd); |
| 142 | m_currentColumnNr += |
| 143 | IndentInfo(toAdd, m_options.formatOptions.tabSize, m_currentColumnNr).column; |
| 144 | lineChanged(); |
| 145 | } else { |
| 146 | if (!v.trimmed().isEmpty()) |
| 147 | textAddCallback(t: tAdd); |
| 148 | m_counter += v.size(); |
| 149 | m_currentLine.append(v); |
| 150 | m_currentColumnNr += |
| 151 | IndentInfo(v, m_options.formatOptions.tabSize, m_currentColumnNr).column; |
| 152 | lineChanged(); |
| 153 | } |
| 154 | if (!eol.isEmpty() |
| 155 | || (m_options.maxLineLength > 0 && m_currentColumnNr > m_options.maxLineLength)) { |
| 156 | reindentAndSplit(eol); |
| 157 | } |
| 158 | return *this; |
| 159 | } |
| 160 | |
| 161 | void LineWriter::flush() |
| 162 | { |
| 163 | if (m_currentLine.size() > 0) |
| 164 | commitLine(eol: QString()); |
| 165 | } |
| 166 | |
| 167 | void LineWriter::eof(bool shouldEnsureNewline) |
| 168 | { |
| 169 | if (shouldEnsureNewline) |
| 170 | ensureNewline(); |
| 171 | reindentAndSplit(eol: QString(), eof: true); |
| 172 | } |
| 173 | |
| 174 | SourceLocation LineWriter::committedLocation() const |
| 175 | { |
| 176 | return SourceLocation(m_utf16Offset, 0, m_lineNr, m_lineUtf16Offset); |
| 177 | } |
| 178 | |
| 179 | int LineWriter::addTextAddCallback(std::function<bool(LineWriter &, TextAddType)> callback) |
| 180 | { |
| 181 | int nextId = ++m_lastCallbackId; |
| 182 | Q_ASSERT(nextId != 0); |
| 183 | if (callback) |
| 184 | m_textAddCallbacks.insert(key: nextId, value: callback); |
| 185 | return nextId; |
| 186 | } |
| 187 | |
| 188 | int LineWriter::addNewlinesAutospacerCallback(int nLines) |
| 189 | { |
| 190 | return addTextAddCallback(callback: [nLines](LineWriter &self, TextAddType t) { |
| 191 | if (t == TextAddType::Normal) { |
| 192 | quint32 c = self.counter(); |
| 193 | QString spacesToPreserve; |
| 194 | bool spaceOnly = QStringView(self.m_currentLine).trimmed().isEmpty(); |
| 195 | if (spaceOnly && !self.m_currentLine.isEmpty()) |
| 196 | spacesToPreserve = self.m_currentLine; |
| 197 | self.ensureNewline(nNewline: nLines, t: LineWriter::TextAddType::Extra); |
| 198 | if (self.counter() != c && !spacesToPreserve.isEmpty()) |
| 199 | self.write(v: spacesToPreserve, tAdd: TextAddType::Extra); |
| 200 | return false; |
| 201 | } else { |
| 202 | return true; |
| 203 | } |
| 204 | }); |
| 205 | } |
| 206 | |
| 207 | void LineWriter::setLineIndent(int indentAmount) |
| 208 | { |
| 209 | int startNonSpace = 0; |
| 210 | while (startNonSpace < m_currentLine.size() && m_currentLine.at(i: startNonSpace).isSpace()) |
| 211 | ++startNonSpace; |
| 212 | int oldColumn = column(localIndex: startNonSpace); |
| 213 | if (indentAmount >= 0) { |
| 214 | QString indent; |
| 215 | if (m_options.formatOptions.useTabs) { |
| 216 | indent = QStringLiteral(u"\t" ).repeated(times: indentAmount / m_options.formatOptions.tabSize) |
| 217 | + QStringLiteral(u" " ).repeated(times: indentAmount % m_options.formatOptions.tabSize); |
| 218 | } else { |
| 219 | indent = QStringLiteral(u" " ).repeated(times: indentAmount); |
| 220 | } |
| 221 | if (indent != m_currentLine.mid(position: 0, n: startNonSpace)) { |
| 222 | quint32 colChange = indentAmount - oldColumn; |
| 223 | m_currentColumnNr += colChange; |
| 224 | m_currentLine = indent + m_currentLine.mid(position: startNonSpace); |
| 225 | m_currentColumnNr = column(localIndex: m_currentLine.size()); |
| 226 | lineChanged(); |
| 227 | } |
| 228 | } |
| 229 | } |
| 230 | |
| 231 | void LineWriter::handleTrailingSpace(LineWriterOptions::TrailingSpace trailingSpace) |
| 232 | { |
| 233 | switch (trailingSpace) { |
| 234 | case LineWriterOptions::TrailingSpace::Preserve: |
| 235 | break; |
| 236 | case LineWriterOptions::TrailingSpace::Remove: { |
| 237 | int lastNonSpace = m_currentLine.size(); |
| 238 | while (lastNonSpace > 0 && m_currentLine.at(i: lastNonSpace - 1).isSpace()) |
| 239 | --lastNonSpace; |
| 240 | if (lastNonSpace != m_currentLine.size()) { |
| 241 | m_currentLine = m_currentLine.mid(position: 0, n: lastNonSpace); |
| 242 | m_currentColumnNr = |
| 243 | column(localIndex: m_currentLine.size()); // to be extra accurate in the potential split |
| 244 | lineChanged(); |
| 245 | } |
| 246 | } break; |
| 247 | } |
| 248 | } |
| 249 | |
| 250 | void LineWriter::reindentAndSplit(const QString &eol, bool eof) |
| 251 | { |
| 252 | // maybe write out |
| 253 | if (!eol.isEmpty() || eof) { |
| 254 | handleTrailingSpace(trailingSpace: m_options.codeTrailingSpace); |
| 255 | commitLine(eol); |
| 256 | } |
| 257 | } |
| 258 | |
| 259 | SourceLocation LineWriter::currentSourceLocation() const |
| 260 | { |
| 261 | return SourceLocation(m_utf16Offset + m_currentLine.size(), 0, m_lineNr, |
| 262 | m_lineUtf16Offset + m_currentLine.size()); |
| 263 | } |
| 264 | |
| 265 | int LineWriter::column(int index) |
| 266 | { |
| 267 | if (index > m_currentLine.size()) |
| 268 | index = m_currentLine.size(); |
| 269 | IndentInfo iInfo(QStringView(m_currentLine).mid(pos: 0, n: index), m_options.formatOptions.tabSize, |
| 270 | m_columnNr); |
| 271 | return iInfo.column; |
| 272 | } |
| 273 | |
| 274 | void LineWriter::textAddCallback(LineWriter::TextAddType t) |
| 275 | { |
| 276 | if (m_textAddCallbacks.isEmpty()) |
| 277 | return; |
| 278 | int iNow = (--m_textAddCallbacks.end()).key() + 1; |
| 279 | while (true) { |
| 280 | auto it = m_textAddCallbacks.lowerBound(key: iNow); |
| 281 | if (it == m_textAddCallbacks.begin()) |
| 282 | break; |
| 283 | --it; |
| 284 | iNow = it.key(); |
| 285 | if (!it.value()(*this, t)) |
| 286 | m_textAddCallbacks.erase(it); |
| 287 | } |
| 288 | } |
| 289 | |
| 290 | void LineWriter::commitLine(const QString &eol, TextAddType tType, int untilChar) |
| 291 | { |
| 292 | if (untilChar == -1) |
| 293 | untilChar = m_currentLine.size(); |
| 294 | bool isSpaceOnly = QStringView(m_currentLine).mid(pos: 0, n: untilChar).trimmed().isEmpty(); |
| 295 | bool isEmptyNewline = !eol.isEmpty() && isSpaceOnly; |
| 296 | // update position, lineNr,... |
| 297 | // write out |
| 298 | for (SinkF &sink : m_innerSinks) |
| 299 | sink(m_currentLine.mid(position: 0, n: untilChar)); |
| 300 | m_utf16Offset += untilChar; |
| 301 | if (!eol.isEmpty()) { |
| 302 | m_utf16Offset += eol.size(); |
| 303 | for (SinkF &sink : m_innerSinks) |
| 304 | sink(eol); |
| 305 | ++m_lineNr; |
| 306 | m_columnNr = 0; |
| 307 | m_lineUtf16Offset = 0; |
| 308 | } else { |
| 309 | m_columnNr = column(index: untilChar); |
| 310 | m_lineUtf16Offset += untilChar; |
| 311 | } |
| 312 | if (untilChar == m_currentLine.size()) { |
| 313 | willCommit(); |
| 314 | m_currentLine.clear(); |
| 315 | } else { |
| 316 | QString nextLine = m_currentLine.mid(position: untilChar); |
| 317 | m_currentLine = m_currentLine.mid(position: 0, n: untilChar); |
| 318 | lineChanged(); |
| 319 | willCommit(); |
| 320 | m_currentLine = nextLine; |
| 321 | } |
| 322 | lineChanged(); |
| 323 | m_currentColumnNr = column(index: m_currentLine.size()); |
| 324 | TextAddType notifyType = tType; |
| 325 | switch (tType) { |
| 326 | case TextAddType::Normal: |
| 327 | if (eol.isEmpty()) |
| 328 | notifyType = TextAddType::PartialCommit; |
| 329 | else |
| 330 | notifyType = TextAddType::Newline; |
| 331 | break; |
| 332 | case TextAddType::Extra: |
| 333 | if (eol.isEmpty()) |
| 334 | notifyType = TextAddType::NewlineExtra; |
| 335 | else |
| 336 | notifyType = TextAddType::PartialCommit; |
| 337 | break; |
| 338 | case TextAddType::Newline: |
| 339 | case TextAddType::NewlineSplit: |
| 340 | case TextAddType::NewlineExtra: |
| 341 | case TextAddType::PartialCommit: |
| 342 | case TextAddType::Eof: |
| 343 | break; |
| 344 | } |
| 345 | if (isEmptyNewline) |
| 346 | ++m_committedEmptyLines; |
| 347 | else if (!isSpaceOnly) |
| 348 | m_committedEmptyLines = 0; |
| 349 | // notify |
| 350 | textAddCallback(t: notifyType); |
| 351 | } |
| 352 | |
| 353 | } // namespace Dom |
| 354 | } // namespace QQmlJS |
| 355 | QT_END_NAMESPACE |
| 356 | |
| 357 | #include "moc_qqmldomlinewriter_p.cpp" |
| 358 | |