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(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 | |
66 | LineWriter &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 | |
82 | LineWriter &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 | |
89 | LineWriter &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 | |
126 | QString 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 | |
140 | template<typename String, typename ...Args> |
141 | static QRegularExpressionMatch matchHelper(QRegularExpression &re, String &&s, Args &&...args) |
142 | { |
143 | return re.matchView(subjectView: s, offset: args...); |
144 | } |
145 | |
146 | LineWriter &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 | |
196 | void LineWriter::flush() |
197 | { |
198 | if (m_currentLine.size() > 0) |
199 | commitLine(eol: QString()); |
200 | } |
201 | |
202 | void LineWriter::eof(bool shouldEnsureNewline) |
203 | { |
204 | if (shouldEnsureNewline) |
205 | ensureNewline(); |
206 | reindentAndSplit(eol: QString(), eof: true); |
207 | } |
208 | |
209 | SourceLocation LineWriter::committedLocation() const |
210 | { |
211 | return SourceLocation(m_utf16Offset, 0, m_lineNr, m_lineUtf16Offset); |
212 | } |
213 | |
214 | PendingSourceLocationId 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 | |
224 | PendingSourceLocationId 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 | |
234 | void 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 | |
248 | int 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 | |
257 | int 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 | |
276 | void 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 | |
302 | void 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 | |
323 | void 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 | |
332 | SourceLocation LineWriter::currentSourceLocation() const |
333 | { |
334 | return SourceLocation(m_utf16Offset + m_currentLine.size(), 0, m_lineNr, |
335 | m_lineUtf16Offset + m_currentLine.size()); |
336 | } |
337 | |
338 | void 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 | |
348 | int 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 | |
357 | void 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 | |
373 | void 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 |
453 | QT_END_NAMESPACE |
454 | |
455 | #include "moc_qqmldomlinewriter_p.cpp" |
456 | |