diff --git a/src/libs/languageserverprotocol/clientcapabilities.cpp b/src/libs/languageserverprotocol/clientcapabilities.cpp index 8401bf83d00..02002504442 100644 --- a/src/libs/languageserverprotocol/clientcapabilities.cpp +++ b/src/libs/languageserverprotocol/clientcapabilities.cpp @@ -106,6 +106,12 @@ TextDocumentClientCapabilities::TextDocumentClientCapabilities() setSynchronization(SynchronizationCapabilities()); setDocumentSymbol(SymbolCapabilities()); setCompletion(CompletionCapabilities()); + CodeActionCapabilities cac; + CodeActionCapabilities::CodeActionLiteralSupport literalSupport; + literalSupport.setCodeActionKind( + CodeActionCapabilities::CodeActionLiteralSupport::CodeActionKind(QList{"*"})); + cac.setCodeActionLiteralSupport(literalSupport); + setCodeAction(cac); } bool TextDocumentClientCapabilities::isValid(QStringList *error) const @@ -123,7 +129,7 @@ bool TextDocumentClientCapabilities::isValid(QStringList *error) const && checkOptional(error, definitionKey) && checkOptional(error, typeDefinitionKey) && checkOptional(error, implementationKey) - && checkOptional(error, codeActionKey) + && checkOptional(error, codeActionKey) && checkOptional(error, codeLensKey) && checkOptional(error, documentLinkKey) && checkOptional(error, colorProviderKey) @@ -168,4 +174,10 @@ bool TextDocumentClientCapabilities::SignatureHelpCapabilities::isValid(QStringL && checkOptional(error, signatureInformationKey); } +bool TextDocumentClientCapabilities::CodeActionCapabilities::isValid(QStringList *errorHierarchy) const +{ + return DynamicRegistrationCapabilities::isValid(errorHierarchy) + && checkOptional(errorHierarchy, codeActionLiteralSupportKey); +} + } // namespace LanguageServerProtocol diff --git a/src/libs/languageserverprotocol/clientcapabilities.h b/src/libs/languageserverprotocol/clientcapabilities.h index 260f498efda..c10341b3f02 100644 --- a/src/libs/languageserverprotocol/clientcapabilities.h +++ b/src/libs/languageserverprotocol/clientcapabilities.h @@ -355,10 +355,53 @@ public: { insert(implementationKey, implementation); } void clearImplementation() { remove(implementationKey); } + class CodeActionCapabilities : public DynamicRegistrationCapabilities + { + public: + using DynamicRegistrationCapabilities::DynamicRegistrationCapabilities; + + class CodeActionLiteralSupport : public JsonObject + { + public: + using JsonObject::JsonObject; + + class CodeActionKind : public JsonObject + { + public: + using JsonObject::JsonObject; + CodeActionKind() : CodeActionKind(QList()) {} + explicit CodeActionKind(const QList &kinds) { setValueSet(kinds); } + + QList valueSet() const { return array(valueSetKey); } + void setValueSet(const QList &valueSet) + { insertArray(valueSetKey, valueSet); } + + bool isValid(QStringList *errorHierarchy) const override + { return checkArray(errorHierarchy, valueSetKey); } + }; + + CodeActionKind codeActionKind() const + { return typedValue(codeActionKindKey); } + void setCodeActionKind(const CodeActionKind &codeActionKind) + { insert(codeActionKindKey, codeActionKind); } + + bool isValid(QStringList *errorHierarchy) const override + { return check(errorHierarchy, codeActionKindKey); } + }; + + Utils::optional codeActionLiteralSupport() const + { return optionalValue(codeActionLiteralSupportKey); } + void setCodeActionLiteralSupport(const CodeActionLiteralSupport &codeActionLiteralSupport) + { insert(codeActionLiteralSupportKey, codeActionLiteralSupport); } + void clearCodeActionLiteralSupport() { remove(codeActionLiteralSupportKey); } + + bool isValid(QStringList *errorHierarchy) const override; + }; + // Whether code action supports dynamic registration. - Utils::optional codeAction() const - { return optionalValue(codeActionKey); } - void setCodeAction(const DynamicRegistrationCapabilities &codeAction) + Utils::optional codeAction() const + { return optionalValue(codeActionKey); } + void setCodeAction(const CodeActionCapabilities &codeAction) { insert(codeActionKey, codeAction); } void clearCodeAction() { remove(codeActionKey); } diff --git a/src/libs/languageserverprotocol/jsonkeys.h b/src/libs/languageserverprotocol/jsonkeys.h index 6fcbcbeb899..6dfc40c252a 100644 --- a/src/libs/languageserverprotocol/jsonkeys.h +++ b/src/libs/languageserverprotocol/jsonkeys.h @@ -45,6 +45,9 @@ constexpr char changesKey[] = "changes"; constexpr char characterKey[] = "character"; constexpr char childrenKey[] = "children"; constexpr char codeActionKey[] = "codeAction"; +constexpr char codeActionKindKey[] = "codeActionKind"; +constexpr char codeActionKindsKey[] = "codeActionKinds"; +constexpr char codeActionLiteralSupportKey[] = "codeActionLiteralSupport"; constexpr char codeActionProviderKey[] = "codeActionProvider"; constexpr char codeKey[] = "code"; constexpr char codeLensKey[] = "codeLens"; @@ -134,6 +137,7 @@ constexpr char nameKey[] = "name"; constexpr char newNameKey[] = "newName"; constexpr char newTextKey[] = "newText"; constexpr char onTypeFormattingKey[] = "onTypeFormatting"; +constexpr char onlyKey[] = "only"; constexpr char openCloseKey[] = "openClose"; constexpr char optionsKey[] = "options"; constexpr char parametersKey[] = "params"; diff --git a/src/libs/languageserverprotocol/languagefeatures.cpp b/src/libs/languageserverprotocol/languagefeatures.cpp index 154138cabc1..29d60dc7f73 100644 --- a/src/libs/languageserverprotocol/languagefeatures.cpp +++ b/src/libs/languageserverprotocol/languagefeatures.cpp @@ -105,6 +105,16 @@ DocumentSymbolsRequest::DocumentSymbolsRequest(const DocumentSymbolParams ¶m : Request(methodName, params) { } +Utils::optional > CodeActionParams::CodeActionContext::only() const +{ + return optionalArray(onlyKey); +} + +void CodeActionParams::CodeActionContext::setOnly(const QList &only) +{ + insertArray(onlyKey, only); +} + bool CodeActionParams::CodeActionContext::isValid(QStringList *error) const { return checkArray(error, diagnosticsKey); @@ -380,4 +390,32 @@ SignatureHelpRequest::SignatureHelpRequest(const TextDocumentPositionParams &par : Request(methodName, params) { } +CodeActionResult::CodeActionResult(const QJsonValue &val) +{ + using ResultArray = QList>; + if (val.isArray()) { + QJsonArray array = val.toArray(); + ResultArray result; + for (const QJsonValue &val : array) { + Command command(val); + if (command.isValid(nullptr)) + result << command; + else + result << CodeAction(val); + } + emplace(result); + return; + } + emplace(nullptr); +} + +bool CodeAction::isValid(QStringList *error) const +{ + return check(error, titleKey) + && checkOptional(error, codeActionKindKey) + && checkOptionalArray(error, diagnosticsKey) + && checkOptional(error, editKey) + && checkOptional(error, commandKey); +} + } // namespace LanguageServerProtocol diff --git a/src/libs/languageserverprotocol/languagefeatures.h b/src/libs/languageserverprotocol/languagefeatures.h index 9db51863618..0282fc5772b 100644 --- a/src/libs/languageserverprotocol/languagefeatures.h +++ b/src/libs/languageserverprotocol/languagefeatures.h @@ -344,12 +344,37 @@ public: constexpr static const char methodName[] = "textDocument/documentSymbol"; }; +/** + * The kind of a code action. + * + * Kinds are a hierarchical list of identifiers separated by `.`, e.g. `"refactor.extract.function"`. + * + * The set of kinds is open and client needs to announce the kinds it supports to the server during + * initialization. + */ + +using CodeActionKind = QString; + +/** + * A set of predefined code action kinds + */ + +namespace CodeActionKinds { +constexpr char QuickFix[] = "quickfix"; +constexpr char Refactor[] = "refactor"; +constexpr char RefactorExtract[] = "refactor.extract"; +constexpr char RefactorInline[] = "refactor.inline"; +constexpr char RefactorRewrite[] = "refactor.rewrite"; +constexpr char Source[] = "source"; +constexpr char SourceOrganizeImports[] = "source.organizeImports"; +} + class LANGUAGESERVERPROTOCOL_EXPORT CodeActionParams : public JsonObject { public: using JsonObject::JsonObject; - class CodeActionContext : public JsonObject + class LANGUAGESERVERPROTOCOL_EXPORT CodeActionContext : public JsonObject { public: using JsonObject::JsonObject; @@ -358,6 +383,10 @@ public: void setDiagnostics(const QList &diagnostics) { insertArray(diagnosticsKey, diagnostics); } + Utils::optional> only() const; + void setOnly(const QList &only); + void clearOnly() { remove(onlyKey); } + bool isValid(QStringList *error) const override; }; @@ -375,8 +404,45 @@ public: bool isValid(QStringList *error) const override; }; +class LANGUAGESERVERPROTOCOL_EXPORT CodeAction : public JsonObject +{ +public: + using JsonObject::JsonObject; + + QString title() const { return typedValue(titleKey); } + void setTitle(QString title) { insert(titleKey, title); } + + Utils::optional kind() const { return optionalValue(kindKey); } + void setKind(const CodeActionKind &kind) { insert(kindKey, kind); } + void clearKind() { remove(kindKey); } + + Utils::optional> diagnostics() const + { return optionalArray(diagnosticsKey); } + void setDiagnostics(const QList &diagnostics) + { insertArray(diagnosticsKey, diagnostics); } + void clearDiagnostics() { remove(diagnosticsKey); } + + Utils::optional edit() const { return optionalValue(editKey); } + void setEdit(const WorkspaceEdit &edit) { insert(editKey, edit); } + void clearEdit() { remove(editKey); } + + Utils::optional command() const { return optionalValue(commandKey); } + void setCommand(const Command &command) { insert(commandKey, command); } + void clearCommand() { remove(commandKey); } + + bool isValid(QStringList *) const override; +}; + +class LANGUAGESERVERPROTOCOL_EXPORT CodeActionResult + : public Utils::variant>, nullptr_t> +{ +public: + using variant::variant; + explicit CodeActionResult(const QJsonValue &val); +}; + class LANGUAGESERVERPROTOCOL_EXPORT CodeActionRequest : public Request< - LanguageClientArray, std::nullptr_t, CodeActionParams> + CodeActionResult, std::nullptr_t, CodeActionParams> { public: CodeActionRequest(const CodeActionParams ¶ms = CodeActionParams()); diff --git a/src/libs/languageserverprotocol/lsptypes.cpp b/src/libs/languageserverprotocol/lsptypes.cpp index 191c9f652ea..41f5eb07bd6 100644 --- a/src/libs/languageserverprotocol/lsptypes.cpp +++ b/src/libs/languageserverprotocol/lsptypes.cpp @@ -75,22 +75,20 @@ bool Diagnostic::isValid(QStringList *error) const && check(error, messageKey); } -Utils::optional>> WorkspaceEdit::changes() const +Utils::optional WorkspaceEdit::changes() const { - using Changes = Utils::optional>>; - Changes changes; auto it = find(changesKey); - if (it != end()) + if (it == end()) return Utils::nullopt; QTC_ASSERT(it.value().type() == QJsonValue::Object, return Changes()); QJsonObject changesObject(it.value().toObject()); - QMap> changesResult; + Changes changesResult; for (const QString &key : changesObject.keys()) - changesResult[key] = LanguageClientArray(changesObject.value(key)).toList(); + changesResult[DocumentUri::fromProtocol(key)] = LanguageClientArray(changesObject.value(key)).toList(); return Utils::make_optional(changesResult); } -void WorkspaceEdit::setChanges(const QMap > &changes) +void WorkspaceEdit::setChanges(const Changes &changes) { QJsonObject changesObject; const auto end = changes.end(); @@ -98,7 +96,7 @@ void WorkspaceEdit::setChanges(const QMap > &changes) QJsonArray edits; for (const TextEdit &edit : it.value()) edits.append(QJsonValue(edit)); - changesObject.insert(it.key(), edits); + changesObject.insert(it.key().toFileName().toString(), edits); } insert(changesKey, changesObject); } @@ -329,6 +327,13 @@ int Position::toPositionInDocument(QTextDocument *doc) const return block.position() + character(); } +QTextCursor Position::toTextCursor(QTextDocument *doc) const +{ + QTextCursor cursor(doc); + cursor.setPosition(toPositionInDocument(doc)); + return cursor; +} + Range::Range(const Position &start, const Position &end) { setStart(start); diff --git a/src/libs/languageserverprotocol/lsptypes.h b/src/libs/languageserverprotocol/lsptypes.h index 46d830b370b..5175255a13d 100644 --- a/src/libs/languageserverprotocol/lsptypes.h +++ b/src/libs/languageserverprotocol/lsptypes.h @@ -86,6 +86,7 @@ public: { return check(error, lineKey) && check(error, characterKey); } int toPositionInDocument(QTextDocument *doc) const; + QTextCursor toTextCursor(QTextDocument *doc) const; }; static bool operator<=(const Position &first, const Position &second) @@ -187,14 +188,17 @@ public: // Title of the command, like `save`. QString title() const { return typedValue(titleKey); } void setTitle(const QString &title) { insert(titleKey, title); } + void clearTitle() { remove(titleKey); } // The identifier of the actual command handler. QString command() const { return typedValue(commandKey); } void setCommand(const QString &command) { insert(commandKey, command); } + void clearCommand() { remove(commandKey); } // Arguments that the command handler should be invoked with. Utils::optional arguments() const { return typedValue(argumentsKey); } - void setArguments(const QJsonObject &arguments) { insert(argumentsKey, arguments); } + void setArguments(const QJsonArray &arguments) { insert(argumentsKey, arguments); } + void clearArguments() { remove(argumentsKey); } bool isValid(QStringList *error) const override { return check(error, titleKey) @@ -276,8 +280,9 @@ public: using JsonObject::JsonObject; // Holds changes to existing resources. - Utils::optional>> changes() const; - void setChanges(const QMap> &changes); + using Changes = QMap>; + Utils::optional changes() const; + void setChanges(const Changes &changes); /* * An array of `TextDocumentEdit`s to express changes to n different text documents diff --git a/src/libs/languageserverprotocol/lsputils.h b/src/libs/languageserverprotocol/lsputils.h index f4f489583a4..d7ae5525bb0 100644 --- a/src/libs/languageserverprotocol/lsputils.h +++ b/src/libs/languageserverprotocol/lsputils.h @@ -150,4 +150,13 @@ QJsonArray enumArrayToJsonArray(const QList &values) return array; } +template +QList jsonArrayToList(const QJsonArray &array) +{ + QList list; + for (const QJsonValue &val : array) + list << fromJsonValue(val); + return list; +} + } // namespace LanguageClient diff --git a/src/libs/languageserverprotocol/servercapabilities.cpp b/src/libs/languageserverprotocol/servercapabilities.cpp index cd6f75a6d4d..fcd1fadeac9 100644 --- a/src/libs/languageserverprotocol/servercapabilities.cpp +++ b/src/libs/languageserverprotocol/servercapabilities.cpp @@ -98,6 +98,19 @@ void ServerCapabilities::setImplementationProvider( insert(implementationProviderKey, Utils::get(implementationProvider)); } +Utils::optional> ServerCapabilities::codeActionProvider() const +{ + QJsonValue provider = value(codeActionProviderKey); + if (provider.isBool()) + return Utils::make_optional(Utils::variant(provider.toBool())); + if (provider.isObject()) { + CodeActionOptions options(provider); + if (options.isValid(nullptr)) + return Utils::make_optional(Utils::variant(options)); + } + return Utils::nullopt; +} + bool ServerCapabilities::isValid(QStringList *error) const { return checkOptional(error, textDocumentSyncKey) diff --git a/src/libs/languageserverprotocol/servercapabilities.h b/src/libs/languageserverprotocol/servercapabilities.h index d8257682ab7..0e31821b17c 100644 --- a/src/libs/languageserverprotocol/servercapabilities.h +++ b/src/libs/languageserverprotocol/servercapabilities.h @@ -120,6 +120,19 @@ enum class TextDocumentSyncKind Incremental = 2 }; +class LANGUAGESERVERPROTOCOL_EXPORT CodeActionOptions : public JsonObject +{ +public: + using JsonObject::JsonObject; + + QList codeActionKinds() const { return array(codeActionKindsKey); } + void setCodeActionKinds(const QList &codeActionKinds) + { insertArray(codeActionKindsKey, codeActionKinds); } + + bool isValid(QStringList *error) const override + { return checkArray(error, codeActionKindsKey); } +}; + class LANGUAGESERVERPROTOCOL_EXPORT ServerCapabilities : public JsonObject { public: @@ -305,10 +318,11 @@ public: void clearWorkspaceSymbolProvider() { remove(workspaceSymbolProviderKey); } // The server provides code actions. - Utils::optional codeActionProvider() const - { return optionalValue(codeActionProviderKey); } + Utils::optional> codeActionProvider() const; void setCodeActionProvider(bool codeActionProvider) { insert(codeActionProviderKey, codeActionProvider); } + void setCodeActionProvider(CodeActionOptions options) + { insert(codeActionProviderKey, options); } void clearCodeActionProvider() { remove(codeActionProviderKey); } // The server provides code lens. diff --git a/src/libs/languageserverprotocol/workspace.cpp b/src/libs/languageserverprotocol/workspace.cpp index d4b8d3118ec..9f73af2ac0a 100644 --- a/src/libs/languageserverprotocol/workspace.cpp +++ b/src/libs/languageserverprotocol/workspace.cpp @@ -91,4 +91,11 @@ DidChangeWatchedFilesNotification::DidChangeWatchedFilesNotification( : Notification(methodName, params) { } +ExecuteCommandParams::ExecuteCommandParams(const Command &command) +{ + setCommand(command.command()); + if (command.arguments().has_value()) + setArguments(command.arguments().value()); +} + } // namespace LanguageServerProtocol diff --git a/src/libs/languageserverprotocol/workspace.h b/src/libs/languageserverprotocol/workspace.h index 771e6d9d9b9..5f69d2c782c 100644 --- a/src/libs/languageserverprotocol/workspace.h +++ b/src/libs/languageserverprotocol/workspace.h @@ -199,15 +199,16 @@ public: class LANGUAGESERVERPROTOCOL_EXPORT ExecuteCommandParams : public JsonObject { public: - using JsonObject::JsonObject; + explicit ExecuteCommandParams(const Command &command); + explicit ExecuteCommandParams(const QJsonValue &value) : JsonObject(value) {} + ExecuteCommandParams() : JsonObject() {} QString command() const { return typedValue(commandKey); } void setCommand(const QString &command) { insert(commandKey, command); } + void clearCommand() { remove(commandKey); } - Utils::optional> arguments() const - { return optionalArray(argumentsKey); } - void setArguments(const QList &arguments) - { insertArray(argumentsKey, arguments); } + Utils::optional arguments() const { return typedValue(argumentsKey); } + void setArguments(const QJsonArray &arguments) { insert(argumentsKey, arguments); } void clearArguments() { remove(argumentsKey); } bool isValid(QStringList *error) const override diff --git a/src/plugins/languageclient/baseclient.cpp b/src/plugins/languageclient/baseclient.cpp index b4a8eef5259..2852fa1b432 100644 --- a/src/plugins/languageclient/baseclient.cpp +++ b/src/plugins/languageclient/baseclient.cpp @@ -24,7 +24,9 @@ ****************************************************************************/ #include "baseclient.h" + #include "languageclientmanager.h" +#include "languageclient/languageclientutils.h" #include #include @@ -59,6 +61,7 @@ namespace LanguageClient { static Q_LOGGING_CATEGORY(LOGLSPCLIENT, "qtc.languageclient.client", QtWarningMsg); static Q_LOGGING_CATEGORY(LOGLSPCLIENTV, "qtc.languageclient.messages", QtWarningMsg); +static Q_LOGGING_CATEGORY(LOGLSPCLIENTPARSE, "qtc.languageclient.parse", QtWarningMsg); BaseClient::BaseClient() : m_id(Core::Id::fromString(QUuid::createUuid().toString())) @@ -71,11 +74,18 @@ BaseClient::BaseClient() BaseClient::~BaseClient() { + using namespace TextEditor; m_buffer.close(); // FIXME: instead of replacing the completion provider in the text document store the // completion provider as a prioritised list in the text document - for (TextEditor::TextDocument *document : m_resetCompletionProvider) + for (TextDocument *document : m_resetCompletionProvider) document->setCompletionAssistProvider(nullptr); + for (Core::IEditor * editor : Core::DocumentModel::editorsForOpenedDocuments()) { + if (auto textEditor = qobject_cast(editor)) { + TextEditorWidget *widget = textEditor->editorWidget(); + widget->setRefactorMarkers(RefactorMarker::filterOutType(widget->refactorMarkers(), id())); + } + } } void BaseClient::initialize() @@ -91,6 +101,7 @@ void BaseClient::initialize() params.setWorkSpaceFolders(Utils::transform(SessionManager::projects(), [](Project *pro){ return WorkSpaceFolder(pro->projectDirectory().toString(), pro->displayName()); })); + initRequest->setParams(params); } initRequest->setResponseCallback([this](const InitializeRequest::Response &initResponse){ intializeCallback(initResponse); @@ -282,6 +293,7 @@ void BaseClient::documentContentsChanged(Core::IDocument *document) } } auto textDocument = qobject_cast(document); + if (syncKind != TextDocumentSyncKind::None) { const auto uri = DocumentUri::fromFileName(document->filePath()); VersionedTextDocumentIdentifier docId(uri); @@ -289,8 +301,14 @@ void BaseClient::documentContentsChanged(Core::IDocument *document) const DidChangeTextDocumentParams params(docId, QString::fromUtf8(document->contents())); sendContent(DidChangeTextDocumentNotification(params)); } - if (textDocument) + + if (textDocument) { + using namespace TextEditor; + if (BaseTextEditor *editor = BaseTextEditor::textEditorForDocument(textDocument)) + if (TextEditorWidget *widget = editor->editorWidget()) + widget->setRefactorMarkers(RefactorMarker::filterOutType(widget->refactorMarkers(), id())); requestDocumentSymbols(textDocument); + } } void BaseClient::registerCapabilities(const QList ®istrations) @@ -493,6 +511,85 @@ void BaseClient::cursorPositionChanged(TextEditor::TextEditorWidget *widget) sendContent(request); } +void BaseClient::requestCodeActions(const DocumentUri &uri, const QList &diagnostics) +{ + const Utils::FileName fileName = uri.toFileName(); + TextEditor::TextDocument *doc = textDocumentForFileName(fileName); + if (!doc) + return; + + const QString method(CodeActionRequest::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(fileName)) + return; + } else { + Utils::variant provider + = m_serverCapabilities.codeActionProvider().value_or(false); + if (!(Utils::holds_alternative(provider) || Utils::get(provider))) + return; + } + + CodeActionParams codeActionParams; + CodeActionParams::CodeActionContext context; + context.setDiagnostics(diagnostics); + codeActionParams.setContext(context); + codeActionParams.setTextDocument(uri); + Position start(0, 0); + const QTextBlock &lastBlock = doc->document()->lastBlock(); + Position end(lastBlock.blockNumber(), lastBlock.length() - 1); + codeActionParams.setRange(Range(start, end)); + CodeActionRequest request(codeActionParams); + request.setResponseCallback( + [uri, self = QPointer(this)](const CodeActionRequest::Response &response) { + if (self) + self->handleCodeActionResponse(response, uri); + }); + sendContent(request); +} + +void BaseClient::handleCodeActionResponse(const CodeActionRequest::Response &response, + const DocumentUri &uri) +{ + if (const Utils::optional &error = response.error()) + log(*error); + if (const Utils::optional &_result = response.result()) { + const CodeActionResult &result = _result.value(); + if (auto list = Utils::get_if>>(&result)) { + for (const Utils::variant &item : *list) { + if (auto action = Utils::get_if(&item)) + updateCodeActionRefactoringMarker(this, *action, uri); + else if (auto command = Utils::get_if(&item)) + ; // todo + } + } + } +} + +void BaseClient::executeCommand(const Command &command) +{ + using CommandOptions = LanguageServerProtocol::ServerCapabilities::ExecuteCommandOptions; + const QString method(ExecuteCommandRequest::methodName); + if (Utils::optional registered = m_dynamicCapabilities.isRegistered(method)) { + if (!registered.value()) + return; + const CommandOptions option(m_dynamicCapabilities.option(method).toObject()); + if (option.isValid(nullptr) && !option.commands().isEmpty() && !option.commands().contains(command.command())) + return; + } else if (Utils::optional option = m_serverCapabilities.executeCommandProvider()) { + if (option->isValid(nullptr) && !option->commands().isEmpty() && !option->commands().contains(command.command())) + return; + } else { + return; + } + + const ExecuteCommandRequest request((ExecuteCommandParams(command))); + sendContent(request); +} + void BaseClient::projectOpened(ProjectExplorer::Project *project) { if (!sendWorkspceFolderChanges()) @@ -640,7 +737,7 @@ void BaseClient::handleMethod(const QString &method, MessageId id, const IConten auto params = dynamic_cast(content)->params().value_or(PublishDiagnosticsParams()); paramsValid = params.isValid(&error); if (paramsValid) - LanguageClientManager::publishDiagnostics(m_id, params); + LanguageClientManager::publishDiagnostics(m_id, params, this); } else if (method == LogMessageNotification::methodName) { auto params = dynamic_cast(content)->params().value_or(LogMessageParams()); paramsValid = params.isValid(&error); @@ -679,6 +776,11 @@ void BaseClient::handleMethod(const QString &method, MessageId id, const IConten paramsValid = params.isValid(&error); if (paramsValid) m_dynamicCapabilities.unregisterCapability(params.unregistrations()); + } else if (method == ApplyWorkspaceEditRequest::methodName) { + auto params = dynamic_cast(content)->params().value_or(ApplyWorkspaceEditParams()); + paramsValid = params.isValid(&error); + if (paramsValid) + applyWorkspaceEdit(params.edit()); } else if (id.isValid(&error)) { Response response; response.setId(id); @@ -773,6 +875,8 @@ bool BaseClient::sendWorkspceFolderChanges() const void BaseClient::parseData(const QByteArray &data) { const qint64 preWritePosition = m_buffer.pos(); + qCDebug(LOGLSPCLIENTPARSE) << "parse buffer pos: " << preWritePosition; + qCDebug(LOGLSPCLIENTPARSE) << " data: " << data; if (!m_buffer.atEnd()) m_buffer.seek(preWritePosition + m_buffer.bytesAvailable()); m_buffer.write(data); @@ -780,6 +884,9 @@ void BaseClient::parseData(const QByteArray &data) while (!m_buffer.atEnd()) { QString parseError; BaseMessage::parse(&m_buffer, parseError, m_currentMessage); + qCDebug(LOGLSPCLIENTPARSE) << " complete: " << m_currentMessage.isComplete(); + qCDebug(LOGLSPCLIENTPARSE) << " length: " << m_currentMessage.contentLength; + qCDebug(LOGLSPCLIENTPARSE) << " content: " << m_currentMessage.content; if (!parseError.isEmpty()) log(parseError); if (!m_currentMessage.isComplete()) diff --git a/src/plugins/languageclient/baseclient.h b/src/plugins/languageclient/baseclient.h index db904193b9d..318d9ce67f9 100644 --- a/src/plugins/languageclient/baseclient.h +++ b/src/plugins/languageclient/baseclient.h @@ -97,6 +97,12 @@ public: void requestDocumentSymbols(TextEditor::TextDocument *document); void cursorPositionChanged(TextEditor::TextEditorWidget *widget); + void requestCodeActions(const LanguageServerProtocol::DocumentUri &uri, + const QList &diagnostics); + void handleCodeActionResponse(const LanguageServerProtocol::CodeActionRequest::Response &response, + const LanguageServerProtocol::DocumentUri &uri); + void executeCommand(const LanguageServerProtocol::Command &command); + // workspace control void projectOpened(ProjectExplorer::Project *project); void projectClosed(ProjectExplorer::Project *project); diff --git a/src/plugins/languageclient/languageclientcodeassist.cpp b/src/plugins/languageclient/languageclientcodeassist.cpp index c8c1ed7a46c..42a4660f6e9 100644 --- a/src/plugins/languageclient/languageclientcodeassist.cpp +++ b/src/plugins/languageclient/languageclientcodeassist.cpp @@ -24,7 +24,9 @@ ****************************************************************************/ #include "languageclientcodeassist.h" + #include "baseclient.h" +#include "languageclientutils.h" #include #include @@ -89,17 +91,6 @@ bool LanguageClientCompletionItem::implicitlyApplies() const bool LanguageClientCompletionItem::prematurelyApplies(const QChar &/*typedCharacter*/) const { return false; } -static void applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator, - const TextEdit &edit) -{ - using namespace Utils::Text; - const Range range = edit.range(); - const QTextDocument *doc = manipulator.textCursorAt(manipulator.currentPosition()).document(); - const int start = positionInText(doc, range.start().line() + 1, range.start().character() + 1); - const int end = positionInText(doc, range.end().line() + 1, range.end().character() + 1); - manipulator.replace(start, end - start, edit.newText()); -} - void LanguageClientCompletionItem::apply(TextEditor::TextDocumentManipulatorInterface &manipulator, int /*basePosition*/) const { diff --git a/src/plugins/languageclient/languageclientmanager.cpp b/src/plugins/languageclient/languageclientmanager.cpp index b3fe7b13c15..b4746b45ef4 100644 --- a/src/plugins/languageclient/languageclientmanager.cpp +++ b/src/plugins/languageclient/languageclientmanager.cpp @@ -40,6 +40,7 @@ #include #include +#include #include using namespace LanguageServerProtocol; @@ -75,6 +76,7 @@ public: LanguageClientManager::LanguageClientManager() { JsonRpcMessageHandler::registerMessageProvider(); + JsonRpcMessageHandler::registerMessageProvider(); JsonRpcMessageHandler::registerMessageProvider(); JsonRpcMessageHandler::registerMessageProvider(); JsonRpcMessageHandler::registerMessageProvider(); @@ -106,7 +108,8 @@ void LanguageClientManager::init() } void LanguageClientManager::publishDiagnostics(const Core::Id &id, - const PublishDiagnosticsParams ¶ms) + const PublishDiagnosticsParams ¶ms, + BaseClient *publishingClient) { const Utils::FileName fileName = params.uri().toFileName(); TextEditor::TextDocument *doc = textDocumentForFileName(fileName); @@ -115,11 +118,14 @@ void LanguageClientManager::publishDiagnostics(const Core::Id &id, removeMarks(fileName, id); managerInstance->m_marks[fileName][id].reserve(params.diagnostics().size()); - for (const Diagnostic& diagnostic : params.diagnostics()) { + QList diagnostics = params.diagnostics(); + for (const Diagnostic& diagnostic : diagnostics) { auto mark = new LanguageClientMark(fileName, diagnostic); managerInstance->m_marks[fileName][id].append(mark); doc->addMark(mark); } + + publishingClient->requestCodeActions(params.uri(), diagnostics); } void LanguageClientManager::removeMark(LanguageClientMark *mark) diff --git a/src/plugins/languageclient/languageclientmanager.h b/src/plugins/languageclient/languageclientmanager.h index 2953e701cda..54f267d86d1 100644 --- a/src/plugins/languageclient/languageclientmanager.h +++ b/src/plugins/languageclient/languageclientmanager.h @@ -56,7 +56,7 @@ public: static void init(); static void publishDiagnostics(const Core::Id &id, - const LanguageServerProtocol::PublishDiagnosticsParams ¶ms); + const LanguageServerProtocol::PublishDiagnosticsParams ¶ms, BaseClient *publishingClient); static void removeMark(LanguageClientMark *mark); static void removeMarks(const Utils::FileName &fileName); diff --git a/src/plugins/languageclient/languageclientutils.cpp b/src/plugins/languageclient/languageclientutils.cpp index cbfb91ccc9f..3e0c884c178 100644 --- a/src/plugins/languageclient/languageclientutils.cpp +++ b/src/plugins/languageclient/languageclientutils.cpp @@ -25,14 +25,169 @@ #include "languageclientutils.h" -#include +#include "baseclient.h" + #include -using namespace LanguageClient; -using namespace LanguageServerProtocol; +#include +#include +#include +#include +#include -TextEditor::TextDocument *LanguageClient::textDocumentForFileName(const Utils::FileName &fileName) +#include +#include + +using namespace LanguageServerProtocol; +using namespace Utils; + +namespace LanguageClient { + +QTextCursor rangeToTextCursor(const Range &range, QTextDocument *doc) +{ + QTextCursor cursor(doc); + cursor.setPosition(range.end().toPositionInDocument(doc)); + cursor.setPosition(range.start().toPositionInDocument(doc), QTextCursor::KeepAnchor); + return cursor; +} + +ChangeSet::Range convertRange(const QTextDocument *doc, const Range &range) +{ + return ChangeSet::Range( + Text::positionInText(doc, range.start().line() + 1, range.start().character() + 1), + Text::positionInText(doc, range.end().line() + 1, range.end().character()) + 1); +} + +ChangeSet editsToChangeSet(const QList &edits, const QTextDocument *doc) +{ + ChangeSet changeSet; + for (const TextEdit &edit : edits) + changeSet.replace(convertRange(doc, edit.range()), edit.newText()); + return changeSet; +} + +bool applyTextDocumentEdit(const TextDocumentEdit &edit) +{ + const QList &edits = edit.edits(); + if (edits.isEmpty()) + return true; + const DocumentUri &uri = edit.id().uri(); + if (TextEditor::TextDocument* doc = textDocumentForFileName(uri.toFileName())) { + LanguageClientValue version = edit.id().version(); + if (!version.isNull() && version.value(0) < doc->document()->revision()) + return false; + } + return applyTextEdits(uri, edits); +} + +bool applyTextEdits(const DocumentUri &uri, const QList &edits) +{ + if (edits.isEmpty()) + return true; + TextEditor::RefactoringChanges changes; + TextEditor::RefactoringFilePtr file; + file = changes.file(uri.toFileName().toString()); + file->setChangeSet(editsToChangeSet(edits, file->document())); + return file->apply(); +} + +void applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator, const TextEdit &edit) +{ + using namespace Utils::Text; + const Range range = edit.range(); + const QTextDocument *doc = manipulator.textCursorAt(manipulator.currentPosition()).document(); + const int start = positionInText(doc, range.start().line() + 1, range.start().character() + 1); + const int end = positionInText(doc, range.end().line() + 1, range.end().character() + 1); + manipulator.replace(start, end - start, edit.newText()); +} + +bool applyWorkspaceEdit(const WorkspaceEdit &edit) +{ + bool result = true; + const QList &documentChanges + = edit.documentChanges().value_or(QList()); + if (!documentChanges.isEmpty()) { + for (const TextDocumentEdit &documentChange : documentChanges) + result |= applyTextDocumentEdit(documentChange); + } else { + const WorkspaceEdit::Changes &changes = edit.changes().value_or(WorkspaceEdit::Changes()); + for (const DocumentUri &file : changes.keys()) + result |= applyTextEdits(file, changes.value(file)); + return result; + } + return result; +} + +TextEditor::TextDocument *textDocumentForFileName(const FileName &fileName) { return qobject_cast( - Core::DocumentModel::documentForFilePath(fileName.toString())); + Core::DocumentModel::documentForFilePath(fileName.toString())); } + +QTextCursor endOfLineCursor(const QTextCursor &cursor) +{ + QTextCursor ret = cursor; + ret.movePosition(QTextCursor::EndOfLine); + return ret; +} + +void updateCodeActionRefactoringMarker(BaseClient *client, + const CodeAction &action, + const DocumentUri &uri) +{ + using namespace TextEditor; + TextDocument* doc = textDocumentForFileName(uri.toFileName()); + if (!doc) + return; + BaseTextEditor *editor = BaseTextEditor::textEditorForDocument(doc); + if (!editor) + return; + + TextEditorWidget *editorWidget = editor->editorWidget(); + + const QList &diagnostics = action.diagnostics().value_or(QList()); + + RefactorMarkers markers; + RefactorMarker marker; + marker.type = client->id(); + if (action.isValid(nullptr)) + marker.tooltip = action.title(); + if (action.edit().has_value()) { + WorkspaceEdit edit = action.edit().value(); + marker.callback = [edit](const TextEditorWidget *) { + applyWorkspaceEdit(edit); + }; + if (diagnostics.isEmpty()) { + QList edits; + if (optional> documentChanges = edit.documentChanges()) { + QList changesForUri = Utils::filtered( + documentChanges.value(), [uri](const TextDocumentEdit &edit) { + return edit.id().uri() == uri; + }); + for (const TextDocumentEdit &edit : changesForUri) + edits << edit.edits(); + } else if (optional localChanges = edit.changes()) { + edits = localChanges.value()[uri]; + } + for (const TextEdit &edit : edits) { + marker.cursor = endOfLineCursor(edit.range().start().toTextCursor(doc->document())); + markers << marker; + } + } + } else if (action.command().has_value()) { + const Command command = action.command().value(); + marker.callback = [command, client = QPointer(client)](const TextEditorWidget *) { + if (client) + client->executeCommand(command); + }; + } else { + return; + } + for (const Diagnostic &diagnostic : diagnostics) { + marker.cursor = endOfLineCursor(diagnostic.range().start().toTextCursor(doc->document())); + markers << marker; + } + editorWidget->setRefactorMarkers(markers + editorWidget->refactorMarkers()); +} + +} // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientutils.h b/src/plugins/languageclient/languageclientutils.h index 2423263954d..bf7ac602e43 100644 --- a/src/plugins/languageclient/languageclientutils.h +++ b/src/plugins/languageclient/languageclientutils.h @@ -26,11 +26,28 @@ #pragma once #include +#include -namespace TextEditor { class TextDocument; } +#include + +namespace TextEditor { +class TextDocument; +class TextDocumentManipulatorInterface; +} // namespace TextEditor namespace LanguageClient { +class BaseClient; + +bool applyWorkspaceEdit(const LanguageServerProtocol::WorkspaceEdit &edit); +bool applyTextDocumentEdit(const LanguageServerProtocol::TextDocumentEdit &edit); +bool applyTextEdits(const LanguageServerProtocol::DocumentUri &uri, + const QList &edits); +void applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator, + const LanguageServerProtocol::TextEdit &edit); TextEditor::TextDocument *textDocumentForFileName(const Utils::FileName &fileName); +void updateCodeActionRefactoringMarker(BaseClient *client, + const LanguageServerProtocol::CodeAction &action, + const LanguageServerProtocol::DocumentUri &uri); } // namespace LanguageClient