| 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 | quint32 PendingSourceLocation::utf16Start() const |
| 13 | { |
| 14 | return value.offset; |
| 15 | } |
| 16 | |
| 17 | quint32 PendingSourceLocation::utf16End() const |
| 18 | { |
| 19 | return value.offset + value.length; |
| 20 | } |
| 21 | |
| 22 | void PendingSourceLocation::changeAtOffset(quint32 offset, qint32 change, qint32 colChange, |
| 23 | qint32 lineChange) |
| 24 | { |
| 25 | if (offset < utf16Start()) { |
| 26 | if (change < 0 && offset - change >= utf16Start()) { |
| 27 | int c1 = offset - utf16Start(); |
| 28 | int c2 = offset - change - utf16Start(); |
| 29 | change = c1; |
| 30 | if (value.length < quint32(c2)) |
| 31 | value.length = 0; |
| 32 | else |
| 33 | value.length -= c2; |
| 34 | } |
| 35 | value.offset += change; |
| 36 | value.startColumn += colChange; |
| 37 | value.startLine += lineChange; |
| 38 | } else if (offset < utf16End()) { |
| 39 | if (change < 0 && offset - change > utf16End()) |
| 40 | change = offset - utf16End(); |
| 41 | value.length += change; |
| 42 | } |
| 43 | } |
| 44 | |
| 45 | void PendingSourceLocation::commit() |
| 46 | { |
| 47 | if (toUpdate) |
| 48 | *toUpdate = value; |
| 49 | if (updater) |
| 50 | updater(value); |
| 51 | } |
| 52 | |
| 53 | LineWriter::LineWriter( |
| 54 | const SinkF &innerSink, const QString &fileName, const LineWriterOptions &options, |
| 55 | int lineNr, int columnNr, int utf16Offset, const QString ¤tLine) |
| 56 | : m_innerSinks({ innerSink }), |
| 57 | m_fileName(fileName), |
| 58 | m_lineNr(lineNr), |
| 59 | m_columnNr(columnNr), |
| 60 | m_currentColumnNr(columnNr), |
| 61 | m_utf16Offset(utf16Offset), |
| 62 | m_currentLine(currentLine), |
| 63 | m_options(options) |
| 64 | { |
| 65 | } |
| 66 | |
| 67 | LineWriter &LineWriter::ensureNewline(int nNewline, TextAddType t) |
| 68 | { |
| 69 | int nToAdd = nNewline; |
| 70 | if (nToAdd <= 0) |
| 71 | return *this; |
| 72 | if (m_currentLine.trimmed().isEmpty()) { |
| 73 | --nToAdd; |
| 74 | if (m_committedEmptyLines >= unsigned(nToAdd)) |
| 75 | return *this; |
| 76 | nToAdd -= m_committedEmptyLines; |
| 77 | } |
| 78 | for (int i = 0; i < nToAdd; ++i) |
| 79 | write(v: u"\n" , tType: t); |
| 80 | return *this; |
| 81 | } |
| 82 | |
| 83 | LineWriter &LineWriter::ensureSpace(TextAddType t) |
| 84 | { |
| 85 | if (!m_currentLine.isEmpty() && !m_currentLine.at(i: m_currentLine.size() - 1).isSpace()) |
| 86 | write(v: u" " , tType: t); |
| 87 | return *this; |
| 88 | } |
| 89 | |
| 90 | LineWriter &LineWriter::ensureSpace(QStringView space, TextAddType t) |
| 91 | { |
| 92 | int tabSize = m_options.formatOptions.tabSize; |
| 93 | IndentInfo ind(space, tabSize); |
| 94 | auto cc = counter(); |
| 95 | if (ind.nNewlines > 0) |
| 96 | ensureNewline(nNewline: ind.nNewlines, t); |
| 97 | if (cc != counter() || m_currentLine.isEmpty() |
| 98 | || !m_currentLine.at(i: m_currentLine.size() - 1).isSpace()) |
| 99 | write(v: ind.trailingString, tType: t); |
| 100 | else { |
| 101 | int len = m_currentLine.size(); |
| 102 | int i = len; |
| 103 | while (i != 0 && m_currentLine.at(i: i - 1).isSpace()) |
| 104 | --i; |
| 105 | QStringView trailingSpace = QStringView(m_currentLine).mid(pos: i, n: len - i); |
| 106 | int trailingSpaceStartColumn = |
| 107 | IndentInfo(QStringView(m_currentLine).mid(pos: 0, n: i), tabSize, m_columnNr).column; |
| 108 | IndentInfo indExisting(trailingSpace, tabSize, trailingSpaceStartColumn); |
| 109 | if (trailingSpaceStartColumn != 0) |
| 110 | ind = IndentInfo(space, tabSize, trailingSpaceStartColumn); |
| 111 | if (i == 0) { |
| 112 | if (indExisting.column < ind.column) { |
| 113 | qint32 utf16Change = ind.trailingString.size() - trailingSpace.size(); |
| 114 | m_currentColumnNr += ind.trailingString.size() - trailingSpace.size(); |
| 115 | m_currentLine.replace( |
| 116 | i, len: len - i, after: ind.trailingString.toString()); // invalidates most QStringViews |
| 117 | changeAtOffset(offset: i, change: utf16Change, colChange: utf16Change, lineChange: 0); |
| 118 | lineChanged(); |
| 119 | } |
| 120 | } else if (indExisting.column < ind.column) { // use just spaces if not at start of a line |
| 121 | write(QStringLiteral(u" " ).repeated(times: ind.column - indExisting.column), tType: t); |
| 122 | } |
| 123 | } |
| 124 | return *this; |
| 125 | } |
| 126 | |
| 127 | QString LineWriter::eolToWrite() const |
| 128 | { |
| 129 | switch (m_options.lineEndings) { |
| 130 | case LineWriterOptions::LineEndings::Unix: |
| 131 | return QStringLiteral(u"\n" ); |
| 132 | case LineWriterOptions::LineEndings::Windows: |
| 133 | return QStringLiteral(u"\r\n" ); |
| 134 | case LineWriterOptions::LineEndings::OldMacOs: |
| 135 | return QStringLiteral(u"\r" ); |
| 136 | } |
| 137 | Q_ASSERT(false); |
| 138 | return QStringLiteral(u"\n" ); |
| 139 | } |
| 140 | |
| 141 | template<typename String, typename ...Args> |
| 142 | static QRegularExpressionMatch matchHelper(QRegularExpression &re, String &&s, Args &&...args) |
| 143 | { |
| 144 | return re.matchView(subjectView: s, offset: args...); |
| 145 | } |
| 146 | |
| 147 | LineWriter &LineWriter::write(QStringView v, TextAddType tAdd) |
| 148 | { |
| 149 | QString eol; |
| 150 | // split multiple lines |
| 151 | static QRegularExpression eolRe(QLatin1String( |
| 152 | "(\r?\n|\r)" )); // does not support split of \r and \n for windows style line endings |
| 153 | QRegularExpressionMatch m = matchHelper(re&: eolRe, s&: v); |
| 154 | if (m.hasMatch()) { |
| 155 | // add line by line |
| 156 | auto i = m.capturedStart(nth: 1); |
| 157 | auto iEnd = m.capturedEnd(nth: 1); |
| 158 | eol = eolToWrite(); |
| 159 | // offset change (eol used vs input) cannot affect things, |
| 160 | // because we cannot have already opened or closed a PendingSourceLocation |
| 161 | if (iEnd < v.size()) { |
| 162 | write(v: v.mid(pos: 0, n: iEnd)); |
| 163 | m = matchHelper(re&: eolRe, s&: v, args&: iEnd); |
| 164 | while (m.hasMatch()) { |
| 165 | write(v: v.mid(pos: iEnd, n: m.capturedEnd(nth: 1) - iEnd)); |
| 166 | iEnd = m.capturedEnd(nth: 1); |
| 167 | m = matchHelper(re&: eolRe, s&: v, args&: iEnd); |
| 168 | } |
| 169 | if (iEnd < v.size()) |
| 170 | write(v: v.mid(pos: iEnd, n: v.size() - iEnd)); |
| 171 | return *this; |
| 172 | } |
| 173 | QStringView toAdd = v.mid(pos: 0, n: i); |
| 174 | if (!toAdd.trimmed().isEmpty()) |
| 175 | textAddCallback(t: tAdd); |
| 176 | m_counter += i; |
| 177 | m_currentLine.append(v: toAdd); |
| 178 | m_currentColumnNr += |
| 179 | IndentInfo(toAdd, m_options.formatOptions.tabSize, m_currentColumnNr).column; |
| 180 | lineChanged(); |
| 181 | } else { |
| 182 | if (!v.trimmed().isEmpty()) |
| 183 | textAddCallback(t: tAdd); |
| 184 | m_counter += v.size(); |
| 185 | m_currentLine.append(v); |
| 186 | m_currentColumnNr += |
| 187 | IndentInfo(v, m_options.formatOptions.tabSize, m_currentColumnNr).column; |
| 188 | lineChanged(); |
| 189 | } |
| 190 | if (!eol.isEmpty() |
| 191 | || (m_options.maxLineLength > 0 && m_currentColumnNr > m_options.maxLineLength)) { |
| 192 | reindentAndSplit(eol); |
| 193 | } |
| 194 | return *this; |
| 195 | } |
| 196 | |
| 197 | void LineWriter::flush() |
| 198 | { |
| 199 | if (m_currentLine.size() > 0) |
| 200 | commitLine(eol: QString()); |
| 201 | } |
| 202 | |
| 203 | void LineWriter::eof(bool shouldEnsureNewline) |
| 204 | { |
| 205 | if (shouldEnsureNewline) |
| 206 | ensureNewline(); |
| 207 | reindentAndSplit(eol: QString(), eof: true); |
| 208 | } |
| 209 | |
| 210 | SourceLocation LineWriter::committedLocation() const |
| 211 | { |
| 212 | return SourceLocation(m_utf16Offset, 0, m_lineNr, m_lineUtf16Offset); |
| 213 | } |
| 214 | |
| 215 | PendingSourceLocationId LineWriter::startSourceLocation(SourceLocation *toUpdate) |
| 216 | { |
| 217 | PendingSourceLocation res; |
| 218 | res.id = ++m_lastSourceLocationId; |
| 219 | res.value = currentSourceLocation(); |
| 220 | res.toUpdate = toUpdate; |
| 221 | m_pendingSourceLocations.insert(key: res.id, value: res); |
| 222 | return res.id; |
| 223 | } |
| 224 | |
| 225 | PendingSourceLocationId LineWriter::startSourceLocation(std::function<void(SourceLocation)> updater) |
| 226 | { |
| 227 | PendingSourceLocation res; |
| 228 | res.id = ++m_lastSourceLocationId; |
| 229 | res.value = currentSourceLocation(); |
| 230 | res.updater = updater; |
| 231 | m_pendingSourceLocations.insert(key: res.id, value: res); |
| 232 | return res.id; |
| 233 | } |
| 234 | |
| 235 | void LineWriter::endSourceLocation(PendingSourceLocationId slId) |
| 236 | { |
| 237 | if (m_pendingSourceLocations.contains(key: slId)) { |
| 238 | auto &pLoc = m_pendingSourceLocations[slId]; |
| 239 | if (!pLoc.open) { |
| 240 | qWarning() << "Trying to close already closed PendingSourceLocation" << int(slId); |
| 241 | } |
| 242 | pLoc.open = false; |
| 243 | pLoc.value.length = m_utf16Offset + m_currentLine.size() - pLoc.value.offset; |
| 244 | } else { |
| 245 | qWarning() << "Trying to close non existing PendingSourceLocation" << int(slId); |
| 246 | } |
| 247 | } |
| 248 | |
| 249 | int LineWriter::addTextAddCallback(std::function<bool(LineWriter &, TextAddType)> callback) |
| 250 | { |
| 251 | int nextId = ++m_lastCallbackId; |
| 252 | Q_ASSERT(nextId != 0); |
| 253 | if (callback) |
| 254 | m_textAddCallbacks.insert(key: nextId, value: callback); |
| 255 | return nextId; |
| 256 | } |
| 257 | |
| 258 | int LineWriter::addNewlinesAutospacerCallback(int nLines) |
| 259 | { |
| 260 | return addTextAddCallback(callback: [nLines](LineWriter &self, TextAddType t) { |
| 261 | if (t == TextAddType::Normal) { |
| 262 | quint32 c = self.counter(); |
| 263 | QString spacesToPreserve; |
| 264 | bool spaceOnly = QStringView(self.m_currentLine).trimmed().isEmpty(); |
| 265 | if (spaceOnly && !self.m_currentLine.isEmpty()) |
| 266 | spacesToPreserve = self.m_currentLine; |
| 267 | self.ensureNewline(nNewline: nLines, t: LineWriter::TextAddType::Extra); |
| 268 | if (self.counter() != c && !spacesToPreserve.isEmpty()) |
| 269 | self.write(v: spacesToPreserve, tAdd: TextAddType::Extra); |
| 270 | return false; |
| 271 | } else { |
| 272 | return true; |
| 273 | } |
| 274 | }); |
| 275 | } |
| 276 | |
| 277 | void LineWriter::setLineIndent(int indentAmount) |
| 278 | { |
| 279 | int startNonSpace = 0; |
| 280 | while (startNonSpace < m_currentLine.size() && m_currentLine.at(i: startNonSpace).isSpace()) |
| 281 | ++startNonSpace; |
| 282 | int oldColumn = column(localIndex: startNonSpace); |
| 283 | if (indentAmount >= 0) { |
| 284 | QString indent; |
| 285 | if (m_options.formatOptions.useTabs) { |
| 286 | indent = QStringLiteral(u"\t" ).repeated(times: indentAmount / m_options.formatOptions.tabSize) |
| 287 | + QStringLiteral(u" " ).repeated(times: indentAmount % m_options.formatOptions.tabSize); |
| 288 | } else { |
| 289 | indent = QStringLiteral(u" " ).repeated(times: indentAmount); |
| 290 | } |
| 291 | if (indent != m_currentLine.mid(position: 0, n: startNonSpace)) { |
| 292 | quint32 colChange = indentAmount - oldColumn; |
| 293 | m_currentColumnNr += colChange; |
| 294 | qint32 oChange = indent.size() - startNonSpace; |
| 295 | m_currentLine = indent + m_currentLine.mid(position: startNonSpace); |
| 296 | m_currentColumnNr = column(localIndex: m_currentLine.size()); |
| 297 | lineChanged(); |
| 298 | changeAtOffset(offset: m_utf16Offset, change: oChange, colChange: oChange, lineChange: 0); |
| 299 | } |
| 300 | } |
| 301 | } |
| 302 | |
| 303 | void LineWriter::handleTrailingSpace(LineWriterOptions::TrailingSpace trailingSpace) |
| 304 | { |
| 305 | switch (trailingSpace) { |
| 306 | case LineWriterOptions::TrailingSpace::Preserve: |
| 307 | break; |
| 308 | case LineWriterOptions::TrailingSpace::Remove: { |
| 309 | int lastNonSpace = m_currentLine.size(); |
| 310 | while (lastNonSpace > 0 && m_currentLine.at(i: lastNonSpace - 1).isSpace()) |
| 311 | --lastNonSpace; |
| 312 | if (lastNonSpace != m_currentLine.size()) { |
| 313 | qint32 oChange = lastNonSpace - m_currentLine.size(); |
| 314 | m_currentLine = m_currentLine.mid(position: 0, n: lastNonSpace); |
| 315 | changeAtOffset(offset: m_utf16Offset + lastNonSpace, change: oChange, colChange: oChange, lineChange: 0); |
| 316 | m_currentColumnNr = |
| 317 | column(localIndex: m_currentLine.size()); // to be extra accurate in the potential split |
| 318 | lineChanged(); |
| 319 | } |
| 320 | } break; |
| 321 | } |
| 322 | } |
| 323 | |
| 324 | void LineWriter::reindentAndSplit(const QString &eol, bool eof) |
| 325 | { |
| 326 | // maybe write out |
| 327 | if (!eol.isEmpty() || eof) { |
| 328 | handleTrailingSpace(trailingSpace: m_options.codeTrailingSpace); |
| 329 | commitLine(eol); |
| 330 | } |
| 331 | } |
| 332 | |
| 333 | SourceLocation LineWriter::currentSourceLocation() const |
| 334 | { |
| 335 | return SourceLocation(m_utf16Offset + m_currentLine.size(), 0, m_lineNr, |
| 336 | m_lineUtf16Offset + m_currentLine.size()); |
| 337 | } |
| 338 | |
| 339 | void LineWriter::changeAtOffset(quint32 offset, qint32 change, qint32 colChange, qint32 lineChange) |
| 340 | { |
| 341 | auto iEnd = m_pendingSourceLocations.end(); |
| 342 | auto i = m_pendingSourceLocations.begin(); |
| 343 | while (i != iEnd) { |
| 344 | i.value().changeAtOffset(offset, change, colChange, lineChange); |
| 345 | ++i; |
| 346 | } |
| 347 | } |
| 348 | |
| 349 | int LineWriter::column(int index) |
| 350 | { |
| 351 | if (index > m_currentLine.size()) |
| 352 | index = m_currentLine.size(); |
| 353 | IndentInfo iInfo(QStringView(m_currentLine).mid(pos: 0, n: index), m_options.formatOptions.tabSize, |
| 354 | m_columnNr); |
| 355 | return iInfo.column; |
| 356 | } |
| 357 | |
| 358 | void LineWriter::textAddCallback(LineWriter::TextAddType t) |
| 359 | { |
| 360 | if (m_textAddCallbacks.isEmpty()) |
| 361 | return; |
| 362 | int iNow = (--m_textAddCallbacks.end()).key() + 1; |
| 363 | while (true) { |
| 364 | auto it = m_textAddCallbacks.lowerBound(key: iNow); |
| 365 | if (it == m_textAddCallbacks.begin()) |
| 366 | break; |
| 367 | --it; |
| 368 | iNow = it.key(); |
| 369 | if (!it.value()(*this, t)) |
| 370 | m_textAddCallbacks.erase(it); |
| 371 | } |
| 372 | } |
| 373 | |
| 374 | void LineWriter::commitLine(const QString &eol, TextAddType tType, int untilChar) |
| 375 | { |
| 376 | if (untilChar == -1) |
| 377 | untilChar = m_currentLine.size(); |
| 378 | bool isSpaceOnly = QStringView(m_currentLine).mid(pos: 0, n: untilChar).trimmed().isEmpty(); |
| 379 | bool isEmptyNewline = !eol.isEmpty() && isSpaceOnly; |
| 380 | quint32 endCommit = m_utf16Offset + untilChar; |
| 381 | // update position, lineNr,... |
| 382 | // write out |
| 383 | for (SinkF &sink : m_innerSinks) |
| 384 | sink(m_currentLine.mid(position: 0, n: untilChar)); |
| 385 | m_utf16Offset += untilChar; |
| 386 | if (!eol.isEmpty()) { |
| 387 | m_utf16Offset += eol.size(); |
| 388 | for (SinkF &sink : m_innerSinks) |
| 389 | sink(eol); |
| 390 | ++m_lineNr; |
| 391 | int oldCol = column(index: untilChar); |
| 392 | m_columnNr = 0; |
| 393 | m_lineUtf16Offset = 0; |
| 394 | changeAtOffset(offset: m_utf16Offset, change: 0, colChange: -oldCol, lineChange: 1); |
| 395 | } else { |
| 396 | m_columnNr = column(index: untilChar); |
| 397 | m_lineUtf16Offset += untilChar; |
| 398 | } |
| 399 | if (untilChar == m_currentLine.size()) { |
| 400 | willCommit(); |
| 401 | m_currentLine.clear(); |
| 402 | } else { |
| 403 | QString nextLine = m_currentLine.mid(position: untilChar); |
| 404 | m_currentLine = m_currentLine.mid(position: 0, n: untilChar); |
| 405 | lineChanged(); |
| 406 | willCommit(); |
| 407 | m_currentLine = nextLine; |
| 408 | } |
| 409 | lineChanged(); |
| 410 | m_currentColumnNr = column(index: m_currentLine.size()); |
| 411 | TextAddType notifyType = tType; |
| 412 | switch (tType) { |
| 413 | case TextAddType::Normal: |
| 414 | if (eol.isEmpty()) |
| 415 | notifyType = TextAddType::PartialCommit; |
| 416 | else |
| 417 | notifyType = TextAddType::Newline; |
| 418 | break; |
| 419 | case TextAddType::Extra: |
| 420 | if (eol.isEmpty()) |
| 421 | notifyType = TextAddType::NewlineExtra; |
| 422 | else |
| 423 | notifyType = TextAddType::PartialCommit; |
| 424 | break; |
| 425 | case TextAddType::Newline: |
| 426 | case TextAddType::NewlineSplit: |
| 427 | case TextAddType::NewlineExtra: |
| 428 | case TextAddType::PartialCommit: |
| 429 | case TextAddType::Eof: |
| 430 | break; |
| 431 | } |
| 432 | if (isEmptyNewline) |
| 433 | ++m_committedEmptyLines; |
| 434 | else if (!isSpaceOnly) |
| 435 | m_committedEmptyLines = 0; |
| 436 | // commit finished pending |
| 437 | auto iEnd = m_pendingSourceLocations.end(); |
| 438 | auto i = m_pendingSourceLocations.begin(); |
| 439 | while (i != iEnd) { |
| 440 | auto &pLoc = i.value(); |
| 441 | if (!pLoc.open && pLoc.utf16End() <= endCommit) { |
| 442 | pLoc.commit(); |
| 443 | i = m_pendingSourceLocations.erase(it: i); |
| 444 | } else { |
| 445 | ++i; |
| 446 | } |
| 447 | } |
| 448 | // notify |
| 449 | textAddCallback(t: notifyType); |
| 450 | } |
| 451 | |
| 452 | } // namespace Dom |
| 453 | } // namespace QQmlJS |
| 454 | QT_END_NAMESPACE |
| 455 | |
| 456 | #include "moc_qqmldomlinewriter_p.cpp" |
| 457 | |