Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
qqmllscodeaction.cpp
Go to the documentation of this file.
1// Copyright (C) 2026 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// Qt-Security score:significant reason:default
4
6
8
9Q_STATIC_LOGGING_CATEGORY(lsCodeActionSupport, "qt.languageserver.codeactionsupport")
10
11struct Tr
12{
13 Q_DECLARE_TR_FUNCTIONS(QQmlCodeActions)
14};
15
16using namespace Qt::StringLiterals;
17using namespace QLspSpecification;
18using namespace QQmlJS::Dom;
19
20using CodeActions = QList<std::variant<Command, CodeAction>>;
21using TextEdits = decltype(std::declval<TextDocumentEdit>().edits);
22
23static QList<std::pair<QString, QQmlJS::SourceLocation>>
24collectNestedIds(const QQmlLSUtils::ItemLocation &item)
25{
26 const auto filePtr =
27 item.domItem.goToFile(item.domItem.canonicalFilePath()).ownerAs<QQmlJS::Dom::QmlFile>();
28 const QString code = filePtr ? filePtr->code() : QString();
29
30 QList<std::pair<QString, QQmlJS::SourceLocation>> innerIds;
31 FileLocations::visitTree(
32 item.fileLocation,
33 [&code, &innerIds](const auto &, const FileLocations::Tree &t) -> bool {
34 const auto idNameLoc = t->info().regions[FileLocationRegion::IdNameRegion];
35 if (idNameLoc.isValid()) {
36 innerIds.append({ code.mid(idNameLoc.begin(), idNameLoc.length), idNameLoc });
37 }
38 return true;
39 });
40 return innerIds;
41}
42
43static CodeActions quickfixes(const QList<Diagnostic> &diagnostics)
44{
45 CodeActions codeActions;
46
47 for (const Diagnostic &diagnostic : diagnostics) {
48 if (!diagnostic.data.has_value())
49 continue;
50
51 const auto &data = diagnostic.data.value();
52
53 int version = data[u"version"].toInt();
54 QJsonArray suggestions = data[u"suggestions"].toArray();
55
56 QList<WorkspaceEdit::DocumentChange> edits;
57 QString message;
58 for (const QJsonValue &suggestion : std::as_const(suggestions)) {
59 message += suggestion[u"message"_s].toString() + u'\n';
60 const auto &documentEdits = suggestion[u"documentEdits"_s].toArray();
61 TextDocumentEdit textDocEdit;
62 for (const auto &documentEdit : documentEdits) {
63 TextEdit textEdit;
64 textEdit.range = {
65 Position{
66 static_cast<unsigned>(documentEdit[u"lspBeginLine"].toDouble()),
67 static_cast<unsigned>(documentEdit[u"lspBeginCharacter"].toDouble()) },
68 Position{ static_cast<unsigned>(documentEdit[u"lspEndLine"].toDouble()),
69 static_cast<unsigned>(documentEdit[u"lspEndCharacter"].toDouble()) }
70 };
71
72 textEdit.newText = documentEdit[u"replacement"_s].toString().toUtf8();
73 QString filename = documentEdit[u"filename"_s].toString();
74 textDocEdit.textDocument = { { filename.toUtf8() }, version };
75 textDocEdit.edits.append(textEdit);
76 }
77 edits.append(textDocEdit);
78 }
79 message.chop(1);
80 WorkspaceEdit edit;
81 edit.documentChanges = edits;
82
83 CodeAction action;
84 // VS Code and QtC ignore everything that is not a 'quickfix'.
85 action.kind = CodeActionKind::QuickFix;
86 action.edit = edit;
87 action.title = message.toUtf8();
88
89 codeActions.append(action);
90 }
91 return codeActions;
92}
93
94static TextEdit todoComment(const Position &pos, const QString &loaderId, const QString &maybeId,
96{
97 QString comment = Tr::tr("// TODO: Move position bindings from the component to the Loader.\n"
98 "// Check all uses of 'parent' inside the root element of the "
99 "component.\n");
100
101 if (!maybeId.isEmpty()) {
102 comment += Tr::tr("// Rename all outer uses of the id \"%1\" to \"%2.item\".\n")
103 .arg(maybeId, loaderId);
104 }
105 for (const auto &id : nestedIds) {
106 comment += Tr::tr("// Rename all outer uses of the id \"%1\" to \"%2.item.%1\".\n")
107 .arg(id.first, loaderId);
108 }
109
110 return { { pos, pos }, comment.toUtf8() };
111}
112
113static TextEdits wrapIntoComponent(const Range &itemRange, const QString &componentId)
114{
115 const QString componentOpen = QString::fromLatin1("Component {\n"
116 " id: %1\n")
117 .arg(componentId);
118 const QString componentClose = QString::fromLatin1("\n}\n");
119 return { TextEdit{ { itemRange.start, itemRange.start }, componentOpen.toUtf8() },
120 TextEdit{ { itemRange.end, itemRange.end }, componentClose.toUtf8() } };
121}
122
123static TextEdit addLoader(const Position &pos, const QString &loaderId, const QString &componentId)
124{
125 const QString loader = QString::fromLatin1("Loader {\n"
126 " id: %2\n"
127 " sourceComponent: %1\n"
128 "}\n")
129 .arg(componentId, loaderId);
130
131 return { { pos, pos }, loader.toUtf8() };
132}
133
134static TextEdits exposeNestedIds(const QQmlJS::SourceLocation &openingBrace,
136{
137 const QLatin1StringView nestedIdPrefix("inner_");
138 QString idAliases = QString::fromLatin1("\n");
139 for (auto &id : nestedIds) {
140 idAliases += QString::fromLatin1("property alias %1: %2%1\n").arg(id.first, nestedIdPrefix);
141 }
142
143 TextEdits edits;
144
145 const auto posAfterLBrace =
146 Position{ static_cast<unsigned int>(static_cast<int>(openingBrace.startLine - 1)),
147 static_cast<unsigned int>(
148 static_cast<int>(openingBrace.startColumn)) /* after { */ };
149 // edit introducing property aliases
150 edits.append(TextEdit{ { posAfterLBrace, posAfterLBrace }, idAliases.toUtf8() });
151
152 // edits appending prefix "inner_" to each nested id
153 for (auto &id : nestedIds) {
154 const auto idPos =
155 Position{ static_cast<unsigned int>(static_cast<int>(id.second.startLine - 1)),
156 static_cast<unsigned int>(static_cast<int>(id.second.startColumn - 1)) };
157 edits.append(TextEdit{ { idPos, idPos }, nestedIdPrefix.toUtf8() });
158 }
159 return edits;
160}
161
162static TextEdits wrapInLoaderTextEdits(const QQmlLSUtils::ItemLocation &item)
163{
164 const auto generateId = [&item](const QString &base) -> QString {
165 const auto ids = item.domItem.component().field(Fields::ids).keys();
166 if (!ids.contains(base)) {
167 return base;
168 };
169 int extraNumber = 1;
170 for (; extraNumber < ids.size(); ++extraNumber) {
171 QString id = base + QString::number(extraNumber);
172 if (!ids.contains(id)) {
173 return id;
174 }
175 }
176 return base + QString::number(extraNumber);
177 };
178
179 const auto objId = item.domItem.idStr();
180 const auto objName = objId.isEmpty() ? item.domItem.name() : objId;
181 const QString componentId = generateId(QLatin1StringView("component_") + objName);
182 const QString loaderId = generateId(QLatin1StringView("loader_") + objName);
183
184 const auto itemRange = QQmlLSUtils::qmlLocationToLspLocation(
185 QQmlLSUtils::Location::tryFrom(item.domItem.canonicalFilePath(),
186 item.fileLocation->info().fullRegion, item.domItem)
187 .value_or(QQmlLSUtils::Location{}));
188
189 QList<std::pair<QString, QQmlJS::SourceLocation>> nestedIds = collectNestedIds(item);
190 if (!objId.isEmpty()) {
191 // We expect the first found nested Id to be an object id.
192 // Watch out for collectNestedIds changes
193 Q_ASSERT(nestedIds.front().first == objId);
194 nestedIds.removeFirst();
195 }
196
197 TextEdits edits;
198 edits << todoComment(itemRange.start, loaderId, objId, nestedIds)
199 << exposeNestedIds(item.fileLocation->info().regions[FileLocationRegion::LeftBraceRegion],
200 nestedIds)
201 << wrapIntoComponent(itemRange, componentId)
202 << addLoader(itemRange.end, loaderId, componentId);
203 return edits;
204}
205
206static CodeActions wrapComponentInLoader(const TextDocumentIdentifier &textDocument,
207 const QQmlLSUtils::ItemLocation &item)
208{
209 if (item.domItem.internalKind() != DomType::QmlObject) {
210 // applicable only to objects
211 return {};
212 }
213 if (item.domItem == item.domItem.component().field(Fields::objects).index(0)) {
214 // not applicable for a root object
215 return {};
216 }
217 if (item.domItem.canonicalPath().last() == Path::fromField(Fields::value)) {
218 // not supported for the binding value, i.e. p: Item{}
219 return {};
220 }
221
222 TextDocumentEdit textDocEdit;
223 textDocEdit.textDocument = { textDocument, {} };
224 textDocEdit.edits = wrapInLoaderTextEdits(item);
225
226 WorkspaceEdit edit;
227 edit.documentChanges = { textDocEdit };
228
229 CodeAction action;
230 action.kind = CodeActionKind::RefactorRewrite;
231 action.title = "Wrap Component in Loader";
232 action.edit = edit;
233 return { action };
234}
235
236static CodeActions refactorings(const TextDocumentIdentifier &textDocument,
237 const QQmlLSUtils::ItemLocation &item)
238{
239 CodeActions codeActions;
240 codeActions.append(wrapComponentInLoader(textDocument, item));
241 return codeActions;
242}
243
244QQmlCodeActionSupport::QQmlCodeActionSupport(QmlLsp::QQmlCodeModelManager *model) : BaseT(model) { }
245
246void QQmlCodeActionSupport::setupCapabilities(QLspSpecification::ServerCapabilities &caps)
247{
248 caps.codeActionProvider = true;
249}
250
251void QQmlCodeActionSupport::registerHandlers(QLanguageServer *, QLanguageServerProtocol *protocol)
252{
253 protocol->registerCodeActionRequestHandler(getRequestHandler());
254}
255
256void QQmlCodeActionSupport::process(QQmlCodeActionSupport::RequestPointerArgument request)
257{
258 CodeActions codeActions;
259 codeActions.append(quickfixes(request->m_parameters.context.diagnostics));
260
261 // QmlObject has the same start location as UiQualifiedId
262 // Therefore in order to get codeActions relevant to QmlObject it's more reliable
263 // to use range.end instead of range.start
264 auto itemsFound = itemsForRequest(request, request->m_parameters.range.end);
265 if (std::holds_alternative<QQmlLSUtils::ErrorMessage>(itemsFound)) {
266 qCWarning(lsCodeActionSupport) << std::get<QQmlLSUtils::ErrorMessage>(itemsFound).message;
267 } else if (std::holds_alternative<QList<QQmlLSUtils::ItemLocation>>(itemsFound)) {
268 QQmlLSUtils::ItemLocation &item =
269 std::get<QList<QQmlLSUtils::ItemLocation>>(itemsFound).front();
270
271 codeActions.append(refactorings(request->m_parameters.textDocument, item));
272 }
273
274 request->m_response.sendResponse(codeActions);
275}
276
277QT_END_NAMESPACE
void process(RequestPointerArgument req) override
void setupCapabilities(QLspSpecification::ServerCapabilities &caps) override
QQmlCodeActionSupport(QmlLsp::QQmlCodeModelManager *codeModel)
void registerHandlers(QLanguageServer *server, QLanguageServerProtocol *protocol) override
Combined button and popup list for selecting options.
static CodeActions quickfixes(const QList< Diagnostic > &diagnostics)
static TextEdits exposeNestedIds(const QQmlJS::SourceLocation &openingBrace, const QList< std::pair< QString, QQmlJS::SourceLocation > > &nestedIds)
static CodeActions refactorings(const TextDocumentIdentifier &textDocument, const QQmlLSUtils::ItemLocation &item)
static TextEdit addLoader(const Position &pos, const QString &loaderId, const QString &componentId)
static TextEdits wrapInLoaderTextEdits(const QQmlLSUtils::ItemLocation &item)
static TextEdits wrapIntoComponent(const Range &itemRange, const QString &componentId)
static TextEdit todoComment(const Position &pos, const QString &loaderId, const QString &maybeId, const QList< std::pair< QString, QQmlJS::SourceLocation > > &nestedIds)
static CodeActions wrapComponentInLoader(const TextDocumentIdentifier &textDocument, const QQmlLSUtils::ItemLocation &item)