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

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