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 <christian.stenger@qt.io>
This commit is contained in:
David Schulz
2023-08-28 15:00:08 +02:00
parent c2ba218583
commit ac5ab71db8
9 changed files with 364 additions and 28 deletions

View File

@@ -77,4 +77,46 @@ bool SemanticTokensClientCapabilities::isValid() const
&& contains(formatsKey);
}
const char resourceOperationCreate[] = "create";
const char resourceOperationRename[] = "rename";
const char resourceOperationDelete[] = "delete";
std::optional<QList<WorkspaceClientCapabilities::WorkspaceEditCapabilities::ResourceOperationKind>>
WorkspaceClientCapabilities::WorkspaceEditCapabilities::resourceOperations() const
{
if (!contains(resourceOperationsKey))
return std::nullopt;
QList<ResourceOperationKind> 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<ResourceOperationKind> &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

View File

@@ -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<QList<ResourceOperationKind>> resourceOperations() const;
void setResourceOperations(const QList<ResourceOperationKind> &resourceOperations);
void clearResourceOperations() { remove(resourceOperationsKey); }
};
// Capabilities specific to `WorkspaceEdit`s

View File

@@ -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";

View File

@@ -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 <utils/textutils.h>
@@ -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<CreateFile>(value);
else if (kind == "rename")
emplace<RenameFile>(value);
else if (kind == "delete")
emplace<DeleteFile>(value);
else
emplace<TextDocumentEdit>(value);
}
using DocumentChangeBase = std::variant<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>;
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

View File

@@ -285,6 +285,106 @@ public:
bool isValid() const override { return contains(textDocumentKey) && contains(editsKey); }
};
class CreateFileOptions : public JsonObject
{
public:
using JsonObject::JsonObject;
std::optional<bool> overwrite() const { return optionalValue<bool>(overwriteKey); }
void setOverwrite(bool overwrite) { insert(overwriteKey, overwrite); }
void clearOverwrite() { remove(overwriteKey); }
std::optional<bool> ignoreIfExists() const { return optionalValue<bool>(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<QString>(uriKey)); }
void setUri(const DocumentUri &uri) { insert(uriKey, uri); }
std::optional<CreateFileOptions> options() const
{ return optionalValue<CreateFileOptions>(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<QString>(oldUriKey)); }
void setOldUri(const DocumentUri &oldUri) { insert(oldUriKey, oldUri); }
DocumentUri newUri() const { return DocumentUri::fromProtocol(typedValue<QString>(newUriKey)); }
void setNewUri(const DocumentUri &newUri) { insert(newUriKey, newUri); }
std::optional<CreateFileOptions> options() const
{ return optionalValue<CreateFileOptions>(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<bool> recursive() const { return optionalValue<bool>(recursiveKey); }
void setRecursive(bool recursive) { insert(recursiveKey, recursive); }
void clearRecursive() { remove(recursiveKey); }
std::optional<bool> ignoreIfNotExists() const { return optionalValue<bool>(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<QString>(uriKey)); }
void setUri(const DocumentUri &uri) { insert(uriKey, uri); }
std::optional<DeleteFileOptions> options() const
{ return optionalValue<DeleteFileOptions>(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<TextDocumentEdit, CreateFile, RenameFile, DeleteFile>
{
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<QList<TextDocumentEdit>> documentChanges() const
{ return optionalArray<TextDocumentEdit>(documentChangesKey); }
void setDocumentChanges(const QList<TextDocumentEdit> &changes)
std::optional<QList<DocumentChange>> documentChanges() const
{ return optionalArray<DocumentChange>(documentChangesKey); }
void setDocumentChanges(const QList<DocumentChange> &changes)
{ insertArray(documentChangesKey, changes); }
};

View File

@@ -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;

View File

@@ -539,24 +539,50 @@ Utils::SearchResultItems generateReplaceItems(const WorkspaceEdit &edits,
bool limitToProjects,
const DocumentUri::PathMapper &pathMapper)
{
Utils::SearchResultItems items;
auto convertEdits = [](const QList<TextEdit> &edits) {
return Utils::transform(edits, [](const TextEdit &edit) {
return ItemData{SymbolSupport::convertRange(edit.range()), QVariant(edit)};
});
};
QMap<Utils::FilePath, QList<ItemData>> rangesInDocument;
auto documentChanges = edits.documentChanges().value_or(QList<TextDocumentEdit>());
auto documentChanges = edits.documentChanges().value_or(QList<DocumentChange>());
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<TextDocumentEdit>(documentChange)) {
const TextDocumentEdit edit = std::get<TextDocumentEdit>(documentChange);
rangesInDocument[edit.textDocument().uri().toFilePath(pathMapper)] = convertEdits(
edit.edits());
} else {
Utils::SearchResultItem item;
if (std::holds_alternative<LanguageServerProtocol::CreateFile>(documentChange)) {
auto op = std::get<LanguageServerProtocol::CreateFile>(documentChange);
item.setLineText(op.message(pathMapper));
item.setFilePath(op.uri().toFilePath(pathMapper));
item.setUserData(QVariant(op));
} else if (std::holds_alternative<RenameFile>(documentChange)) {
auto op = std::get<RenameFile>(documentChange);
item.setLineText(op.message(pathMapper));
item.setFilePath(op.oldUri().toFilePath(pathMapper));
item.setUserData(QVariant(op));
} else if (std::holds_alternative<LanguageServerProtocol::DeleteFile>(documentChange)) {
auto op = std::get<LanguageServerProtocol::DeleteFile>(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<Utils::FilePath> affectedNonOpenFilePaths;
QMap<Utils::FilePath, QList<TextEdit>> editsForDocuments;
QList<DocumentChange> 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());

View File

@@ -120,11 +120,10 @@ void applyTextEdit(TextDocumentManipulatorInterface &manipulator,
bool applyWorkspaceEdit(const Client *client, const WorkspaceEdit &edit)
{
bool result = true;
const QList<TextDocumentEdit> &documentChanges
= edit.documentChanges().value_or(QList<TextDocumentEdit>());
const auto documentChanges = edit.documentChanges().value_or(QList<DocumentChange>());
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<WorkspaceEdit> edit = action.edit()) {
if (diagnostics.isEmpty()) {
QList<TextEdit> edits;
if (std::optional<QList<TextDocumentEdit>> documentChanges = edit->documentChanges()) {
QList<TextDocumentEdit> changesForUri = Utils::filtered(
*documentChanges, [uri](const TextDocumentEdit &edit) {
return edit.textDocument().uri() == uri;
});
for (const TextDocumentEdit &edit : changesForUri)
edits << edit.edits();
if (std::optional<QList<DocumentChange>> documentChanges = edit->documentChanges()) {
for (const DocumentChange &change : *documentChanges) {
if (auto edit = std::get_if<TextDocumentEdit>(&change)) {
if (edit->textDocument().uri() == uri)
edits << edit->edits();
}
}
} else if (std::optional<WorkspaceEdit::Changes> 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<TextDocumentEdit>(change)) {
return applyTextDocumentEdit(client, std::get<TextDocumentEdit>(change));
} else if (std::holds_alternative<LanguageServerProtocol::CreateFile>(change)) {
const auto createOperation = std::get<LanguageServerProtocol::CreateFile>(change);
const FilePath filePath = createOperation.uri().toFilePath(client->hostPathMapper());
if (filePath.exists()) {
if (const std::optional<CreateFileOptions> 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<RenameFile>(change)) {
const RenameFile renameOperation = std::get<RenameFile>(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<CreateFileOptions> 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<LanguageServerProtocol::DeleteFile>(change)) {
const auto deleteOperation = std::get<LanguageServerProtocol::DeleteFile>(change);
const FilePath filePath = deleteOperation.uri().toFilePath(client->hostPathMapper());
if (const std::optional<DeleteFileOptions> 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

View File

@@ -35,6 +35,8 @@ bool LANGUAGECLIENT_EXPORT applyTextEdits(const Client *client,
bool LANGUAGECLIENT_EXPORT applyTextEdits(const Client *client,
const Utils::FilePath &filePath,
const QList<LanguageServerProtocol::TextEdit> &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);