1// Copyright (C) 2023 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 <qqmlrangeformatting_p.h>
5#include <qqmlcodemodel_p.h>
6#include <qqmllsutils_p.h>
7
8#include <QtQmlDom/private/qqmldomitem_p.h>
9#include <QtQmlDom/private/qqmldomindentinglinewriter_p.h>
10#include <QtQmlDom/private/qqmldomcodeformatter_p.h>
11#include <QtQmlDom/private/qqmldomoutwriter_p.h>
12#include <QtQmlDom/private/qqmldommock_p.h>
13#include <QtQmlDom/private/qqmldomcompare_p.h>
14
15QT_BEGIN_NAMESPACE
16
17QQmlRangeFormatting::QQmlRangeFormatting(QmlLsp::QQmlCodeModel *codeModel)
18 : QQmlBaseModule(codeModel)
19{
20}
21
22QString QQmlRangeFormatting::name() const
23{
24 return u"QQmlRangeFormatting"_s;
25}
26
27void QQmlRangeFormatting::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol)
28{
29 protocol->registerDocumentRangeFormattingRequestHandler(handler: getRequestHandler());
30}
31
32void QQmlRangeFormatting::setupCapabilities(const QLspSpecification::InitializeParams &,
33 QLspSpecification::InitializeResult &serverCapabilities)
34{
35 serverCapabilities.capabilities.documentRangeFormattingProvider = true;
36}
37
38void QQmlRangeFormatting::process(RequestPointerArgument request)
39{
40 using namespace QQmlJS::Dom;
41 QList<QLspSpecification::TextEdit> result{};
42
43 QmlLsp::OpenDocument doc = m_codeModel->openDocumentByUrl(
44 url: QQmlLSUtils::lspUriToQmlUrl(uri: request->m_parameters.textDocument.uri));
45
46 DomItem file = doc.snapshot.doc.fileObject(option: GoTo::MostLikely);
47 if (!file) {
48 qWarning() << u"Could not find the file"_s << doc.snapshot.doc.toString();
49 return;
50 }
51
52 if (auto envPtr = file.environment().ownerAs<DomEnvironment>())
53 envPtr->clearReferenceCache();
54
55 auto qmlFile = file.ownerAs<QmlFile>();
56 auto code = qmlFile->code();
57
58 // Range requested to be formatted
59 const auto selectedRange = request->m_parameters.range;
60 const auto selectedRangeStartLine = selectedRange.start.line;
61 const auto selectedRangeEndLine = selectedRange.end.line;
62 Q_ASSERT(selectedRangeStartLine >= 0);
63 Q_ASSERT(selectedRangeEndLine >= 0);
64
65 LineWriterOptions options;
66 options.attributesSequence = LineWriterOptions::AttributesSequence::Preserve;
67
68 QTextStream in(&code);
69 FormatTextStatus status = FormatTextStatus::initialStatus();
70 FormatPartialStatus partialStatus({}, options.formatOptions, status);
71
72 // Get the token status of the previous line without performing write operation
73 int lineNumber = 0;
74 while (!in.atEnd()) {
75 const auto line = in.readLine();
76 partialStatus = formatCodeLine(line, options: options.formatOptions, initialStatus: partialStatus.currentStatus);
77 if (++lineNumber >= selectedRangeStartLine)
78 break;
79 }
80
81 QString resultText;
82 QTextStream out(&resultText);
83 IndentingLineWriter lw([&out](QStringView writtenText) { out << writtenText.toUtf8(); },
84 QString(), options, partialStatus.currentStatus);
85 OutWriter ow(lw);
86 ow.indentNextlines = true;
87
88 // TODO: This is a workaround and will/should be handled by the actual formatter
89 // once we improve the range-formatter design in QTBUG-116139
90 const auto removeSpaces = [](const QString &line) {
91 QString result;
92 QTextStream out(&result);
93 bool previousIsSpace = false;
94
95 int newLineCount = 0;
96 for (int i = 0; i < line.length(); ++i) {
97 QChar c = line.at(i);
98 if (c.isSpace()) {
99 if (c == '\n'_L1 && newLineCount < 2) {
100 out << '\n'_L1;
101 ++newLineCount;
102 } else if (c == '\r'_L1 && (i + 1) < line.length() && line.at(i: i + 1) == '\n'_L1
103 && newLineCount < 2) {
104 out << "\r\n";
105 ++newLineCount;
106 ++i;
107 } else {
108 if (!previousIsSpace)
109 out << ' '_L1;
110 }
111 previousIsSpace = true;
112 } else {
113 out << c;
114 previousIsSpace = false;
115 newLineCount = 0;
116 }
117 }
118
119 out.flush();
120 return result;
121 };
122
123 const auto startOffset = QQmlLSUtils::textOffsetFrom(code, row: selectedRangeStartLine, character: 0);
124 auto endOffset = QQmlLSUtils::textOffsetFrom(code, row: selectedRangeEndLine + 1, character: 0);
125
126 // note: the character at endOffset (usually a \n) is ignored. Therefore avoid
127 // formatting \r\n that will ignore \n and format \r as a space (' ').
128 if (endOffset < code.size() && code[endOffset - 1] == u'\r' && code[endOffset] == u'\n')
129 --endOffset;
130
131 const auto &toFormat = code.mid(position: startOffset, n: endOffset - startOffset);
132 ow.write(v: removeSpaces(toFormat));
133 ow.flush();
134 ow.eof();
135
136 const auto documentLineCount = QQmlLSUtils::textRowAndColumnFrom(code, offset: code.length()).line;
137 code.replace(i: startOffset, len: toFormat.length(), after: resultText);
138
139 QLspSpecification::TextEdit add;
140 add.newText = code.toUtf8();
141 add.range = { .start: { .line: 0, .character: 0 }, .end: { .line: documentLineCount + 1 } };
142 result.append(t: add);
143
144 request->m_response.sendResponse(r: result);
145}
146
147QT_END_NAMESPACE
148

source code of qtdeclarative/src/qmlls/qqmlrangeformatting.cpp