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
8QT_BEGIN_NAMESPACE
9namespace QQmlJS {
10namespace Dom {
11
12quint32 PendingSourceLocation::utf16Start() const
13{
14 return value.offset;
15}
16
17quint32 PendingSourceLocation::utf16End() const
18{
19 return value.offset + value.length;
20}
21
22void 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
45void PendingSourceLocation::commit()
46{
47 if (toUpdate)
48 *toUpdate = value;
49 if (updater)
50 updater(value);
51}
52
53LineWriter::LineWriter(
54 const SinkF &innerSink, const QString &fileName, const LineWriterOptions &options,
55 int lineNr, int columnNr, int utf16Offset, const QString &currentLine)
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
67LineWriter &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
83LineWriter &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
90LineWriter &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
127QString 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
141template<typename String, typename ...Args>
142static QRegularExpressionMatch matchHelper(QRegularExpression &re, String &&s, Args &&...args)
143{
144 return re.matchView(subjectView: s, offset: args...);
145}
146
147LineWriter &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
197void LineWriter::flush()
198{
199 if (m_currentLine.size() > 0)
200 commitLine(eol: QString());
201}
202
203void LineWriter::eof(bool shouldEnsureNewline)
204{
205 if (shouldEnsureNewline)
206 ensureNewline();
207 reindentAndSplit(eol: QString(), eof: true);
208}
209
210SourceLocation LineWriter::committedLocation() const
211{
212 return SourceLocation(m_utf16Offset, 0, m_lineNr, m_lineUtf16Offset);
213}
214
215PendingSourceLocationId 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
225PendingSourceLocationId 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
235void 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
249int 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
258int 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
277void 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
303void 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
324void 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
333SourceLocation LineWriter::currentSourceLocation() const
334{
335 return SourceLocation(m_utf16Offset + m_currentLine.size(), 0, m_lineNr,
336 m_lineUtf16Offset + m_currentLine.size());
337}
338
339void 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
349int 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
358void 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
374void 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
454QT_END_NAMESPACE
455
456#include "moc_qqmldomlinewriter_p.cpp"
457

source code of qtdeclarative/src/qmldom/qqmldomlinewriter.cpp