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 &params,
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