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 | #ifndef QQMLBASEMODULE_P_H |
5 | #define QQMLBASEMODULE_P_H |
6 | |
7 | // |
8 | // W A R N I N G |
9 | // ------------- |
10 | // |
11 | // This file is not part of the Qt API. It exists purely as an |
12 | // implementation detail. This header file may change from version to |
13 | // version without notice, or even be removed. |
14 | // |
15 | // We mean it. |
16 | // |
17 | |
18 | #include "qlanguageserver_p.h" |
19 | #include "qqmlcodemodel_p.h" |
20 | #include "qqmllsutils_p.h" |
21 | #include <QtQmlDom/private/qqmldom_utils_p.h> |
22 | |
23 | #include <QObject> |
24 | #include <type_traits> |
25 | #include <unordered_map> |
26 | |
27 | template<typename ParametersT, typename ResponseT> |
28 | struct BaseRequest |
29 | { |
30 | // allow using Parameters and Response type aliases in the |
31 | // implementations of the different requests. |
32 | using Parameters = ParametersT; |
33 | using Response = ResponseT; |
34 | |
35 | // The version of the code on which the typedefinition request was made. |
36 | // Request is received: mark it with the current version of the textDocument. |
37 | // Then, wait for the codemodel to finish creating a snapshot version that is newer or equal to |
38 | // the textDocument version at request-received-time. |
39 | int m_minVersion; |
40 | Parameters m_parameters; |
41 | Response m_response; |
42 | |
43 | bool fillFrom(QmlLsp::OpenDocument doc, const Parameters ¶ms, Response &&response); |
44 | }; |
45 | |
46 | /*! |
47 | \internal |
48 | \brief This class sends a result or an error when going out of scope. |
49 | |
50 | It has a helper method \c setErrorFrom that sets an error from variant and optionals. |
51 | */ |
52 | |
53 | template<typename Result, typename ResponseCallback> |
54 | struct ResponseScopeGuard |
55 | { |
56 | Q_DISABLE_COPY_MOVE(ResponseScopeGuard) |
57 | |
58 | std::variant<Result *, QQmlLSUtils::ErrorMessage> m_response; |
59 | ResponseCallback &m_callback; |
60 | |
61 | ResponseScopeGuard(Result &results, ResponseCallback &callback) |
62 | : m_response(&results), m_callback(callback) |
63 | { |
64 | } |
65 | |
66 | // note: discards the current result or error message, if there is any |
67 | void setError(const QQmlLSUtils::ErrorMessage &error) { m_response = error; } |
68 | |
69 | template<typename... T> |
70 | bool setErrorFrom(const std::variant<T...> &variant) |
71 | { |
72 | static_assert(std::disjunction_v<std::is_same<T, QQmlLSUtils::ErrorMessage>...>, |
73 | "ResponseScopeGuard::setErrorFrom was passed a variant that never contains" |
74 | " an error message." ); |
75 | if (auto x = std::get_if<QQmlLSUtils::ErrorMessage>(&variant)) { |
76 | setError(*x); |
77 | return true; |
78 | } |
79 | return false; |
80 | } |
81 | |
82 | /*! |
83 | \internal |
84 | Note: use it as follows: |
85 | \badcode |
86 | if (scopeGuard.setErrorFrom(xxx)) { |
87 | // do early exit |
88 | } |
89 | // xxx was not an error, continue |
90 | \endcode |
91 | */ |
92 | bool setErrorFrom(const std::optional<QQmlLSUtils::ErrorMessage> &error) |
93 | { |
94 | if (error) { |
95 | setError(*error); |
96 | return true; |
97 | } |
98 | return false; |
99 | } |
100 | |
101 | ~ResponseScopeGuard() |
102 | { |
103 | std::visit(qOverloadedVisitor{ [this](Result *result) { m_callback.sendResponse(*result); }, |
104 | [this](const QQmlLSUtils::ErrorMessage &error) { |
105 | m_callback.sendErrorResponse(error.code, |
106 | error.message.toUtf8()); |
107 | } }, |
108 | m_response); |
109 | } |
110 | }; |
111 | |
112 | template<typename RequestType> |
113 | struct QQmlBaseModule : public QLanguageServerModule |
114 | { |
115 | using RequestParameters = typename RequestType::Parameters; |
116 | using RequestResponse = typename RequestType::Response; |
117 | using RequestPointer = std::unique_ptr<RequestType>; |
118 | using RequestPointerArgument = RequestPointer &&; |
119 | using BaseT = QQmlBaseModule<RequestType>; |
120 | |
121 | QQmlBaseModule(QmlLsp::QQmlCodeModel *codeModel); |
122 | ~QQmlBaseModule(); |
123 | |
124 | void requestHandler(const RequestParameters ¶meters, RequestResponse &&response); |
125 | decltype(auto) getRequestHandler(); |
126 | // processes a request in a different thread. |
127 | virtual void process(RequestPointerArgument toBeProcessed) = 0; |
128 | std::variant<QList<QQmlLSUtils::ItemLocation>, QQmlLSUtils::ErrorMessage> |
129 | itemsForRequest(const RequestPointer &request); |
130 | |
131 | public Q_SLOTS: |
132 | void updatedSnapshot(const QByteArray &uri); |
133 | |
134 | protected: |
135 | QMutex m_pending_mutex; |
136 | std::unordered_multimap<QString, RequestPointer> m_pending; |
137 | QmlLsp::QQmlCodeModel *m_codeModel; |
138 | }; |
139 | |
140 | template<typename Parameters, typename Response> |
141 | bool BaseRequest<Parameters, Response>::fillFrom(QmlLsp::OpenDocument doc, const Parameters ¶ms, |
142 | Response &&response) |
143 | { |
144 | Q_UNUSED(doc); |
145 | m_parameters = params; |
146 | m_response = std::move(response); |
147 | |
148 | if (!doc.textDocument) { |
149 | qDebug() << "Cannot find document in qmlls's codemodel, did you open it before accessing " |
150 | "it?" ; |
151 | return false; |
152 | } |
153 | |
154 | { |
155 | QMutexLocker l(doc.textDocument->mutex()); |
156 | m_minVersion = doc.textDocument->version().value_or(u: 0); |
157 | } |
158 | return true; |
159 | } |
160 | |
161 | template<typename RequestType> |
162 | QQmlBaseModule<RequestType>::QQmlBaseModule(QmlLsp::QQmlCodeModel *codeModel) |
163 | : m_codeModel(codeModel) |
164 | { |
165 | QObject::connect(m_codeModel, &QmlLsp::QQmlCodeModel::updatedSnapshot, this, |
166 | &QQmlBaseModule<RequestType>::updatedSnapshot); |
167 | } |
168 | |
169 | template<typename RequestType> |
170 | QQmlBaseModule<RequestType>::~QQmlBaseModule() |
171 | { |
172 | QMutexLocker l(&m_pending_mutex); |
173 | m_pending.clear(); // empty the m_pending while the mutex is hold |
174 | } |
175 | |
176 | template<typename RequestType> |
177 | decltype(auto) QQmlBaseModule<RequestType>::getRequestHandler() |
178 | { |
179 | auto handler = [this](const QByteArray &, const RequestParameters ¶meters, |
180 | RequestResponse &&response) { |
181 | requestHandler(parameters, response: std::move(response)); |
182 | }; |
183 | return handler; |
184 | } |
185 | |
186 | template<typename RequestType> |
187 | void QQmlBaseModule<RequestType>::requestHandler(const RequestParameters ¶meters, |
188 | RequestResponse &&response) |
189 | { |
190 | auto req = std::make_unique<RequestType>(); |
191 | QmlLsp::OpenDocument doc = m_codeModel->openDocumentByUrl( |
192 | url: QQmlLSUtils::lspUriToQmlUrl(uri: parameters.textDocument.uri)); |
193 | |
194 | if (!req->fillFrom(doc, parameters, std::move(response))) { |
195 | req->m_response.sendErrorResponse(0, "Received invalid request" , parameters); |
196 | return; |
197 | } |
198 | const int minVersion = req->m_minVersion; |
199 | { |
200 | QMutexLocker l(&m_pending_mutex); |
201 | m_pending.insert({ QString::fromUtf8(req->m_parameters.textDocument.uri), std::move(req) }); |
202 | } |
203 | |
204 | if (doc.snapshot.docVersion && *doc.snapshot.docVersion >= minVersion) |
205 | updatedSnapshot(uri: QQmlLSUtils::lspUriToQmlUrl(uri: parameters.textDocument.uri)); |
206 | } |
207 | |
208 | template<typename RequestType> |
209 | void QQmlBaseModule<RequestType>::updatedSnapshot(const QByteArray &url) |
210 | { |
211 | QmlLsp::OpenDocumentSnapshot doc = m_codeModel->snapshotByUrl(url); |
212 | std::vector<RequestPointer> toCompl; |
213 | { |
214 | QMutexLocker l(&m_pending_mutex); |
215 | for (auto [it, end] = m_pending.equal_range(QString::fromUtf8(ba: url)); it != end;) { |
216 | if (auto &[key, value] = *it; |
217 | doc.docVersion && value->m_minVersion <= *doc.docVersion) { |
218 | toCompl.push_back(std::move(value)); |
219 | it = m_pending.erase(it); |
220 | } else { |
221 | ++it; |
222 | } |
223 | } |
224 | } |
225 | for (auto it = toCompl.rbegin(), end = toCompl.rend(); it != end; ++it) { |
226 | process(toBeProcessed: std::move(*it)); |
227 | } |
228 | } |
229 | |
230 | template<typename RequestType> |
231 | std::variant<QList<QQmlLSUtils::ItemLocation>, QQmlLSUtils::ErrorMessage> |
232 | QQmlBaseModule<RequestType>::itemsForRequest(const RequestPointer &request) |
233 | { |
234 | |
235 | QmlLsp::OpenDocument doc = m_codeModel->openDocumentByUrl( |
236 | url: QQmlLSUtils::lspUriToQmlUrl(uri: request->m_parameters.textDocument.uri)); |
237 | |
238 | if (!doc.snapshot.validDocVersion || doc.snapshot.validDocVersion != doc.snapshot.docVersion) { |
239 | return QQmlLSUtils::ErrorMessage{ .code: 0, |
240 | .message: u"Cannot proceed: current QML document is invalid! Fix" |
241 | u" all the errors in your QML code and try again."_s }; |
242 | } |
243 | |
244 | QQmlJS::Dom::DomItem file = doc.snapshot.validDoc.fileObject(option: QQmlJS::Dom::GoTo::MostLikely); |
245 | // clear reference cache to resolve latest versions (use a local env instead?) |
246 | if (auto envPtr = file.environment().ownerAs<QQmlJS::Dom::DomEnvironment>()) |
247 | envPtr->clearReferenceCache(); |
248 | if (!file) { |
249 | return QQmlLSUtils::ErrorMessage{ |
250 | .code: 0, |
251 | .message: u"Could not find file %1 in project."_s .arg(a: doc.snapshot.doc.toString()), |
252 | }; |
253 | } |
254 | |
255 | auto itemsFound = QQmlLSUtils::itemsFromTextLocation(file, line: request->m_parameters.position.line, |
256 | character: request->m_parameters.position.character); |
257 | |
258 | if (itemsFound.isEmpty()) { |
259 | return QQmlLSUtils::ErrorMessage{ |
260 | .code: 0, |
261 | .message: u"Could not find any items at given text location."_s , |
262 | }; |
263 | } |
264 | return itemsFound; |
265 | } |
266 | |
267 | #endif // QQMLBASEMODULE_P_H |
268 | |