1 /****************************************************************************
2 **
3 ** Copyright (C) 2020 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25
26 #include "languageclientsymbolsupport.h"
27
28 #include "client.h"
29 #include "languageclientutils.h"
30
31 #include <coreplugin/editormanager/editormanager.h>
32 #include <coreplugin/find/searchresultwindow.h>
33
34 #include <utils/mimetypes/mimedatabase.h>
35
36 #include <QFile>
37
38 using namespace LanguageServerProtocol;
39
40 namespace LanguageClient {
41
SymbolSupport(Client * client)42 SymbolSupport::SymbolSupport(Client *client) : m_client(client)
43 {}
44
45 template<typename Request>
sendTextDocumentPositionParamsRequest(Client * client,const Request & request,const DynamicCapabilities & dynamicCapabilities,const ServerCapabilities & serverCapability)46 static void sendTextDocumentPositionParamsRequest(Client *client,
47 const Request &request,
48 const DynamicCapabilities &dynamicCapabilities,
49 const ServerCapabilities &serverCapability)
50 {
51 if (!request.isValid(nullptr))
52 return;
53 const DocumentUri uri = request.params().value().textDocument().uri();
54 const bool supportedFile = client->isSupportedUri(uri);
55 bool sendMessage = dynamicCapabilities.isRegistered(Request::methodName).value_or(false);
56 if (sendMessage) {
57 const TextDocumentRegistrationOptions option(
58 dynamicCapabilities.option(Request::methodName));
59 if (option.isValid())
60 sendMessage = option.filterApplies(
61 Utils::FilePath::fromString(QUrl(uri).adjusted(QUrl::PreferLocalFile).toString()));
62 else
63 sendMessage = supportedFile;
64 } else {
65 const Utils::optional<Utils::variant<bool, WorkDoneProgressOptions>> &provider
66 = serverCapability.referencesProvider();
67 sendMessage = provider.has_value();
68 if (sendMessage && Utils::holds_alternative<bool>(*provider))
69 sendMessage = Utils::get<bool>(*provider);
70 }
71 if (sendMessage)
72 client->sendContent(request);
73 }
74
handleGotoDefinitionResponse(const GotoDefinitionRequest::Response & response,Utils::ProcessLinkCallback callback,Utils::optional<Utils::Link> linkUnderCursor)75 static void handleGotoDefinitionResponse(const GotoDefinitionRequest::Response &response,
76 Utils::ProcessLinkCallback callback,
77 Utils::optional<Utils::Link> linkUnderCursor)
78 {
79 if (Utils::optional<GotoResult> _result = response.result()) {
80 const GotoResult result = _result.value();
81 if (Utils::holds_alternative<std::nullptr_t>(result))
82 return;
83 if (auto ploc = Utils::get_if<Location>(&result)) {
84 callback(linkUnderCursor.value_or(ploc->toLink()));
85 } else if (auto plloc = Utils::get_if<QList<Location>>(&result)) {
86 if (!plloc->isEmpty())
87 callback(linkUnderCursor.value_or(plloc->value(0).toLink()));
88 }
89 }
90 }
91
generateDocPosParams(TextEditor::TextDocument * document,const QTextCursor & cursor)92 static TextDocumentPositionParams generateDocPosParams(TextEditor::TextDocument *document,
93 const QTextCursor &cursor)
94 {
95 const DocumentUri uri = DocumentUri::fromFilePath(document->filePath());
96 const TextDocumentIdentifier documentId(uri);
97 const Position pos(cursor);
98 return TextDocumentPositionParams(documentId, pos);
99 }
100
findLinkAt(TextEditor::TextDocument * document,const QTextCursor & cursor,Utils::ProcessLinkCallback callback,const bool resolveTarget)101 void SymbolSupport::findLinkAt(TextEditor::TextDocument *document,
102 const QTextCursor &cursor,
103 Utils::ProcessLinkCallback callback,
104 const bool resolveTarget)
105 {
106 if (!m_client->reachable())
107 return;
108 GotoDefinitionRequest request(generateDocPosParams(document, cursor));
109 Utils::optional<Utils::Link> linkUnderCursor;
110 if (!resolveTarget) {
111 QTextCursor linkCursor = cursor;
112 linkCursor.select(QTextCursor::WordUnderCursor);
113 Utils::Link link(document->filePath(),
114 linkCursor.blockNumber() + 1,
115 linkCursor.positionInBlock());
116 link.linkTextStart = linkCursor.selectionStart();
117 link.linkTextEnd = linkCursor.selectionEnd();
118 linkUnderCursor = link;
119 }
120 request.setResponseCallback(
121 [callback, linkUnderCursor](const GotoDefinitionRequest::Response &response) {
122 handleGotoDefinitionResponse(response, callback, linkUnderCursor);
123 });
124
125 sendTextDocumentPositionParamsRequest(m_client,
126 request,
127 m_client->dynamicCapabilities(),
128 m_client->capabilities());
129
130 }
131
132 struct ItemData
133 {
134 Core::Search::TextRange range;
135 QVariant userData;
136 };
137
getFileContents(const Utils::FilePath & filePath)138 QStringList SymbolSupport::getFileContents(const Utils::FilePath &filePath)
139 {
140 QString fileContent;
141 if (TextEditor::TextDocument *document = TextEditor::TextDocument::textDocumentForFilePath(
142 filePath)) {
143 fileContent = document->plainText();
144 } else {
145 Utils::TextFileFormat format;
146 format.lineTerminationMode = Utils::TextFileFormat::LFLineTerminator;
147 QString error;
148 const QTextCodec *codec = Core::EditorManager::defaultTextCodec();
149 if (Utils::TextFileFormat::readFile(filePath, codec, &fileContent, &format, &error)
150 != Utils::TextFileFormat::ReadSuccess) {
151 qDebug() << "Failed to read file" << filePath << ":" << error;
152 }
153 }
154 return fileContent.split("\n");
155 }
156
generateSearchResultItems(const QMap<Utils::FilePath,QList<ItemData>> & rangesInDocument)157 QList<Core::SearchResultItem> generateSearchResultItems(
158 const QMap<Utils::FilePath, QList<ItemData>> &rangesInDocument)
159 {
160 QList<Core::SearchResultItem> result;
161 for (auto it = rangesInDocument.begin(); it != rangesInDocument.end(); ++it) {
162 const Utils::FilePath &filePath = it.key();
163
164 Core::SearchResultItem item;
165 item.setFilePath(filePath);
166 item.setUseTextEditorFont(true);
167
168 QStringList lines = SymbolSupport::getFileContents(filePath);
169 for (const ItemData &data : it.value()) {
170 item.setMainRange(data.range);
171 if (data.range.begin.line > 0 && data.range.begin.line <= lines.size())
172 item.setLineText(lines[data.range.begin.line - 1]);
173 item.setUserData(data.userData);
174 result << item;
175 }
176 }
177 return result;
178 }
179
generateSearchResultItems(const LanguageClientArray<Location> & locations)180 QList<Core::SearchResultItem> generateSearchResultItems(
181 const LanguageClientArray<Location> &locations)
182 {
183 if (locations.isNull())
184 return {};
185 QMap<Utils::FilePath, QList<ItemData>> rangesInDocument;
186 for (const Location &location : locations.toList())
187 rangesInDocument[location.uri().toFilePath()]
188 << ItemData{SymbolSupport::convertRange(location.range()), {}};
189 return generateSearchResultItems(rangesInDocument);
190 }
191
handleFindReferencesResponse(const FindReferencesRequest::Response & response,const QString & wordUnderCursor,const ResultHandler & handler)192 void SymbolSupport::handleFindReferencesResponse(const FindReferencesRequest::Response &response,
193 const QString &wordUnderCursor,
194 const ResultHandler &handler)
195 {
196 const auto result = response.result();
197 if (handler) {
198 const LanguageClientArray<Location> locations = result.value_or(nullptr);
199 handler(locations.isNull() ? QList<Location>() : locations.toList());
200 return;
201 }
202 if (result) {
203 Core::SearchResult *search = Core::SearchResultWindow::instance()->startNewSearch(
204 tr("Find References with %1 for:").arg(m_client->name()), "", wordUnderCursor);
205 search->addResults(generateSearchResultItems(result.value()),
206 Core::SearchResult::AddOrdered);
207 QObject::connect(search,
208 &Core::SearchResult::activated,
209 [](const Core::SearchResultItem &item) {
210 Core::EditorManager::openEditorAtSearchResult(item);
211 });
212 search->finishSearch(false);
213 search->popup();
214 }
215 }
216
findUsages(TextEditor::TextDocument * document,const QTextCursor & cursor,const ResultHandler & handler)217 Utils::optional<MessageId> SymbolSupport::findUsages(
218 TextEditor::TextDocument *document, const QTextCursor &cursor, const ResultHandler &handler)
219 {
220 if (!m_client->reachable())
221 return {};
222 ReferenceParams params(generateDocPosParams(document, cursor));
223 params.setContext(ReferenceParams::ReferenceContext(true));
224 FindReferencesRequest request(params);
225 QTextCursor termCursor(cursor);
226 termCursor.select(QTextCursor::WordUnderCursor);
227 request.setResponseCallback([this, wordUnderCursor = termCursor.selectedText(), handler](
228 const FindReferencesRequest::Response &response) {
229 handleFindReferencesResponse(response, wordUnderCursor, handler);
230 });
231
232 sendTextDocumentPositionParamsRequest(m_client,
233 request,
234 m_client->dynamicCapabilities(),
235 m_client->capabilities());
236 return request.id();
237 }
238
supportsRename(Client * client,TextEditor::TextDocument * document,bool & prepareSupported)239 static bool supportsRename(Client *client,
240 TextEditor::TextDocument *document,
241 bool &prepareSupported)
242 {
243 if (!client->reachable())
244 return false;
245 prepareSupported = false;
246 if (client->dynamicCapabilities().isRegistered(RenameRequest::methodName)) {
247 QJsonObject options
248 = client->dynamicCapabilities().option(RenameRequest::methodName).toObject();
249 prepareSupported = ServerCapabilities::RenameOptions(options).prepareProvider().value_or(
250 false);
251 const TextDocumentRegistrationOptions docOps(options);
252 if (docOps.isValid()
253 && !docOps.filterApplies(document->filePath(),
254 Utils::mimeTypeForName(document->mimeType()))) {
255 return false;
256 }
257 }
258 if (auto renameProvider = client->capabilities().renameProvider()) {
259 if (Utils::holds_alternative<bool>(*renameProvider)) {
260 if (!Utils::get<bool>(*renameProvider))
261 return false;
262 } else if (Utils::holds_alternative<ServerCapabilities::RenameOptions>(*renameProvider)) {
263 prepareSupported = Utils::get<ServerCapabilities::RenameOptions>(*renameProvider)
264 .prepareProvider()
265 .value_or(false);
266 }
267 } else {
268 return false;
269 }
270 return true;
271 }
272
supportsRename(TextEditor::TextDocument * document)273 bool SymbolSupport::supportsRename(TextEditor::TextDocument *document)
274 {
275 bool prepareSupported;
276 return LanguageClient::supportsRename(m_client, document, prepareSupported);
277 }
278
renameSymbol(TextEditor::TextDocument * document,const QTextCursor & cursor)279 void SymbolSupport::renameSymbol(TextEditor::TextDocument *document, const QTextCursor &cursor)
280 {
281 bool prepareSupported;
282 if (!LanguageClient::supportsRename(m_client, document, prepareSupported))
283 return;
284
285 QTextCursor tc = cursor;
286 tc.select(QTextCursor::WordUnderCursor);
287 if (prepareSupported)
288 requestPrepareRename(generateDocPosParams(document, cursor), tc.selectedText());
289 else
290 startRenameSymbol(generateDocPosParams(document, cursor), tc.selectedText());
291 }
292
requestPrepareRename(const TextDocumentPositionParams & params,const QString & placeholder)293 void SymbolSupport::requestPrepareRename(const TextDocumentPositionParams ¶ms,
294 const QString &placeholder)
295 {
296 PrepareRenameRequest request(params);
297 request.setResponseCallback([this, params, placeholder](
298 const PrepareRenameRequest::Response &response) {
299 const Utils::optional<PrepareRenameRequest::Response::Error> &error = response.error();
300 if (error.has_value())
301 m_client->log(*error);
302
303 const Utils::optional<PrepareRenameResult> &result = response.result();
304 if (result.has_value()) {
305 if (Utils::holds_alternative<PlaceHolderResult>(*result)) {
306 auto placeHolderResult = Utils::get<PlaceHolderResult>(*result);
307 startRenameSymbol(params, placeHolderResult.placeHolder());
308 } else if (Utils::holds_alternative<Range>(*result)) {
309 auto range = Utils::get<Range>(*result);
310 startRenameSymbol(params, placeholder);
311 }
312 }
313 });
314 m_client->sendContent(request);
315 }
316
requestRename(const TextDocumentPositionParams & positionParams,const QString & newName,Core::SearchResult * search)317 void SymbolSupport::requestRename(const TextDocumentPositionParams &positionParams,
318 const QString &newName,
319 Core::SearchResult *search)
320 {
321 RenameParams params(positionParams);
322 params.setNewName(newName);
323 RenameRequest request(params);
324 request.setResponseCallback([this, search](const RenameRequest::Response &response) {
325 handleRenameResponse(search, response);
326 });
327 m_client->sendContent(request);
328 search->setTextToReplace(newName);
329 search->popup();
330 }
331
generateReplaceItems(const WorkspaceEdit & edits)332 QList<Core::SearchResultItem> generateReplaceItems(const WorkspaceEdit &edits)
333 {
334 auto convertEdits = [](const QList<TextEdit> &edits) {
335 return Utils::transform(edits, [](const TextEdit &edit) {
336 return ItemData{SymbolSupport::convertRange(edit.range()), QVariant(edit)};
337 });
338 };
339 QMap<Utils::FilePath, QList<ItemData>> rangesInDocument;
340 auto documentChanges = edits.documentChanges().value_or(QList<TextDocumentEdit>());
341 if (!documentChanges.isEmpty()) {
342 for (const TextDocumentEdit &documentChange : qAsConst(documentChanges)) {
343 rangesInDocument[documentChange.textDocument().uri().toFilePath()] = convertEdits(
344 documentChange.edits());
345 }
346 } else {
347 auto changes = edits.changes().value_or(WorkspaceEdit::Changes());
348 for (auto it = changes.begin(), end = changes.end(); it != end; ++it)
349 rangesInDocument[it.key().toFilePath()] = convertEdits(it.value());
350 }
351 return generateSearchResultItems(rangesInDocument);
352 }
353
startRenameSymbol(const TextDocumentPositionParams & positionParams,const QString & placeholder)354 void SymbolSupport::startRenameSymbol(const TextDocumentPositionParams &positionParams,
355 const QString &placeholder)
356 {
357 Core::SearchResult *search = Core::SearchResultWindow::instance()->startNewSearch(
358 tr("Find References with %1 for:").arg(m_client->name()),
359 "",
360 placeholder,
361 Core::SearchResultWindow::SearchAndReplace);
362 search->setSearchAgainSupported(true);
363 auto label = new QLabel(tr("Search Again to update results and re-enable Replace"));
364 label->setVisible(false);
365 search->setAdditionalReplaceWidget(label);
366 QObject::connect(search, &Core::SearchResult::activated, [](const Core::SearchResultItem &item) {
367 Core::EditorManager::openEditorAtSearchResult(item);
368 });
369 QObject::connect(search, &Core::SearchResult::replaceTextChanged, [search]() {
370 search->additionalReplaceWidget()->setVisible(true);
371 search->setSearchAgainEnabled(true);
372 search->setReplaceEnabled(false);
373 });
374 QObject::connect(search,
375 &Core::SearchResult::searchAgainRequested,
376 [this, positionParams, search]() {
377 search->restart();
378 requestRename(positionParams, search->textToReplace(), search);
379 });
380 QObject::connect(search,
381 &Core::SearchResult::replaceButtonClicked,
382 [this, positionParams](const QString & /*replaceText*/,
383 const QList<Core::SearchResultItem> &checkedItems) {
384 applyRename(checkedItems);
385 });
386
387 requestRename(positionParams, placeholder, search);
388 }
389
handleRenameResponse(Core::SearchResult * search,const RenameRequest::Response & response)390 void SymbolSupport::handleRenameResponse(Core::SearchResult *search,
391 const RenameRequest::Response &response)
392 {
393 const Utils::optional<PrepareRenameRequest::Response::Error> &error = response.error();
394 if (error.has_value())
395 m_client->log(*error);
396
397 const Utils::optional<WorkspaceEdit> &edits = response.result();
398 if (edits.has_value()) {
399 search->addResults(generateReplaceItems(*edits), Core::SearchResult::AddOrdered);
400 search->additionalReplaceWidget()->setVisible(false);
401 search->setReplaceEnabled(true);
402 search->setSearchAgainEnabled(false);
403 search->finishSearch(false);
404 } else {
405 search->finishSearch(true);
406 }
407 }
408
applyRename(const QList<Core::SearchResultItem> & checkedItems)409 void SymbolSupport::applyRename(const QList<Core::SearchResultItem> &checkedItems)
410 {
411 QMap<DocumentUri, QList<TextEdit>> editsForDocuments;
412 for (const Core::SearchResultItem &item : checkedItems) {
413 auto uri = DocumentUri::fromFilePath(Utils::FilePath::fromString(item.path().value(0)));
414 TextEdit edit(item.userData().toJsonObject());
415 if (edit.isValid())
416 editsForDocuments[uri] << edit;
417 }
418
419 for (auto it = editsForDocuments.begin(), end = editsForDocuments.end(); it != end; ++it)
420 applyTextEdits(it.key(), it.value());
421 }
422
convertRange(const Range & range)423 Core::Search::TextRange SymbolSupport::convertRange(const Range &range)
424 {
425 auto convertPosition = [](const Position &pos) {
426 return Core::Search::TextPosition(pos.line() + 1, pos.character());
427 };
428 return Core::Search::TextRange(convertPosition(range.start()), convertPosition(range.end()));
429 }
430
431 } // namespace LanguageClient
432