From 55a10b0e7acd95fbe4af7ce7004545c2b6786499 Mon Sep 17 00:00:00 2001 From: David Schulz Date: Wed, 8 Jan 2020 12:03:54 +0100 Subject: [PATCH] LanguageClient: add lsp based auto formatter Change-Id: I2a7347961b4633868aa3b033c351a1e709c3597e Reviewed-by: Christian Stenger --- src/libs/languageserverprotocol/lsptypes.cpp | 8 + src/libs/languageserverprotocol/lsptypes.h | 7 +- src/plugins/languageclient/CMakeLists.txt | 1 + src/plugins/languageclient/client.cpp | 114 +++++++++++++- src/plugins/languageclient/client.h | 8 +- src/plugins/languageclient/languageclient.pro | 2 + src/plugins/languageclient/languageclient.qbs | 2 + .../languageclientformatter.cpp | 146 ++++++++++++++++++ .../languageclient/languageclientformatter.h | 57 +++++++ 9 files changed, 333 insertions(+), 12 deletions(-) create mode 100644 src/plugins/languageclient/languageclientformatter.cpp create mode 100644 src/plugins/languageclient/languageclientformatter.h diff --git a/src/libs/languageserverprotocol/lsptypes.cpp b/src/libs/languageserverprotocol/lsptypes.cpp index 6b8bc0f7498..ac5c4224dd9 100644 --- a/src/libs/languageserverprotocol/lsptypes.cpp +++ b/src/libs/languageserverprotocol/lsptypes.cpp @@ -432,4 +432,12 @@ LanguageServerProtocol::MarkupKind::operator QJsonValue() const return {}; } +Utils::Text::Replacement TextEdit::toReplacement(QTextDocument *document) const +{ + const Range &range = this->range(); + const int start = range.start().toPositionInDocument(document); + const int end = range.end().toPositionInDocument(document); + return Utils::Text::Replacement(start, end - start, newText()); +} + } // namespace LanguageServerProtocol diff --git a/src/libs/languageserverprotocol/lsptypes.h b/src/libs/languageserverprotocol/lsptypes.h index fca410636db..804284252b2 100644 --- a/src/libs/languageserverprotocol/lsptypes.h +++ b/src/libs/languageserverprotocol/lsptypes.h @@ -30,9 +30,10 @@ #include "lsputils.h" #include -#include -#include #include +#include +#include +#include #include #include @@ -222,6 +223,8 @@ public: QString newText() const { return typedValue(newTextKey); } void setNewText(const QString &text) { insert(newTextKey, text); } + Utils::Text::Replacement toReplacement(QTextDocument *document) const; + bool isValid(QStringList *error) const override { return check(error, rangeKey) && check(error, newTextKey); } }; diff --git a/src/plugins/languageclient/CMakeLists.txt b/src/plugins/languageclient/CMakeLists.txt index a1366444e54..d1910757dbc 100644 --- a/src/plugins/languageclient/CMakeLists.txt +++ b/src/plugins/languageclient/CMakeLists.txt @@ -8,6 +8,7 @@ add_qtc_plugin(LanguageClient languageclient.qrc languageclientcompletionassist.cpp languageclientcompletionassist.h languageclientfunctionhint.cpp languageclientfunctionhint.h + languageclientformatter.cpp languageclientformatter.h languageclienthoverhandler.cpp languageclienthoverhandler.h languageclientinterface.cpp languageclientinterface.h languageclientmanager.cpp languageclientmanager.h diff --git a/src/plugins/languageclient/client.cpp b/src/plugins/languageclient/client.cpp index 4fa092d0dec..852142c1745 100644 --- a/src/plugins/languageclient/client.cpp +++ b/src/plugins/languageclient/client.cpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include @@ -353,13 +354,13 @@ void Client::cancelRequest(const MessageId &id) void Client::closeDocument(TextEditor::TextDocument *document) { - if (m_openedDocument.remove(document) == 0) - return; - const DocumentUri &uri = DocumentUri::fromFilePath(document->filePath()); - const DidCloseTextDocumentParams params(TextDocumentIdentifier{uri}); - m_highlights[uri].clear(); - sendContent(uri, DidCloseTextDocumentNotification(params)); deactivateDocument(document); + const DocumentUri &uri = DocumentUri::fromFilePath(document->filePath()); + m_highlights[uri].clear(); + if (m_openedDocument.remove(document) != 0) { + DidCloseTextDocumentParams params(TextDocumentIdentifier{uri}); + sendContent(DidCloseTextDocumentNotification(params)); + } } void Client::activateDocument(TextEditor::TextDocument *document) @@ -378,6 +379,7 @@ void Client::activateDocument(TextEditor::TextDocument *document) document->setFunctionHintAssistProvider(m_clientProviders.functionHintProvider); document->setQuickFixAssistProvider(m_clientProviders.quickFixAssistProvider); } + document->setFormatter(new LanguageClientFormatter(document, this)); for (Core::IEditor *editor : Core::DocumentModel::editorsForDocument(document)) { updateEditorToolBar(editor); if (auto textEditor = qobject_cast(editor)) @@ -389,6 +391,7 @@ void Client::deactivateDocument(TextEditor::TextDocument *document) { hideDiagnostics(document); resetAssistProviders(document); + document->setFormatter(nullptr); if (TextEditor::SyntaxHighlighter *highlighter = document->syntaxHighlighter()) highlighter->clearAllExtraFormats(); for (Core::IEditor *editor : Core::DocumentModel::editorsForDocument(document)) { @@ -397,9 +400,9 @@ void Client::deactivateDocument(TextEditor::TextDocument *document) } } -bool Client::documentOpen(TextEditor::TextDocument *document) const +bool Client::documentOpen(const TextEditor::TextDocument *document) const { - return m_openedDocument.contains(document); + return m_openedDocument.contains(const_cast(document)); } void Client::documentContentsSaved(TextEditor::TextDocument *document) @@ -716,6 +719,101 @@ void Client::executeCommand(const Command &command) sendContent(request); } +static const FormattingOptions formattingOptions(const TextEditor::TabSettings &settings) +{ + FormattingOptions options; + options.setTabSize(settings.m_tabSize); + options.setInsertSpace(settings.m_tabPolicy == TextEditor::TabSettings::SpacesOnlyTabPolicy); + return options; +} + +template +static void handleFormattingResponse(const DocumentUri &uri, + const QPointer client, + const FormattingResponse &response) +{ + if (client) { + if (const Utils::optional &error = response.error()) + client->log(*error); + } + if (Utils::optional> result = response.result()) { + if (!result->isNull()) { + applyTextEdits(uri, result->toList()); + } + } + +} + +void Client::formatFile(const TextEditor::TextDocument *document) +{ + if (!isSupportedDocument(document)) + return; + + const FilePath &filePath = document->filePath(); + const QString method(DocumentFormattingRequest::methodName); + if (Utils::optional registered = m_dynamicCapabilities.isRegistered(method)) { + if (!registered.value()) + return; + const TextDocumentRegistrationOptions option( + m_dynamicCapabilities.option(method).toObject()); + if (option.isValid(nullptr) + && !option.filterApplies(filePath, Utils::mimeTypeForName(document->mimeType()))) { + return; + } + } else if (!m_serverCapabilities.documentFormattingProvider().value_or(false)) { + return; + } + + DocumentFormattingParams params; + const DocumentUri uri = DocumentUri::fromFilePath(filePath); + params.setTextDocument(uri); + params.setOptions(formattingOptions(document->tabSettings())); + DocumentFormattingRequest request(params); + request.setResponseCallback( + [uri, self = QPointer(this)](const DocumentFormattingRequest::Response &response) { + handleFormattingResponse(uri, self, response); + }); + sendContent(request); +} + +void Client::formatRange(const TextEditor::TextDocument *document, const QTextCursor &cursor) +{ + if (!isSupportedDocument(document)) + return; + + const FilePath &filePath = document->filePath(); + const QString method(DocumentRangeFormattingRequest::methodName); + if (Utils::optional registered = m_dynamicCapabilities.isRegistered(method)) { + if (!registered.value()) + return; + const TextDocumentRegistrationOptions option( + m_dynamicCapabilities.option(method).toObject()); + if (option.isValid(nullptr) + && !option.filterApplies(filePath, Utils::mimeTypeForName(document->mimeType()))) { + return; + } + } else if (!m_serverCapabilities.documentRangeFormattingProvider().value_or(false)) { + return; + } + DocumentRangeFormattingParams params; + const DocumentUri uri = DocumentUri::fromFilePath(filePath); + params.setTextDocument(uri); + params.setOptions(formattingOptions(document->tabSettings())); + if (!cursor.hasSelection()) { + QTextCursor c = cursor; + c.select(QTextCursor::LineUnderCursor); + params.setRange(Range(c)); + } else { + params.setRange(Range(cursor)); + } + DocumentRangeFormattingRequest request(params); + request.setResponseCallback([uri, self = QPointer(this)]( + const DocumentRangeFormattingRequest::Response &response) { + handleFormattingResponse(uri, self, response); + }); + sendContent(request); +} + const ProjectExplorer::Project *Client::project() const { return m_project; diff --git a/src/plugins/languageclient/client.h b/src/plugins/languageclient/client.h index 86f3e8f4837..b2a91307f14 100644 --- a/src/plugins/languageclient/client.h +++ b/src/plugins/languageclient/client.h @@ -29,10 +29,11 @@ #include "dynamiccapabilities.h" #include "languageclient_global.h" #include "languageclientcompletionassist.h" +#include "languageclientformatter.h" #include "languageclientfunctionhint.h" +#include "languageclienthoverhandler.h" #include "languageclientquickfix.h" #include "languageclientsettings.h" -#include "languageclienthoverhandler.h" #include #include @@ -100,7 +101,7 @@ public: void closeDocument(TextEditor::TextDocument *document); void activateDocument(TextEditor::TextDocument *document); void deactivateDocument(TextEditor::TextDocument *document); - bool documentOpen(TextEditor::TextDocument *document) const; + bool documentOpen(const TextEditor::TextDocument *document) const; void documentContentsSaved(TextEditor::TextDocument *document); void documentWillSave(Core::IDocument *document); void documentContentsChanged(TextEditor::TextDocument *document, @@ -120,6 +121,9 @@ public: const LanguageServerProtocol::DocumentUri &uri); void executeCommand(const LanguageServerProtocol::Command &command); + void formatFile(const TextEditor::TextDocument *document); + void formatRange(const TextEditor::TextDocument *document, const QTextCursor &cursor); + // workspace control void setCurrentProject(ProjectExplorer::Project *project); const ProjectExplorer::Project *project() const; diff --git a/src/plugins/languageclient/languageclient.pro b/src/plugins/languageclient/languageclient.pro index 755c0bcc29c..5db41008d1c 100644 --- a/src/plugins/languageclient/languageclient.pro +++ b/src/plugins/languageclient/languageclient.pro @@ -8,6 +8,7 @@ HEADERS += \ dynamiccapabilities.h \ languageclient_global.h \ languageclientcompletionassist.h \ + languageclientformatter.h \ languageclientfunctionhint.h \ languageclienthoverhandler.h \ languageclientinterface.h \ @@ -26,6 +27,7 @@ SOURCES += \ documentsymbolcache.cpp \ dynamiccapabilities.cpp \ languageclientcompletionassist.cpp \ + languageclientformatter.cpp \ languageclientfunctionhint.cpp \ languageclienthoverhandler.cpp \ languageclientinterface.cpp \ diff --git a/src/plugins/languageclient/languageclient.qbs b/src/plugins/languageclient/languageclient.qbs index 5ff9c340755..ce1583381de 100644 --- a/src/plugins/languageclient/languageclient.qbs +++ b/src/plugins/languageclient/languageclient.qbs @@ -22,6 +22,8 @@ QtcPlugin { "dynamiccapabilities.h", "languageclient.qrc", "languageclient_global.h", + "languageclientformatter.cpp", + "languageclientformatter.h", "languageclienthoverhandler.cpp", "languageclienthoverhandler.h", "languageclientfunctionhint.cpp", diff --git a/src/plugins/languageclient/languageclientformatter.cpp b/src/plugins/languageclient/languageclientformatter.cpp new file mode 100644 index 00000000000..4097d000ce0 --- /dev/null +++ b/src/plugins/languageclient/languageclientformatter.cpp @@ -0,0 +1,146 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#include "languageclientformatter.h" + +#include "languageclientutils.h" + +#include +#include +#include + +#include + +using namespace LanguageServerProtocol; +using namespace Utils; + +namespace LanguageClient { + +LanguageClientFormatter::LanguageClientFormatter(TextEditor::TextDocument *document, Client *client) + : m_client(client) + , m_document(document) +{ + m_cancelConnection = QObject::connect(document->document(), + &QTextDocument::contentsChanged, + [this]() { + if (m_ignoreCancel) + m_ignoreCancel = false; + else + cancelCurrentRequest(); + }); +} + +LanguageClientFormatter::~LanguageClientFormatter() +{ + QObject::disconnect(m_cancelConnection); + cancelCurrentRequest(); +} + +static const FormattingOptions formattingOptions(const TextEditor::TabSettings &settings) +{ + FormattingOptions options; + options.setTabSize(settings.m_tabSize); + options.setInsertSpace(settings.m_tabPolicy == TextEditor::TabSettings::SpacesOnlyTabPolicy); + return options; +} + +QFutureWatcher *LanguageClientFormatter::format( + const QTextCursor &cursor, const TextEditor::TabSettings &tabSettings) +{ + cancelCurrentRequest(); + m_progress = QFutureInterface(); + + const FilePath &filePath = m_document->filePath(); + const DynamicCapabilities dynamicCapabilities = m_client->dynamicCapabilities(); + const QString method(DocumentRangeFormattingRequest::methodName); + if (Utils::optional registered = dynamicCapabilities.isRegistered(method)) { + if (!registered.value()) + return nullptr; + const TextDocumentRegistrationOptions option(dynamicCapabilities.option(method).toObject()); + if (option.isValid(nullptr) + && !option.filterApplies(filePath, Utils::mimeTypeForName(m_document->mimeType()))) { + return nullptr; + } + } else if (!m_client->capabilities().documentRangeFormattingProvider().value_or(false)) { + return nullptr; + } + DocumentRangeFormattingParams params; + const DocumentUri uri = DocumentUri::fromFilePath(filePath); + params.setTextDocument(uri); + params.setOptions(formattingOptions(tabSettings)); + if (!cursor.hasSelection()) { + QTextCursor c = cursor; + c.select(QTextCursor::LineUnderCursor); + params.setRange(Range(c)); + } else { + params.setRange(Range(cursor)); + } + DocumentRangeFormattingRequest request(params); + request.setResponseCallback([this](const DocumentRangeFormattingRequest::Response &response) { + handleResponse(response); + }); + m_currentRequest = request.id(); + m_client->sendContent(request); + // ignore first contents changed, because this function is called inside a begin/endEdit block + m_ignoreCancel = true; + m_progress.reportStarted(); + auto watcher = new QFutureWatcher(); + watcher->setFuture(m_progress.future()); + QObject::connect(watcher, &QFutureWatcher::canceled, [this]() { + cancelCurrentRequest(); + }); + return watcher; +} + +void LanguageClientFormatter::cancelCurrentRequest() +{ + if (m_currentRequest.has_value()) { + m_progress.reportCanceled(); + m_progress.reportFinished(); + m_client->cancelRequest(*m_currentRequest); + m_ignoreCancel = false; + m_currentRequest = nullopt; + } +} + +void LanguageClientFormatter::handleResponse(const DocumentRangeFormattingRequest::Response &response) +{ + m_currentRequest = nullopt; + if (const optional &error = response.error()) + m_client->log(*error); + Text::Replacements replacements; + if (optional> result = response.result()) { + if (!result->isNull()) { + const QList results = result->toList(); + replacements.reserve(results.size()); + for (const TextEdit &edit : results) + replacements.emplace_back(edit.toReplacement(m_document->document())); + } + } + m_progress.reportResult(replacements); + m_progress.reportFinished(); +} + +} // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientformatter.h b/src/plugins/languageclient/languageclientformatter.h new file mode 100644 index 00000000000..a9be3f7cab6 --- /dev/null +++ b/src/plugins/languageclient/languageclientformatter.h @@ -0,0 +1,57 @@ +/**************************************************************************** +** +** Copyright (C) 2019 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of Qt Creator. +** +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 3 as published by the Free Software +** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-3.0.html. +** +****************************************************************************/ + +#pragma once + +#include "client.h" + +#include + +namespace TextEditor { class TextDocument; } +namespace LanguageClient { + +class LanguageClientFormatter : public TextEditor::Formatter +{ +public: + LanguageClientFormatter(TextEditor::TextDocument *document, Client *client); + ~LanguageClientFormatter() override; + + QFutureWatcher *format( + const QTextCursor &cursor, const TextEditor::TabSettings &tabSettings) override; + +private: + void cancelCurrentRequest(); + void handleResponse( + const LanguageServerProtocol::DocumentRangeFormattingRequest::Response &response); + + Client *m_client = nullptr; // not owned + QMetaObject::Connection m_cancelConnection; + TextEditor::TextDocument *m_document; // not owned + bool m_ignoreCancel = false; + QFutureInterface m_progress; + Utils::optional m_currentRequest; +}; + +} // namespace LanguageClient