From ac5ab71db83017a186ca9d06a21c8f7cb3e4ec41 Mon Sep 17 00:00:00 2001 From: David Schulz Date: Mon, 28 Aug 2023 15:00:08 +0200 Subject: [PATCH] LSP: add resource operations to the protocol implementation This allows the language server to request file creation, renaming, or deletion. Fixes: QTCREATORBUG-29542 Change-Id: I31ab3c0b36f87d3b797b54ff4261cab85a322e2c Reviewed-by: Christian Stenger --- .../clientcapabilities.cpp | 42 ++++++ .../clientcapabilities.h | 8 ++ src/libs/languageserverprotocol/jsonkeys.h | 9 +- src/libs/languageserverprotocol/lsptypes.cpp | 74 +++++++++++ src/libs/languageserverprotocol/lsptypes.h | 124 ++++++++++++++++-- src/plugins/languageclient/client.cpp | 8 ++ .../languageclientsymbolsupport.cpp | 50 ++++++- .../languageclient/languageclientutils.cpp | 75 +++++++++-- .../languageclient/languageclientutils.h | 2 + 9 files changed, 364 insertions(+), 28 deletions(-) diff --git a/src/libs/languageserverprotocol/clientcapabilities.cpp b/src/libs/languageserverprotocol/clientcapabilities.cpp index 321b46ecd2e..acae7ad554a 100644 --- a/src/libs/languageserverprotocol/clientcapabilities.cpp +++ b/src/libs/languageserverprotocol/clientcapabilities.cpp @@ -77,4 +77,46 @@ bool SemanticTokensClientCapabilities::isValid() const && contains(formatsKey); } +const char resourceOperationCreate[] = "create"; +const char resourceOperationRename[] = "rename"; +const char resourceOperationDelete[] = "delete"; + +std::optional> +WorkspaceClientCapabilities::WorkspaceEditCapabilities::resourceOperations() const +{ + if (!contains(resourceOperationsKey)) + return std::nullopt; + QList result; + for (const QJsonValue &value : this->value(resourceOperationsKey).toArray()) { + const QString str = value.toString(); + if (str == resourceOperationCreate) + result << ResourceOperationKind::Create; + else if (str == resourceOperationRename) + result << ResourceOperationKind::Rename; + else if (str == resourceOperationDelete) + result << ResourceOperationKind::Delete; + } + return result; +} + +void WorkspaceClientCapabilities::WorkspaceEditCapabilities::setResourceOperations( + const QList &resourceOperations) +{ + QJsonArray array; + for (const auto &kind : resourceOperations) { + switch (kind) { + case ResourceOperationKind::Create: + array << resourceOperationCreate; + break; + case ResourceOperationKind::Rename: + array << resourceOperationRename; + break; + case ResourceOperationKind::Delete: + array << resourceOperationDelete; + break; + } + } + insert(resourceOperationsKey, array); +} + } // namespace LanguageServerProtocol diff --git a/src/libs/languageserverprotocol/clientcapabilities.h b/src/libs/languageserverprotocol/clientcapabilities.h index 7b18c24a762..584c4b8d539 100644 --- a/src/libs/languageserverprotocol/clientcapabilities.h +++ b/src/libs/languageserverprotocol/clientcapabilities.h @@ -555,6 +555,14 @@ public: void setDocumentChanges(bool documentChanges) { insert(documentChangesKey, documentChanges); } void clearDocumentChanges() { remove(documentChangesKey); } + + enum class ResourceOperationKind { Create, Rename, Delete }; + + // The resource operations the client supports. Clients should at least support 'create', + // 'rename' and 'delete' files and folders. + std::optional> resourceOperations() const; + void setResourceOperations(const QList &resourceOperations); + void clearResourceOperations() { remove(resourceOperationsKey); } }; // Capabilities specific to `WorkspaceEdit`s diff --git a/src/libs/languageserverprotocol/jsonkeys.h b/src/libs/languageserverprotocol/jsonkeys.h index e7190afce2e..250b5ca87c1 100644 --- a/src/libs/languageserverprotocol/jsonkeys.h +++ b/src/libs/languageserverprotocol/jsonkeys.h @@ -98,6 +98,8 @@ constexpr char hierarchicalDocumentSymbolSupportKey[] = "hierarchicalDocumentSym constexpr char hoverKey[] = "hover"; constexpr char hoverProviderKey[] = "hoverProvider"; constexpr char idKey[] = "id"; +constexpr char ignoreIfExistsKey[] = "ignoreIfExists"; +constexpr char ignoreIfNotExistsKey[] = "ignoreIfNotExists"; constexpr char implementationKey[] = "implementation"; constexpr char implementationProviderKey[] = "implementationProvider"; constexpr char includeDeclarationKey[] = "includeDeclaration"; @@ -127,11 +129,14 @@ constexpr char multiLineTokenSupportKey[] = "multiLineTokenSupport"; constexpr char nameKey[] = "name"; constexpr char newNameKey[] = "newName"; constexpr char newTextKey[] = "newText"; +constexpr char newUriKey[] = "newUri"; +constexpr char oldUriKey[] = "oldUri"; constexpr char onTypeFormattingKey[] = "onTypeFormatting"; constexpr char onlyKey[] = "only"; constexpr char openCloseKey[] = "openClose"; constexpr char optionsKey[] = "options"; constexpr char overlappingTokenSupportKey[] = "overlappingTokenSupport"; +constexpr char overwriteKey[] = "overwrite"; constexpr char parametersKey[] = "parameters"; constexpr char paramsKey[] = "params"; constexpr char patternKey[] = "pattern"; @@ -147,6 +152,7 @@ constexpr char rangeFormattingKey[] = "rangeFormatting"; constexpr char rangeKey[] = "range"; constexpr char rangeLengthKey[] = "rangeLength"; constexpr char reasonKey[] = "reason"; +constexpr char recursiveKey[] = "recursive"; constexpr char redKey[] = "red"; constexpr char referencesKey[] = "references"; constexpr char referencesProviderKey[] = "referencesProvider"; @@ -158,6 +164,7 @@ constexpr char renameKey[] = "rename"; constexpr char renameProviderKey[] = "renameProvider"; constexpr char requestsKey[] = "requests"; constexpr char resolveProviderKey[] = "resolveProvider"; +constexpr char resourceOperationsKey[] = "resourceOperations"; constexpr char resultIdKey[] = "resultId"; constexpr char resultKey[] = "result"; constexpr char retryKey[] = "retry"; @@ -194,8 +201,8 @@ constexpr char textDocumentSyncKey[] = "textDocumentSync"; constexpr char textEditKey[] = "textEdit"; constexpr char textKey[] = "text"; constexpr char titleKey[] = "title"; -constexpr char tokenKey[] = "token"; constexpr char toKey[] = "to"; +constexpr char tokenKey[] = "token"; constexpr char tokenModifiersKey[] = "tokenModifiers"; constexpr char tokenTypesKey[] = "tokenTypes"; constexpr char traceKey[] = "trace"; diff --git a/src/libs/languageserverprotocol/lsptypes.cpp b/src/libs/languageserverprotocol/lsptypes.cpp index d8f02e256fc..6b2975fbe51 100644 --- a/src/libs/languageserverprotocol/lsptypes.cpp +++ b/src/libs/languageserverprotocol/lsptypes.cpp @@ -2,6 +2,8 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "lsptypes.h" + +#include "languageserverprotocoltr.h" #include "lsputils.h" #include @@ -413,4 +415,76 @@ LanguageServerProtocol::MarkupKind::operator QJsonValue() const return {}; } +DocumentChange::DocumentChange(const QJsonValue &value) +{ + const QString kind = value["kind"].toString(); + if (kind == "create") + emplace(value); + else if (kind == "rename") + emplace(value); + else if (kind == "delete") + emplace(value); + else + emplace(value); +} + +using DocumentChangeBase = std::variant; + +bool DocumentChange::isValid() const +{ + return std::visit([](const auto &v) { return v.isValid(); }, DocumentChangeBase(*this)); +} + +DocumentChange::operator const QJsonValue () const +{ + return std::visit([](const auto &v) { return QJsonValue(v); }, DocumentChangeBase(*this)); +} + +CreateFile::CreateFile() +{ + insert(kindKey, "create"); +} + +QString CreateFile::message(const DocumentUri::PathMapper &mapToHostPath) const +{ + return Tr::tr("Create %1").arg(uri().toFilePath(mapToHostPath).toUserOutput()); +} + +bool LanguageServerProtocol::CreateFile::isValid() const +{ + return contains(uriKey) && value(kindKey) == "create"; +} + +RenameFile::RenameFile() +{ + insert(kindKey, "rename"); +} + +QString RenameFile::message(const DocumentUri::PathMapper &mapToHostPath) const +{ + return Tr::tr("Rename %1 to %2") + .arg(oldUri().toFilePath(mapToHostPath).toUserOutput(), + newUri().toFilePath(mapToHostPath).toUserOutput()); +} + +bool RenameFile::isValid() const +{ + return contains(oldUriKey) && contains(newUriKey) && value(kindKey) == "rename"; +} + +DeleteFile::DeleteFile() +{ + insert(kindKey, "delete"); +} + +QString DeleteFile::message(const DocumentUri::PathMapper &mapToHostPath) const +{ + return Tr::tr("Delete %1").arg(uri().toFilePath(mapToHostPath).toUserOutput()); +} + +bool DeleteFile::isValid() const +{ + return contains(uriKey) && value(kindKey) == "delete"; +} + } // namespace LanguageServerProtocol diff --git a/src/libs/languageserverprotocol/lsptypes.h b/src/libs/languageserverprotocol/lsptypes.h index c4a4e7a0bc8..135b4c124a2 100644 --- a/src/libs/languageserverprotocol/lsptypes.h +++ b/src/libs/languageserverprotocol/lsptypes.h @@ -285,6 +285,106 @@ public: bool isValid() const override { return contains(textDocumentKey) && contains(editsKey); } }; +class CreateFileOptions : public JsonObject +{ +public: + using JsonObject::JsonObject; + + std::optional overwrite() const { return optionalValue(overwriteKey); } + void setOverwrite(bool overwrite) { insert(overwriteKey, overwrite); } + void clearOverwrite() { remove(overwriteKey); } + + std::optional ignoreIfExists() const { return optionalValue(ignoreIfExistsKey); } + void setIgnoreIfExists(bool ignoreIfExists) { insert(ignoreIfExistsKey, ignoreIfExists); } + void clearIgnoreIfExists() { remove(ignoreIfExistsKey); } +}; + +class LANGUAGESERVERPROTOCOL_EXPORT CreateFile : public JsonObject +{ +public: + using JsonObject::JsonObject; + CreateFile(); + + DocumentUri uri() const { return DocumentUri::fromProtocol(typedValue(uriKey)); } + void setUri(const DocumentUri &uri) { insert(uriKey, uri); } + + std::optional options() const + { return optionalValue(optionsKey); } + void setOptions(const CreateFileOptions &options) { insert(optionsKey, options); } + void clearOptions() { remove(optionsKey); } + + QString message(const DocumentUri::PathMapper &mapToHostPath) const; + + bool isValid() const override; +}; + +class LANGUAGESERVERPROTOCOL_EXPORT RenameFile : public JsonObject +{ +public: + using JsonObject::JsonObject; + RenameFile(); + + DocumentUri oldUri() const { return DocumentUri::fromProtocol(typedValue(oldUriKey)); } + void setOldUri(const DocumentUri &oldUri) { insert(oldUriKey, oldUri); } + + DocumentUri newUri() const { return DocumentUri::fromProtocol(typedValue(newUriKey)); } + void setNewUri(const DocumentUri &newUri) { insert(newUriKey, newUri); } + + std::optional options() const + { return optionalValue(optionsKey); } + void setOptions(const CreateFileOptions &options) { insert(optionsKey, options); } + void clearOptions() { remove(optionsKey); } + + QString message(const DocumentUri::PathMapper &mapToHostPath) const; + + bool isValid() const override; +}; + +class DeleteFileOptions : public JsonObject +{ +public: + using JsonObject::JsonObject; + + std::optional recursive() const { return optionalValue(recursiveKey); } + void setRecursive(bool recursive) { insert(recursiveKey, recursive); } + void clearRecursive() { remove(recursiveKey); } + + std::optional ignoreIfNotExists() const { return optionalValue(ignoreIfNotExistsKey); } + void setIgnoreIfNotExists(bool ignoreIfNotExists) { insert(ignoreIfNotExistsKey, ignoreIfNotExists); } + void clearIgnoreIfNotExists() { remove(ignoreIfNotExistsKey); } +}; + +class LANGUAGESERVERPROTOCOL_EXPORT DeleteFile : public JsonObject +{ +public: + using JsonObject::JsonObject; + DeleteFile(); + + DocumentUri uri() const { return DocumentUri::fromProtocol(typedValue(uriKey)); } + void setUri(const DocumentUri &uri) { insert(uriKey, uri); } + + std::optional options() const + { return optionalValue(optionsKey); } + void setOptions(const DeleteFileOptions &options) { insert(optionsKey, options); } + void clearOptions() { remove(optionsKey); } + + QString message(const DocumentUri::PathMapper &mapToHostPath) const; + + bool isValid() const override; +}; + +class LANGUAGESERVERPROTOCOL_EXPORT DocumentChange + : public std::variant +{ +public: + using variant::variant; + DocumentChange(const QJsonValue &value); + + bool isValid() const; + + operator const QJsonValue() const; +}; + class LANGUAGESERVERPROTOCOL_EXPORT WorkspaceEdit : public JsonObject { public: @@ -296,17 +396,23 @@ public: void setChanges(const Changes &changes); /* - * An array of `TextDocumentEdit`s to express changes to n different text documents - * where each text document edit addresses a specific version of a text document. - * Whether a client supports versioned document edits is expressed via - * `WorkspaceClientCapabilities.workspaceEdit.documentChanges`. + * Depending on the client capability + * `workspace.workspaceEdit.resourceOperations` document changes are either + * an array of `TextDocumentEdit`s to express changes to n different text + * documents where each text document edit addresses a specific version of + * a text document. Or it can contain above `TextDocumentEdit`s mixed with + * create, rename and delete file / folder operations. * - * Note: If the client can handle versioned document edits and if documentChanges are present, - * the latter are preferred over changes. + * Whether a client supports versioned document edits is expressed via + * `workspace.workspaceEdit.documentChanges` client capability. + * + * If a client neither supports `documentChanges` nor + * `workspace.workspaceEdit.resourceOperations` then only plain `TextEdit`s + * using the `changes` property are supported. */ - std::optional> documentChanges() const - { return optionalArray(documentChangesKey); } - void setDocumentChanges(const QList &changes) + std::optional> documentChanges() const + { return optionalArray(documentChangesKey); } + void setDocumentChanges(const QList &changes) { insertArray(documentChangesKey, changes); } }; diff --git a/src/plugins/languageclient/client.cpp b/src/plugins/languageclient/client.cpp index c0922538c3f..1d93f869540 100644 --- a/src/plugins/languageclient/client.cpp +++ b/src/plugins/languageclient/client.cpp @@ -375,6 +375,14 @@ static ClientCapabilities generateClientCapabilities() { ClientCapabilities capabilities; WorkspaceClientCapabilities workspaceCapabilities; + WorkspaceClientCapabilities::WorkspaceEditCapabilities workspaceEditCapabilities; + workspaceEditCapabilities.setDocumentChanges(true); + using ResourceOperationKind + = WorkspaceClientCapabilities::WorkspaceEditCapabilities::ResourceOperationKind; + workspaceEditCapabilities.setResourceOperations({ResourceOperationKind::Create, + ResourceOperationKind::Rename, + ResourceOperationKind::Delete}); + workspaceCapabilities.setWorkspaceEdit(workspaceEditCapabilities); workspaceCapabilities.setWorkspaceFolders(true); workspaceCapabilities.setApplyEdit(true); DynamicRegistrationCapabilities allowDynamicRegistration; diff --git a/src/plugins/languageclient/languageclientsymbolsupport.cpp b/src/plugins/languageclient/languageclientsymbolsupport.cpp index 268bd391e81..8ba9507a640 100644 --- a/src/plugins/languageclient/languageclientsymbolsupport.cpp +++ b/src/plugins/languageclient/languageclientsymbolsupport.cpp @@ -539,24 +539,50 @@ Utils::SearchResultItems generateReplaceItems(const WorkspaceEdit &edits, bool limitToProjects, const DocumentUri::PathMapper &pathMapper) { + Utils::SearchResultItems items; auto convertEdits = [](const QList &edits) { return Utils::transform(edits, [](const TextEdit &edit) { return ItemData{SymbolSupport::convertRange(edit.range()), QVariant(edit)}; }); }; QMap> rangesInDocument; - auto documentChanges = edits.documentChanges().value_or(QList()); + auto documentChanges = edits.documentChanges().value_or(QList()); if (!documentChanges.isEmpty()) { - for (const TextDocumentEdit &documentChange : std::as_const(documentChanges)) { - rangesInDocument[documentChange.textDocument().uri().toFilePath(pathMapper)] - = convertEdits(documentChange.edits()); + for (const DocumentChange &documentChange : std::as_const(documentChanges)) { + if (std::holds_alternative(documentChange)) { + const TextDocumentEdit edit = std::get(documentChange); + rangesInDocument[edit.textDocument().uri().toFilePath(pathMapper)] = convertEdits( + edit.edits()); + } else { + Utils::SearchResultItem item; + + if (std::holds_alternative(documentChange)) { + auto op = std::get(documentChange); + item.setLineText(op.message(pathMapper)); + item.setFilePath(op.uri().toFilePath(pathMapper)); + item.setUserData(QVariant(op)); + } else if (std::holds_alternative(documentChange)) { + auto op = std::get(documentChange); + item.setLineText(op.message(pathMapper)); + item.setFilePath(op.oldUri().toFilePath(pathMapper)); + item.setUserData(QVariant(op)); + } else if (std::holds_alternative(documentChange)) { + auto op = std::get(documentChange); + item.setLineText(op.message(pathMapper)); + item.setFilePath(op.uri().toFilePath(pathMapper)); + item.setUserData(QVariant(op)); + } + + items << item; + } } } else { auto changes = edits.changes().value_or(WorkspaceEdit::Changes()); for (auto it = changes.begin(), end = changes.end(); it != end; ++it) rangesInDocument[it.key().toFilePath(pathMapper)] = convertEdits(it.value()); } - return generateSearchResultItems(rangesInDocument, search, limitToProjects); + items += generateSearchResultItems(rangesInDocument, search, limitToProjects); + return items; } Core::SearchResult *SymbolSupport::createSearch(const TextDocumentPositionParams &positionParams, @@ -659,15 +685,25 @@ void SymbolSupport::applyRename(const Utils::SearchResultItems &checkedItems, { QSet affectedNonOpenFilePaths; QMap> editsForDocuments; + QList changes; for (const Utils::SearchResultItem &item : checkedItems) { const auto filePath = Utils::FilePath::fromUserInput(item.path().value(0)); if (!m_client->documentForFilePath(filePath)) affectedNonOpenFilePaths << filePath; - TextEdit edit(item.userData().toJsonObject()); - if (edit.isValid()) + const QJsonObject jsonObject = item.userData().toJsonObject(); + if (const TextEdit edit(jsonObject); edit.isValid()) editsForDocuments[filePath] << edit; + else if (const LanguageServerProtocol::CreateFile createFile(jsonObject); createFile.isValid()) + changes << createFile; + else if (const RenameFile renameFile(jsonObject); renameFile.isValid()) + changes << renameFile; + else if (const LanguageServerProtocol::DeleteFile deleteFile(jsonObject); deleteFile.isValid()) + changes << deleteFile; } + for (const DocumentChange &change : changes) + applyDocumentChange(m_client, change); + for (auto it = editsForDocuments.begin(), end = editsForDocuments.end(); it != end; ++it) applyTextEdits(m_client, it.key(), it.value()); diff --git a/src/plugins/languageclient/languageclientutils.cpp b/src/plugins/languageclient/languageclientutils.cpp index 19cd740506b..2df9331d2bf 100644 --- a/src/plugins/languageclient/languageclientutils.cpp +++ b/src/plugins/languageclient/languageclientutils.cpp @@ -120,11 +120,10 @@ void applyTextEdit(TextDocumentManipulatorInterface &manipulator, bool applyWorkspaceEdit(const Client *client, const WorkspaceEdit &edit) { bool result = true; - const QList &documentChanges - = edit.documentChanges().value_or(QList()); + const auto documentChanges = edit.documentChanges().value_or(QList()); if (!documentChanges.isEmpty()) { - for (const TextDocumentEdit &documentChange : documentChanges) - result |= applyTextDocumentEdit(client, documentChange); + for (const DocumentChange &documentChange : documentChanges) + result |= applyDocumentChange(client, documentChange); } else { const WorkspaceEdit::Changes &changes = edit.changes().value_or(WorkspaceEdit::Changes()); for (auto it = changes.cbegin(); it != changes.cend(); ++it) @@ -188,13 +187,13 @@ void updateCodeActionRefactoringMarker(Client *client, if (std::optional edit = action.edit()) { if (diagnostics.isEmpty()) { QList edits; - if (std::optional> documentChanges = edit->documentChanges()) { - QList changesForUri = Utils::filtered( - *documentChanges, [uri](const TextDocumentEdit &edit) { - return edit.textDocument().uri() == uri; - }); - for (const TextDocumentEdit &edit : changesForUri) - edits << edit.edits(); + if (std::optional> documentChanges = edit->documentChanges()) { + for (const DocumentChange &change : *documentChanges) { + if (auto edit = std::get_if(&change)) { + if (edit->textDocument().uri() == uri) + edits << edit->edits(); + } + } } else if (std::optional localChanges = edit->changes()) { edits = (*localChanges)[uri]; } @@ -345,4 +344,58 @@ const QIcon symbolIcon(int type) return icons[kind]; } +bool applyDocumentChange(const Client *client, const DocumentChange &change) +{ + if (!client) + return false; + + if (std::holds_alternative(change)) { + return applyTextDocumentEdit(client, std::get(change)); + } else if (std::holds_alternative(change)) { + const auto createOperation = std::get(change); + const FilePath filePath = createOperation.uri().toFilePath(client->hostPathMapper()); + if (filePath.exists()) { + if (const std::optional options = createOperation.options()) { + if (options->overwrite().value_or(false)) { + if (!filePath.removeFile()) + return false; + } else if (options->ignoreIfExists().value_or(false)) { + return true; + } + } + } + return filePath.ensureExistingFile(); + } else if (std::holds_alternative(change)) { + const RenameFile renameOperation = std::get(change); + const FilePath oldPath = renameOperation.oldUri().toFilePath(client->hostPathMapper()); + if (!oldPath.exists()) + return false; + const FilePath newPath = renameOperation.newUri().toFilePath(client->hostPathMapper()); + if (oldPath == newPath) + return true; + if (newPath.exists()) { + if (const std::optional options = renameOperation.options()) { + if (options->overwrite().value_or(false)) { + if (!newPath.removeFile()) + return false; + } else if (options->ignoreIfExists().value_or(false)) { + return true; + } + } + } + return oldPath.renameFile(newPath); + } else if (std::holds_alternative(change)) { + const auto deleteOperation = std::get(change); + const FilePath filePath = deleteOperation.uri().toFilePath(client->hostPathMapper()); + if (const std::optional options = deleteOperation.options()) { + if (!filePath.exists()) + return options->ignoreIfNotExists().value_or(false); + if (filePath.isDir() && options->recursive().value_or(false)) + return filePath.removeRecursively(); + } + return filePath.removeFile(); + } + return false; +} + } // namespace LanguageClient diff --git a/src/plugins/languageclient/languageclientutils.h b/src/plugins/languageclient/languageclientutils.h index f24b8e45e74..13795f52d85 100644 --- a/src/plugins/languageclient/languageclientutils.h +++ b/src/plugins/languageclient/languageclientutils.h @@ -35,6 +35,8 @@ bool LANGUAGECLIENT_EXPORT applyTextEdits(const Client *client, bool LANGUAGECLIENT_EXPORT applyTextEdits(const Client *client, const Utils::FilePath &filePath, const QList &edits); +bool LANGUAGECLIENT_EXPORT applyDocumentChange(const Client *client, + const LanguageServerProtocol::DocumentChange &change); void LANGUAGECLIENT_EXPORT applyTextEdit(TextEditor::TextDocumentManipulatorInterface &manipulator, const LanguageServerProtocol::TextEdit &edit, bool newTextIsSnippet = false);