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
12LineWriter::LineWriter(
13 const SinkF &innerSink, const QString &fileName, const LineWriterOptions &options,
14 int lineNr, int columnNr, int utf16Offset, const QString &currentLine)
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
26LineWriter &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
42LineWriter &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
49LineWriter &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
56LineWriter &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
91QString 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
105template<typename String, typename ...Args>
106static QRegularExpressionMatch matchHelper(QRegularExpression &re, String &&s, Args &&...args)
107{
108 return re.matchView(subjectView: s, offset: args...);
109}
110
111LineWriter &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
161void LineWriter::flush()
162{
163 if (m_currentLine.size() > 0)
164 commitLine(eol: QString());
165}
166
167void LineWriter::eof(bool shouldEnsureNewline)
168{
169 if (shouldEnsureNewline)
170 ensureNewline();
171 reindentAndSplit(eol: QString(), eof: true);
172}
173
174SourceLocation LineWriter::committedLocation() const
175{
176 return SourceLocation(m_utf16Offset, 0, m_lineNr, m_lineUtf16Offset);
177}
178
179int 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
188int 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
207void 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
231void 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
250void 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
259SourceLocation LineWriter::currentSourceLocation() const
260{
261 return SourceLocation(m_utf16Offset + m_currentLine.size(), 0, m_lineNr,
262 m_lineUtf16Offset + m_currentLine.size());
263}
264
265int 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
274void 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
290void 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
355QT_END_NAMESPACE
356
357#include "moc_qqmldomlinewriter_p.cpp"
358

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