diff --git a/src/plugins/coreplugin/find/searchresultwidget.cpp b/src/plugins/coreplugin/find/searchresultwidget.cpp index bfa941edae1..ce1e079303c 100644 --- a/src/plugins/coreplugin/find/searchresultwidget.cpp +++ b/src/plugins/coreplugin/find/searchresultwidget.cpp @@ -441,6 +441,11 @@ void SearchResultWidget::setSearchAgainEnabled(bool enabled) m_searchAgainButton->setEnabled(enabled); } +void SearchResultWidget::setReplaceEnabled(bool enabled) +{ + m_replaceButton->setEnabled(enabled); +} + void SearchResultWidget::finishSearch(bool canceled) { Id sizeWarningId(SIZE_WARNING_ID); diff --git a/src/plugins/coreplugin/find/searchresultwidget.h b/src/plugins/coreplugin/find/searchresultwidget.h index ee91ded1322..a2c63af482e 100644 --- a/src/plugins/coreplugin/find/searchresultwidget.h +++ b/src/plugins/coreplugin/find/searchresultwidget.h @@ -91,6 +91,8 @@ public: void setSearchAgainSupported(bool supported); void setSearchAgainEnabled(bool enabled); + void setReplaceEnabled(bool enabled); + public slots: void finishSearch(bool canceled); void sendRequestPopup(); diff --git a/src/plugins/coreplugin/find/searchresultwindow.cpp b/src/plugins/coreplugin/find/searchresultwindow.cpp index c9380011a44..ce4e6f3b833 100644 --- a/src/plugins/coreplugin/find/searchresultwindow.cpp +++ b/src/plugins/coreplugin/find/searchresultwindow.cpp @@ -858,6 +858,14 @@ void SearchResult::setTextToReplace(const QString &textToReplace) m_widget->setTextToReplace(textToReplace); } +/*! + Sets whether replace is enabled and can be triggered by the user +*/ +void SearchResult::setReplaceEnabled(bool enabled) +{ + m_widget->setReplaceEnabled(enabled); +} + /*! * Removes all search results. */ diff --git a/src/plugins/coreplugin/find/searchresultwindow.h b/src/plugins/coreplugin/find/searchresultwindow.h index 6a450cb596b..0ca0a55b635 100644 --- a/src/plugins/coreplugin/find/searchresultwindow.h +++ b/src/plugins/coreplugin/find/searchresultwindow.h @@ -77,6 +77,7 @@ public slots: void finishSearch(bool canceled); void setTextToReplace(const QString &textToReplace); void restart(); + void setReplaceEnabled(bool enabled); void setSearchAgainEnabled(bool enabled); void popup(); diff --git a/src/plugins/languageclient/client.cpp b/src/plugins/languageclient/client.cpp index dcd19fd5c1c..3d675a1e8b4 100644 --- a/src/plugins/languageclient/client.cpp +++ b/src/plugins/languageclient/client.cpp @@ -41,13 +41,14 @@ #include #include #include +#include #include #include #include #include +#include #include #include -#include #include #include #include @@ -235,6 +236,11 @@ static ClientCapabilities generateClientCapabilities() hover.setDynamicRegistration(true); documentCapabilities.setHover(hover); + TextDocumentClientCapabilities::RenameClientCapabilities rename; + rename.setPrepareSupport(true); + rename.setDynamicRegistration(true); + documentCapabilities.setRename(rename); + documentCapabilities.setReferences(allowDynamicRegistration); documentCapabilities.setDocumentHighlight(allowDynamicRegistration); documentCapabilities.setDefinition(allowDynamicRegistration); @@ -398,8 +404,13 @@ void Client::activateDocument(TextEditor::TextDocument *document) document->setFormatter(new LanguageClientFormatter(document, this)); for (Core::IEditor *editor : Core::DocumentModel::editorsForDocument(document)) { updateEditorToolBar(editor); - if (auto textEditor = qobject_cast(editor)) + if (auto textEditor = qobject_cast(editor)) { textEditor->editorWidget()->addHoverHandler(&m_hoverHandler); + if (symbolSupport().supportsRename(document)) { + textEditor->editorWidget()->addOptionalActions( + TextEditor::TextEditorActionHandler::RenameSymbol); + } + } } } diff --git a/src/plugins/languageclient/languageclientmanager.cpp b/src/plugins/languageclient/languageclientmanager.cpp index 41dc0f31b48..85f323916dc 100644 --- a/src/plugins/languageclient/languageclientmanager.cpp +++ b/src/plugins/languageclient/languageclientmanager.cpp @@ -415,6 +415,11 @@ void LanguageClientManager::editorOpened(Core::IEditor *editor) if (auto client = clientForDocument(document)) client->symbolSupport().findUsages(document, cursor); }); + connect(widget, &TextEditorWidget::requestRename, this, + [document = textEditor->textDocument()](const QTextCursor &cursor) { + if (auto client = clientForDocument(document)) + client->symbolSupport().renameSymbol(document, cursor); + }); connect(widget, &TextEditorWidget::cursorPositionChanged, this, [this, widget]() { // TODO This would better be a compressing timer QTimer::singleShot(50, this, [widget = QPointer(widget)]() { diff --git a/src/plugins/languageclient/languageclientsymbolsupport.cpp b/src/plugins/languageclient/languageclientsymbolsupport.cpp index db065ed061e..d0ab7cff286 100644 --- a/src/plugins/languageclient/languageclientsymbolsupport.cpp +++ b/src/plugins/languageclient/languageclientsymbolsupport.cpp @@ -26,10 +26,13 @@ #include "languageclientsymbolsupport.h" #include "client.h" +#include "languageclientutils.h" #include #include +#include + using namespace LanguageServerProtocol; namespace LanguageClient { @@ -120,21 +123,24 @@ void SymbolSupport::findLinkAt(TextEditor::TextDocument *document, } -QList generateSearchResultItems( - const LanguageClientArray &locations) +Core::Search::TextRange convertRange(const Range &range) { auto convertPosition = [](const Position &pos) { return Core::Search::TextPosition(pos.line() + 1, pos.character()); }; - auto convertRange = [convertPosition](const Range &range) { - return Core::Search::TextRange(convertPosition(range.start()), convertPosition(range.end())); - }; + return Core::Search::TextRange(convertPosition(range.start()), convertPosition(range.end())); +} + +struct ItemData +{ + Core::Search::TextRange range; + QVariant userData; +}; + +QList generateSearchResultItems( + const QMap> &rangesInDocument) +{ QList result; - if (locations.isNull()) - return result; - QMap> rangesInDocument; - for (const Location &location : locations.toList()) - rangesInDocument[location.uri().toFilePath().toString()] << convertRange(location.range()); for (auto it = rangesInDocument.begin(); it != rangesInDocument.end(); ++it) { const QString &fileName = it.key(); QFile file(fileName); @@ -145,18 +151,31 @@ QList generateSearchResultItems( item.useTextEditorFont = true; QStringList lines = QString::fromLocal8Bit(file.readAll()).split(QChar::LineFeed); - for (const Core::Search::TextRange &range : it.value()) { - item.mainRange = range; - if (file.isOpen() && range.begin.line > 0 && range.begin.line <= lines.size()) - item.text = lines[range.begin.line - 1]; + for (const ItemData &data : it.value()) { + item.mainRange = data.range; + if (file.isOpen() && data.range.begin.line > 0 && data.range.begin.line <= lines.size()) + item.text = lines[data.range.begin.line - 1]; else item.text.clear(); + item.userData = data.userData; result << item; } } return result; } +QList generateSearchResultItems( + const LanguageClientArray &locations) +{ + if (locations.isNull()) + return {}; + QMap> rangesInDocument; + for (const Location &location : locations.toList()) + rangesInDocument[location.uri().toFilePath().toString()] + << ItemData{convertRange(location.range()), {}}; + return generateSearchResultItems(rangesInDocument); +} + void SymbolSupport::handleFindReferencesResponse(const FindReferencesRequest::Response &response, const QString &wordUnderCursor) { @@ -195,4 +214,188 @@ void SymbolSupport::findUsages(TextEditor::TextDocument *document, const QTextCu m_client->capabilities().referencesProvider()); } +static bool supportsRename(Client *client, + TextEditor::TextDocument *document, + bool &prepareSupported) +{ + if (!client->reachable()) + return false; + prepareSupported = false; + if (client->dynamicCapabilities().isRegistered(RenameRequest::methodName)) { + QJsonObject options + = client->dynamicCapabilities().option(RenameRequest::methodName).toObject(); + prepareSupported = ServerCapabilities::RenameOptions(options).prepareProvider().value_or( + false); + const TextDocumentRegistrationOptions docOps(options); + if (docOps.isValid(nullptr) + && !docOps.filterApplies(document->filePath(), + Utils::mimeTypeForName(document->mimeType()))) { + return false; + } + } + if (auto renameProvider = client->capabilities().renameProvider()) { + if (Utils::holds_alternative(*renameProvider)) { + if (!Utils::get(*renameProvider)) + return false; + } else if (Utils::holds_alternative(*renameProvider)) { + prepareSupported = Utils::get(*renameProvider) + .prepareProvider() + .value_or(false); + } + } else { + return false; + } + return true; +} + +bool SymbolSupport::supportsRename(TextEditor::TextDocument *document) +{ + bool prepareSupported; + return LanguageClient::supportsRename(m_client, document, prepareSupported); +} + +void SymbolSupport::renameSymbol(TextEditor::TextDocument *document, const QTextCursor &cursor) +{ + bool prepareSupported; + if (!LanguageClient::supportsRename(m_client, document, prepareSupported)) + return; + + QTextCursor tc = cursor; + tc.select(QTextCursor::WordUnderCursor); + if (prepareSupported) + requestPrepareRename(generateDocPosParams(document, cursor), tc.selectedText()); + else + startRenameSymbol(generateDocPosParams(document, cursor), tc.selectedText()); +} + +void SymbolSupport::requestPrepareRename(const TextDocumentPositionParams ¶ms, + const QString &placeholder) +{ + PrepareRenameRequest request(params); + request.setResponseCallback([this, params, placeholder]( + const PrepareRenameRequest::Response &response) { + const Utils::optional &error = response.error(); + if (error.has_value()) + m_client->log(*error); + + const Utils::optional &result = response.result(); + if (result.has_value()) { + if (Utils::holds_alternative(*result)) { + auto placeHolderResult = Utils::get(*result); + startRenameSymbol(params, placeHolderResult.placeHolder()); + } else if (Utils::holds_alternative(*result)) { + auto range = Utils::get(*result); + startRenameSymbol(params, placeholder); + } + } + }); + m_client->sendContent(request); +} + +void SymbolSupport::requestRename(const TextDocumentPositionParams &positionParams, + const QString &newName, + Core::SearchResult *search) +{ + RenameParams params(positionParams); + params.setNewName(newName); + RenameRequest request(params); + request.setResponseCallback([this, search](const RenameRequest::Response &response) { + handleRenameResponse(search, response); + }); + m_client->sendContent(request); + search->setTextToReplace(newName); + search->popup(); +} + +QList generateReplaceItems(const WorkspaceEdit &edits) +{ + auto convertEdits = [](const QList &edits) { + return Utils::transform(edits, [](const TextEdit &edit) { + return ItemData{convertRange(edit.range()), QVariant(edit)}; + }); + }; + QMap> rangesInDocument; + auto documentChanges = edits.documentChanges().value_or(QList()); + if (!documentChanges.isEmpty()) { + for (const TextDocumentEdit &documentChange : qAsConst(documentChanges)) { + rangesInDocument[documentChange.textDocument().uri().toFilePath().toString()] = convertEdits( + documentChange.edits()); + } + } else { + auto changes = edits.changes().value_or(WorkspaceEdit::Changes()); + for (auto it = changes.begin(), end = changes.end(); it != end; ++it) + rangesInDocument[it.key().toFilePath().toString()] = convertEdits(it.value()); + } + return generateSearchResultItems(rangesInDocument); +} + +void SymbolSupport::startRenameSymbol(const TextDocumentPositionParams &positionParams, + const QString &placeholder) +{ + Core::SearchResult *search = Core::SearchResultWindow::instance()->startNewSearch( + tr("Find References with %1 for:").arg(m_client->name()), + "", + placeholder, + Core::SearchResultWindow::SearchAndReplace); + search->setSearchAgainSupported(true); + auto label = new QLabel(tr("Search Again to update results and re-enable Replace")); + label->setVisible(false); + search->setAdditionalReplaceWidget(label); + QObject::connect(search, &Core::SearchResult::activated, [](const Core::SearchResultItem &item) { + Core::EditorManager::openEditorAtSearchResult(item); + }); + QObject::connect(search, &Core::SearchResult::replaceTextChanged, [search]() { + search->additionalReplaceWidget()->setVisible(true); + search->setSearchAgainEnabled(true); + search->setReplaceEnabled(false); + }); + QObject::connect(search, + &Core::SearchResult::searchAgainRequested, + [this, positionParams, search]() { + search->restart(); + requestRename(positionParams, search->textToReplace(), search); + }); + QObject::connect(search, + &Core::SearchResult::replaceButtonClicked, + [this, positionParams](const QString & /*replaceText*/, + const QList &checkedItems) { + applyRename(checkedItems); + }); + + requestRename(positionParams, placeholder, search); +} + +void SymbolSupport::handleRenameResponse(Core::SearchResult *search, + const RenameRequest::Response &response) +{ + const Utils::optional &error = response.error(); + if (error.has_value()) + m_client->log(*error); + + const Utils::optional &edits = response.result(); + if (edits.has_value()) { + search->addResults(generateReplaceItems(*edits), Core::SearchResult::AddOrdered); + search->additionalReplaceWidget()->setVisible(false); + search->setReplaceEnabled(true); + search->setSearchAgainEnabled(false); + search->finishSearch(false); + } else { + search->finishSearch(true); + } +} + +void SymbolSupport::applyRename(const QList &checkedItems) +{ + QMap> editsForDocuments; + for (const Core::SearchResultItem &item : checkedItems) { + auto uri = DocumentUri::fromFilePath(Utils::FilePath::fromString(item.path.value(0))); + TextEdit edit(item.userData.toJsonObject()); + if (edit.isValid(nullptr)) + editsForDocuments[uri] << edit; + } + + for (auto it = editsForDocuments.begin(), end = editsForDocuments.end(); it != end; ++it) + applyTextEdits(it.key(), it.value()); +} + } // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientsymbolsupport.h b/src/plugins/languageclient/languageclientsymbolsupport.h index 7d09545e7f3..61dd8e3d053 100644 --- a/src/plugins/languageclient/languageclientsymbolsupport.h +++ b/src/plugins/languageclient/languageclientsymbolsupport.h @@ -29,6 +29,11 @@ #include +namespace Core { +class SearchResult; +class SearchResultItem; +} + namespace LanguageClient { class Client; @@ -45,11 +50,24 @@ public: const bool resolveTarget); void findUsages(TextEditor::TextDocument *document, const QTextCursor &cursor); + bool supportsRename(TextEditor::TextDocument *document); + void renameSymbol(TextEditor::TextDocument *document, const QTextCursor &cursor); + private: void handleFindReferencesResponse( const LanguageServerProtocol::FindReferencesRequest::Response &response, const QString &wordUnderCursor); + void requestPrepareRename(const LanguageServerProtocol::TextDocumentPositionParams ¶ms, + const QString &placeholder); + void requestRename(const LanguageServerProtocol::TextDocumentPositionParams &positionParams, + const QString &newName, Core::SearchResult *search); + void startRenameSymbol(const LanguageServerProtocol::TextDocumentPositionParams ¶ms, + const QString &placeholder); + void handleRenameResponse(Core::SearchResult *search, + const LanguageServerProtocol::RenameRequest::Response &response); + void applyRename(const QList &checkedItems); + Client *m_client = nullptr; };