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 | |