[clangd] Interfaces for writing code tweaks

Summary:
The code tweaks are an implementation of mini-refactorings exposed
via the LSP code actions. They run in two stages:
  - Stage 1. Decides whether the action is available to the user and
    collects all the information required to finish the action.
    Should be cheap, since this will run over all the actions known to
    clangd on each textDocument/codeAction request from the client.

  - Stage 2. Uses information from stage 1 to produce the actual edits
    that the code action should perform. This stage can be expensive and
    will only run if the user chooses to perform the specified action in
    the UI.

One unfortunate consequence of this change is increased latency of
processing the textDocument/codeAction requests, which now wait for an
AST. However, we cannot avoid this with what we have available in the LSP
today.

Reviewers: kadircet, ioeric, hokein, sammccall

Reviewed By: sammccall

Subscribers: mgrang, mgorny, MaskRay, jkorous, arphaman, cfe-commits

Differential Revision: https://reviews.llvm.org/D56267

llvm-svn: 352494
This commit is contained in:
Ilya Biryukov 2019-01-29 14:17:36 +00:00
parent 81675c8f3b
commit cce67a32cf
15 changed files with 416 additions and 19 deletions

View File

@ -71,6 +71,8 @@ add_clang_library(clangDaemon
index/dex/PostingList.cpp index/dex/PostingList.cpp
index/dex/Trigram.cpp index/dex/Trigram.cpp
refactor/Tweak.cpp
LINK_LIBS LINK_LIBS
clangAST clangAST
clangASTMatchers clangASTMatchers
@ -108,6 +110,7 @@ add_clang_library(clangDaemon
${CLANGD_ATOMIC_LIB} ${CLANGD_ATOMIC_LIB}
) )
add_subdirectory(refactor/tweaks)
if( LLVM_LIB_FUZZING_ENGINE OR LLVM_USE_SANITIZE_COVERAGE ) if( LLVM_LIB_FUZZING_ENGINE OR LLVM_USE_SANITIZE_COVERAGE )
add_subdirectory(fuzzer) add_subdirectory(fuzzer)
endif() endif()

View File

@ -8,11 +8,14 @@
#include "ClangdLSPServer.h" #include "ClangdLSPServer.h"
#include "Diagnostics.h" #include "Diagnostics.h"
#include "Protocol.h"
#include "SourceCode.h" #include "SourceCode.h"
#include "Trace.h" #include "Trace.h"
#include "URI.h" #include "URI.h"
#include "clang/Tooling/Core/Replacement.h"
#include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/ScopeExit.h"
#include "llvm/Support/Errc.h" #include "llvm/Support/Errc.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FormatVariadic.h" #include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/Path.h" #include "llvm/Support/Path.h"
#include "llvm/Support/ScopedPrinter.h" #include "llvm/Support/ScopedPrinter.h"
@ -30,6 +33,28 @@ public:
} }
}; };
/// Transforms a tweak into a code action that would apply it if executed.
/// EXPECTS: T.prepare() was called and returned true.
CodeAction toCodeAction(const ClangdServer::TweakRef &T, const URIForFile &File,
Range Selection) {
CodeAction CA;
CA.title = T.Title;
CA.kind = CodeAction::REFACTOR_KIND;
// This tweak may have an expensive second stage, we only run it if the user
// actually chooses it in the UI. We reply with a command that would run the
// corresponding tweak.
// FIXME: for some tweaks, computing the edits is cheap and we could send them
// directly.
CA.command.emplace();
CA.command->title = T.Title;
CA.command->command = Command::CLANGD_APPLY_TWEAK;
CA.command->tweakArgs.emplace();
CA.command->tweakArgs->file = File;
CA.command->tweakArgs->tweakID = T.ID;
CA.command->tweakArgs->selection = Selection;
return CA;
};
void adjustSymbolKinds(llvm::MutableArrayRef<DocumentSymbol> Syms, void adjustSymbolKinds(llvm::MutableArrayRef<DocumentSymbol> Syms,
SymbolKindBitset Kinds) { SymbolKindBitset Kinds) {
for (auto &S : Syms) { for (auto &S : Syms) {
@ -338,7 +363,9 @@ void ClangdLSPServer::onInitialize(const InitializeParams &Params,
{"referencesProvider", true}, {"referencesProvider", true},
{"executeCommandProvider", {"executeCommandProvider",
llvm::json::Object{ llvm::json::Object{
{"commands", {ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND}}, {"commands",
{ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND,
ExecuteCommandParams::CLANGD_APPLY_TWEAK}},
}}, }},
}}}}); }}}});
} }
@ -400,7 +427,7 @@ void ClangdLSPServer::onFileEvent(const DidChangeWatchedFilesParams &Params) {
void ClangdLSPServer::onCommand(const ExecuteCommandParams &Params, void ClangdLSPServer::onCommand(const ExecuteCommandParams &Params,
Callback<llvm::json::Value> Reply) { Callback<llvm::json::Value> Reply) {
auto ApplyEdit = [&](WorkspaceEdit WE) { auto ApplyEdit = [this](WorkspaceEdit WE) {
ApplyWorkspaceEditParams Edit; ApplyWorkspaceEditParams Edit;
Edit.edit = std::move(WE); Edit.edit = std::move(WE);
// Ideally, we would wait for the response and if there is no error, we // Ideally, we would wait for the response and if there is no error, we
@ -420,6 +447,31 @@ void ClangdLSPServer::onCommand(const ExecuteCommandParams &Params,
Reply("Fix applied."); Reply("Fix applied.");
ApplyEdit(*Params.workspaceEdit); ApplyEdit(*Params.workspaceEdit);
} else if (Params.command == ExecuteCommandParams::CLANGD_APPLY_TWEAK &&
Params.tweakArgs) {
auto Code = DraftMgr.getDraft(Params.tweakArgs->file.file());
if (!Code)
return Reply(llvm::createStringError(
llvm::inconvertibleErrorCode(),
"trying to apply a code action for a non-added file"));
auto Action = [ApplyEdit](decltype(Reply) Reply, URIForFile File,
std::string Code,
llvm::Expected<tooling::Replacements> R) {
if (!R)
return Reply(R.takeError());
WorkspaceEdit WE;
WE.changes.emplace();
(*WE.changes)[File.uri()] = replacementsToEdits(Code, *R);
Reply("Fix applied.");
ApplyEdit(std::move(WE));
};
Server->applyTweak(Params.tweakArgs->file.file(),
Params.tweakArgs->selection, Params.tweakArgs->tweakID,
Bind(Action, std::move(Reply), Params.tweakArgs->file,
std::move(*Code)));
} else { } else {
// We should not get here because ExecuteCommandParams would not have // We should not get here because ExecuteCommandParams would not have
// parsed in the first place and this handler should not be called. But if // parsed in the first place and this handler should not be called. But if
@ -601,28 +653,53 @@ static llvm::Optional<Command> asCommand(const CodeAction &Action) {
void ClangdLSPServer::onCodeAction(const CodeActionParams &Params, void ClangdLSPServer::onCodeAction(const CodeActionParams &Params,
Callback<llvm::json::Value> Reply) { Callback<llvm::json::Value> Reply) {
auto Code = DraftMgr.getDraft(Params.textDocument.uri.file()); URIForFile File = Params.textDocument.uri;
auto Code = DraftMgr.getDraft(File.file());
if (!Code) if (!Code)
return Reply(llvm::make_error<LSPError>( return Reply(llvm::make_error<LSPError>(
"onCodeAction called for non-added file", ErrorCode::InvalidParams)); "onCodeAction called for non-added file", ErrorCode::InvalidParams));
// We provide a code action for Fixes on the specified diagnostics. // We provide a code action for Fixes on the specified diagnostics.
std::vector<CodeAction> Actions; std::vector<CodeAction> FixIts;
for (const Diagnostic &D : Params.context.diagnostics) { for (const Diagnostic &D : Params.context.diagnostics) {
for (auto &F : getFixes(Params.textDocument.uri.file(), D)) { for (auto &F : getFixes(File.file(), D)) {
Actions.push_back(toCodeAction(F, Params.textDocument.uri)); FixIts.push_back(toCodeAction(F, Params.textDocument.uri));
Actions.back().diagnostics = {D}; FixIts.back().diagnostics = {D};
} }
} }
if (SupportsCodeAction) // Now enumerate the semantic code actions.
Reply(llvm::json::Array(Actions)); auto ConsumeActions =
else { [this](decltype(Reply) Reply, URIForFile File, std::string Code,
std::vector<Command> Commands; Range Selection, std::vector<CodeAction> FixIts,
for (const auto &Action : Actions) llvm::Expected<std::vector<ClangdServer::TweakRef>> Tweaks) {
if (auto Command = asCommand(Action)) if (!Tweaks) {
Commands.push_back(std::move(*Command)); auto Err = Tweaks.takeError();
Reply(llvm::json::Array(Commands)); if (Err.isA<CancelledError>())
} return Reply(std::move(Err)); // do no logging, this is expected.
elog("error while getting semantic code actions: {0}",
std::move(Err));
return Reply(llvm::json::Array(FixIts));
}
std::vector<CodeAction> Actions = std::move(FixIts);
Actions.reserve(Actions.size() + Tweaks->size());
for (const auto &T : *Tweaks)
Actions.push_back(toCodeAction(T, File, Selection));
if (SupportsCodeAction)
return Reply(llvm::json::Array(Actions));
std::vector<Command> Commands;
for (const auto &Action : Actions) {
if (auto Command = asCommand(Action))
Commands.push_back(std::move(*Command));
}
return Reply(llvm::json::Array(Commands));
};
Server->enumerateTweaks(File.file(), Params.range,
Bind(ConsumeActions, std::move(Reply),
std::move(File), std::move(*Code), Params.range,
std::move(FixIts)));
} }
void ClangdLSPServer::onCompletion(const CompletionParams &Params, void ClangdLSPServer::onCompletion(const CompletionParams &Params,

View File

@ -16,11 +16,13 @@
#include "XRefs.h" #include "XRefs.h"
#include "index/FileIndex.h" #include "index/FileIndex.h"
#include "index/Merge.h" #include "index/Merge.h"
#include "refactor/Tweak.h"
#include "clang/Format/Format.h" #include "clang/Format/Format.h"
#include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/CompilerInstance.h"
#include "clang/Frontend/CompilerInvocation.h" #include "clang/Frontend/CompilerInvocation.h"
#include "clang/Lex/Preprocessor.h" #include "clang/Lex/Preprocessor.h"
#include "clang/Tooling/CompilationDatabase.h" #include "clang/Tooling/CompilationDatabase.h"
#include "clang/Tooling/Core/Replacement.h"
#include "clang/Tooling/Refactoring/RefactoringResultConsumer.h" #include "clang/Tooling/Refactoring/RefactoringResultConsumer.h"
#include "clang/Tooling/Refactoring/Rename/RenamingAction.h" #include "clang/Tooling/Refactoring/Rename/RenamingAction.h"
#include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/ArrayRef.h"
@ -28,10 +30,12 @@
#include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/ScopeExit.h"
#include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringRef.h"
#include "llvm/Support/Errc.h" #include "llvm/Support/Errc.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h" #include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h" #include "llvm/Support/Path.h"
#include "llvm/Support/raw_ostream.h" #include "llvm/Support/raw_ostream.h"
#include <future> #include <future>
#include <memory>
#include <mutex> #include <mutex>
namespace clang { namespace clang {
@ -325,6 +329,56 @@ void ClangdServer::rename(PathRef File, Position Pos, llvm::StringRef NewName,
"Rename", File, Bind(Action, File.str(), NewName.str(), std::move(CB))); "Rename", File, Bind(Action, File.str(), NewName.str(), std::move(CB)));
} }
void ClangdServer::enumerateTweaks(PathRef File, Range Sel,
Callback<std::vector<TweakRef>> CB) {
auto Action = [Sel](decltype(CB) CB, std::string File,
Expected<InputsAndAST> InpAST) {
if (!InpAST)
return CB(InpAST.takeError());
auto &AST = InpAST->AST;
auto CursorLoc = sourceLocationInMainFile(
AST.getASTContext().getSourceManager(), Sel.start);
if (!CursorLoc)
return CB(CursorLoc.takeError());
Tweak::Selection Inputs = {InpAST->Inputs.Contents, InpAST->AST,
*CursorLoc};
std::vector<TweakRef> Res;
for (auto &T : prepareTweaks(Inputs))
Res.push_back({T->id(), T->title()});
CB(std::move(Res));
};
WorkScheduler.runWithAST("EnumerateTweaks", File,
Bind(Action, std::move(CB), File.str()));
}
void ClangdServer::applyTweak(PathRef File, Range Sel, TweakID ID,
Callback<tooling::Replacements> CB) {
auto Action = [ID, Sel](decltype(CB) CB, std::string File,
Expected<InputsAndAST> InpAST) {
if (!InpAST)
return CB(InpAST.takeError());
auto &AST = InpAST->AST;
auto CursorLoc = sourceLocationInMainFile(
AST.getASTContext().getSourceManager(), Sel.start);
if (!CursorLoc)
return CB(CursorLoc.takeError());
Tweak::Selection Inputs = {InpAST->Inputs.Contents, InpAST->AST,
*CursorLoc};
auto A = prepareTweak(ID, Inputs);
if (!A)
return CB(A.takeError());
// FIXME: run formatter on top of resulting replacements.
return CB((*A)->apply(Inputs));
};
WorkScheduler.runWithAST("ApplyTweak", File,
Bind(Action, std::move(CB), File.str()));
}
void ClangdServer::dumpAST(PathRef File, void ClangdServer::dumpAST(PathRef File,
llvm::unique_function<void(std::string)> Callback) { llvm::unique_function<void(std::string)> Callback) {
auto Action = [](decltype(Callback) Callback, auto Action = [](decltype(Callback) Callback,

View File

@ -21,8 +21,10 @@
#include "index/Background.h" #include "index/Background.h"
#include "index/FileIndex.h" #include "index/FileIndex.h"
#include "index/Index.h" #include "index/Index.h"
#include "refactor/Tweak.h"
#include "clang/Tooling/CompilationDatabase.h" #include "clang/Tooling/CompilationDatabase.h"
#include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Core/Replacement.h"
#include "llvm/ADT/FunctionExtras.h"
#include "llvm/ADT/IntrusiveRefCntPtr.h" #include "llvm/ADT/IntrusiveRefCntPtr.h"
#include "llvm/ADT/Optional.h" #include "llvm/ADT/Optional.h"
#include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringRef.h"
@ -211,6 +213,18 @@ public:
void rename(PathRef File, Position Pos, llvm::StringRef NewName, void rename(PathRef File, Position Pos, llvm::StringRef NewName,
Callback<std::vector<tooling::Replacement>> CB); Callback<std::vector<tooling::Replacement>> CB);
struct TweakRef {
TweakID ID; /// ID to pass for applyTweak.
std::string Title; /// A single-line message to show in the UI.
};
/// Enumerate the code tweaks available to the user at a specified point.
void enumerateTweaks(PathRef File, Range Sel,
Callback<std::vector<TweakRef>> CB);
/// Apply the code tweak with a specified \p ID.
void applyTweak(PathRef File, Range Sel, TweakID ID,
Callback<tooling::Replacements> CB);
/// Only for testing purposes. /// Only for testing purposes.
/// Waits until all requests to worker thread are finished and dumps AST for /// Waits until all requests to worker thread are finished and dumps AST for
/// \p File. \p File must be in the list of added documents. /// \p File. \p File must be in the list of added documents.

View File

@ -421,6 +421,9 @@ bool fromJSON(const llvm::json::Value &Params, WorkspaceEdit &R) {
const llvm::StringLiteral ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND = const llvm::StringLiteral ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND =
"clangd.applyFix"; "clangd.applyFix";
const llvm::StringLiteral ExecuteCommandParams::CLANGD_APPLY_TWEAK =
"clangd.applyTweak";
bool fromJSON(const llvm::json::Value &Params, ExecuteCommandParams &R) { bool fromJSON(const llvm::json::Value &Params, ExecuteCommandParams &R) {
llvm::json::ObjectMapper O(Params); llvm::json::ObjectMapper O(Params);
if (!O || !O.map("command", R.command)) if (!O || !O.map("command", R.command))
@ -431,6 +434,8 @@ bool fromJSON(const llvm::json::Value &Params, ExecuteCommandParams &R) {
return Args && Args->size() == 1 && return Args && Args->size() == 1 &&
fromJSON(Args->front(), R.workspaceEdit); fromJSON(Args->front(), R.workspaceEdit);
} }
if (R.command == ExecuteCommandParams::CLANGD_APPLY_TWEAK)
return Args && Args->size() == 1 && fromJSON(Args->front(), R.tweakArgs);
return false; // Unrecognized command. return false; // Unrecognized command.
} }
@ -497,10 +502,13 @@ llvm::json::Value toJSON(const Command &C) {
auto Cmd = llvm::json::Object{{"title", C.title}, {"command", C.command}}; auto Cmd = llvm::json::Object{{"title", C.title}, {"command", C.command}};
if (C.workspaceEdit) if (C.workspaceEdit)
Cmd["arguments"] = {*C.workspaceEdit}; Cmd["arguments"] = {*C.workspaceEdit};
if (C.tweakArgs)
Cmd["arguments"] = {*C.tweakArgs};
return std::move(Cmd); return std::move(Cmd);
} }
const llvm::StringLiteral CodeAction::QUICKFIX_KIND = "quickfix"; const llvm::StringLiteral CodeAction::QUICKFIX_KIND = "quickfix";
const llvm::StringLiteral CodeAction::REFACTOR_KIND = "refactor";
llvm::json::Value toJSON(const CodeAction &CA) { llvm::json::Value toJSON(const CodeAction &CA) {
auto CodeAction = llvm::json::Object{{"title", CA.title}}; auto CodeAction = llvm::json::Object{{"title", CA.title}};
@ -544,6 +552,17 @@ llvm::json::Value toJSON(const WorkspaceEdit &WE) {
return llvm::json::Object{{"changes", std::move(FileChanges)}}; return llvm::json::Object{{"changes", std::move(FileChanges)}};
} }
bool fromJSON(const llvm::json::Value &Params, TweakArgs &A) {
llvm::json::ObjectMapper O(Params);
return O && O.map("file", A.file) && O.map("selection", A.selection) &&
O.map("tweakID", A.tweakID);
}
llvm::json::Value toJSON(const TweakArgs &A) {
return llvm::json::Object{
{"tweakID", A.tweakID}, {"selection", A.selection}, {"file", A.file}};
}
llvm::json::Value toJSON(const ApplyWorkspaceEditParams &Params) { llvm::json::Value toJSON(const ApplyWorkspaceEditParams &Params) {
return llvm::json::Object{{"edit", Params.edit}}; return llvm::json::Object{{"edit", Params.edit}};
} }

View File

@ -631,6 +631,21 @@ struct WorkspaceEdit {
bool fromJSON(const llvm::json::Value &, WorkspaceEdit &); bool fromJSON(const llvm::json::Value &, WorkspaceEdit &);
llvm::json::Value toJSON(const WorkspaceEdit &WE); llvm::json::Value toJSON(const WorkspaceEdit &WE);
/// Arguments for the 'applyTweak' command. The server sends these commands as a
/// response to the textDocument/codeAction request. The client can later send a
/// command back to the server if the user requests to execute a particular code
/// tweak.
struct TweakArgs {
/// A file provided by the client on a textDocument/codeAction request.
URIForFile file;
/// A selection provided by the client on a textDocument/codeAction request.
Range selection;
/// ID of the tweak that should be executed. Corresponds to Tweak::id().
std::string tweakID;
};
bool fromJSON(const llvm::json::Value &, TweakArgs &);
llvm::json::Value toJSON(const TweakArgs &A);
/// Exact commands are not specified in the protocol so we define the /// Exact commands are not specified in the protocol so we define the
/// ones supported by Clangd here. The protocol specifies the command arguments /// ones supported by Clangd here. The protocol specifies the command arguments
/// to be "any[]" but to make this safer and more manageable, each command we /// to be "any[]" but to make this safer and more manageable, each command we
@ -642,12 +657,15 @@ llvm::json::Value toJSON(const WorkspaceEdit &WE);
struct ExecuteCommandParams { struct ExecuteCommandParams {
// Command to apply fix-its. Uses WorkspaceEdit as argument. // Command to apply fix-its. Uses WorkspaceEdit as argument.
const static llvm::StringLiteral CLANGD_APPLY_FIX_COMMAND; const static llvm::StringLiteral CLANGD_APPLY_FIX_COMMAND;
// Command to apply the code action. Uses TweakArgs as argument.
const static llvm::StringLiteral CLANGD_APPLY_TWEAK;
/// The command identifier, e.g. CLANGD_APPLY_FIX_COMMAND /// The command identifier, e.g. CLANGD_APPLY_FIX_COMMAND
std::string command; std::string command;
// Arguments // Arguments
llvm::Optional<WorkspaceEdit> workspaceEdit; llvm::Optional<WorkspaceEdit> workspaceEdit;
llvm::Optional<TweakArgs> tweakArgs;
}; };
bool fromJSON(const llvm::json::Value &, ExecuteCommandParams &); bool fromJSON(const llvm::json::Value &, ExecuteCommandParams &);
@ -669,6 +687,7 @@ struct CodeAction {
/// Used to filter code actions. /// Used to filter code actions.
llvm::Optional<std::string> kind; llvm::Optional<std::string> kind;
const static llvm::StringLiteral QUICKFIX_KIND; const static llvm::StringLiteral QUICKFIX_KIND;
const static llvm::StringLiteral REFACTOR_KIND;
/// The diagnostics that this code action resolves. /// The diagnostics that this code action resolves.
llvm::Optional<std::vector<Diagnostic>> diagnostics; llvm::Optional<std::vector<Diagnostic>> diagnostics;

View File

@ -141,6 +141,16 @@ Position sourceLocToPosition(const SourceManager &SM, SourceLocation Loc) {
return P; return P;
} }
llvm::Expected<SourceLocation> sourceLocationInMainFile(const SourceManager &SM,
Position P) {
llvm::StringRef Code = SM.getBuffer(SM.getMainFileID())->getBuffer();
auto Offset =
positionToOffset(Code, P, /*AllowColumnBeyondLineLength=*/false);
if (!Offset)
return Offset.takeError();
return SM.getLocForStartOfFile(SM.getMainFileID()).getLocWithOffset(*Offset);
}
Range halfOpenToRange(const SourceManager &SM, CharSourceRange R) { Range halfOpenToRange(const SourceManager &SM, CharSourceRange R) {
// Clang is 1-based, LSP uses 0-based indexes. // Clang is 1-based, LSP uses 0-based indexes.
Position Begin = sourceLocToPosition(SM, R.getBegin()); Position Begin = sourceLocToPosition(SM, R.getBegin());

View File

@ -56,6 +56,11 @@ Position offsetToPosition(llvm::StringRef Code, size_t Offset);
/// FIXME: This should return an error if the location is invalid. /// FIXME: This should return an error if the location is invalid.
Position sourceLocToPosition(const SourceManager &SM, SourceLocation Loc); Position sourceLocToPosition(const SourceManager &SM, SourceLocation Loc);
/// Return the file location, corresponding to \p P. Note that one should take
/// care to avoid comparing the result with expansion locations.
llvm::Expected<SourceLocation> sourceLocationInMainFile(const SourceManager &SM,
Position P);
// Converts a half-open clang source range to an LSP range. // Converts a half-open clang source range to an LSP range.
// Note that clang also uses closed source ranges, which this can't handle! // Note that clang also uses closed source ranges, which this can't handle!
Range halfOpenToRange(const SourceManager &SM, CharSourceRange R); Range halfOpenToRange(const SourceManager &SM, CharSourceRange R);

View File

@ -0,0 +1,74 @@
//===--- Tweak.cpp -----------------------------------------------*- C++-*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
#include "Tweak.h"
#include "Logger.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/Registry.h"
#include <functional>
#include <memory>
LLVM_INSTANTIATE_REGISTRY(llvm::Registry<clang::clangd::Tweak>);
namespace clang {
namespace clangd {
/// A handy typedef to save some typing.
typedef llvm::Registry<Tweak> TweakRegistry;
namespace {
/// Asserts invariants on TweakRegistry. No-op with assertion disabled.
void validateRegistry() {
#ifndef NDEBUG
llvm::StringSet<> Seen;
for (const auto &E : TweakRegistry::entries()) {
// REGISTER_TWEAK ensures E.getName() is equal to the tweak class name.
// We check that id() matches it.
assert(E.instantiate()->id() == E.getName() &&
"id should be equal to class name");
assert(Seen.try_emplace(E.getName()).second && "duplicate check id");
}
#endif
}
} // namespace
std::vector<std::unique_ptr<Tweak>> prepareTweaks(const Tweak::Selection &S) {
validateRegistry();
std::vector<std::unique_ptr<Tweak>> Available;
for (const auto &E : TweakRegistry::entries()) {
std::unique_ptr<Tweak> T = E.instantiate();
if (!T->prepare(S))
continue;
Available.push_back(std::move(T));
}
// Ensure deterministic order of the results.
llvm::sort(Available,
[](const std::unique_ptr<Tweak> &L,
const std::unique_ptr<Tweak> &R) { return L->id() < R->id(); });
return Available;
}
llvm::Expected<std::unique_ptr<Tweak>> prepareTweak(TweakID ID,
const Tweak::Selection &S) {
auto It = llvm::find_if(
TweakRegistry::entries(),
[ID](const TweakRegistry::entry &E) { return E.getName() == ID; });
if (It == TweakRegistry::end())
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"id of the tweak is invalid");
std::unique_ptr<Tweak> T = It->instantiate();
if (!T->prepare(S))
return llvm::createStringError(llvm::inconvertibleErrorCode(),
"failed to prepare() a check");
return T;
}
} // namespace clangd
} // namespace clang

View File

@ -0,0 +1,98 @@
//===--- Tweak.h -------------------------------------------------*- C++-*-===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//
// Tweaks are small refactoring-like actions that run over the AST and produce
// the set of edits as a result. They are local, i.e. they should take the
// current editor context, e.g. the cursor position and selection into account.
// The actions are executed in two stages:
// - Stage 1 should check whether the action is available in a current
// context. It should be cheap and fast to compute as it is executed for all
// available actions on every client request, which happen quite frequently.
// - Stage 2 is performed after stage 1 and can be more expensive to compute.
// It is performed when the user actually chooses the action.
//===----------------------------------------------------------------------===//
#ifndef LLVM_CLANG_TOOLS_EXTRA_CLANGD_REFACTOR_ACTIONS_TWEAK_H
#define LLVM_CLANG_TOOLS_EXTRA_CLANGD_REFACTOR_ACTIONS_TWEAK_H
#include "ClangdUnit.h"
#include "Protocol.h"
#include "clang/Tooling/Core/Replacement.h"
#include "llvm/ADT/Optional.h"
#include "llvm/ADT/StringRef.h"
namespace clang {
namespace clangd {
using TweakID = llvm::StringRef;
/// An interface base for small context-sensitive refactoring actions.
/// To implement a new tweak use the following pattern in a .cpp file:
/// class MyTweak : public Tweak {
/// public:
/// TweakID id() const override final; // definition provided by
/// // REGISTER_TWEAK.
/// // implement other methods here.
/// };
/// REGISTER_TWEAK(MyTweak);
class Tweak {
public:
/// Input to prepare and apply tweaks.
struct Selection {
/// The text of the active document.
llvm::StringRef Code;
/// Parsed AST of the active file.
ParsedAST &AST;
/// A location of the cursor in the editor.
SourceLocation Cursor;
// FIXME: add selection when there are checks relying on it.
// FIXME: provide a way to get sources and ASTs for other files.
// FIXME: cache some commonly required information (e.g. AST nodes under
// cursor) to avoid redundant AST visit in every action.
};
virtual ~Tweak() = default;
/// A unique id of the action, it is always equal to the name of the class
/// defining the Tweak. Definition is provided automatically by
/// REGISTER_TWEAK.
virtual TweakID id() const = 0;
/// Run the first stage of the action. The non-None result indicates that the
/// action is available and should be shown to the user. Returns None if the
/// action is not available.
/// This function should be fast, if the action requires non-trivial work it
/// should be moved into 'apply'.
/// Returns true iff the action is available and apply() can be called on it.
virtual bool prepare(const Selection &Sel) = 0;
/// Run the second stage of the action that would produce the actual changes.
/// EXPECTS: prepare() was called and returned true.
virtual Expected<tooling::Replacements> apply(const Selection &Sel) = 0;
/// A one-line title of the action that should be shown to the users in the
/// UI.
/// EXPECTS: prepare() was called and returned true.
virtual std::string title() const = 0;
};
// All tweaks must be registered in the .cpp file next to their definition.
#define REGISTER_TWEAK(Subclass) \
::llvm::Registry<::clang::clangd::Tweak>::Add<Subclass> \
TweakRegistrationFor##Subclass(#Subclass, /*Description=*/""); \
::clang::clangd::TweakID Subclass::id() const { \
return llvm::StringLiteral(#Subclass); \
}
/// Calls prepare() on all tweaks, returning those that can run on the
/// selection.
std::vector<std::unique_ptr<Tweak>> prepareTweaks(const Tweak::Selection &S);
// Calls prepare() on the tweak with a given ID.
// If prepare() returns false, returns an error.
// If prepare() returns true, returns the corresponding tweak.
llvm::Expected<std::unique_ptr<Tweak>> prepareTweak(TweakID ID,
const Tweak::Selection &S);
} // namespace clangd
} // namespace clang
#endif

View File

@ -0,0 +1,13 @@
include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../..)
# A target containing all code tweaks (i.e. mini-refactorings) provided by
# clangd.
# Built as an object library to make sure linker does not remove global
# constructors that register individual tweaks in a global registry.
# To enable these tweaks in exectubales or shared libraries, add
# $<TARGET_OBJECTS:obj.clangDaemonTweaks> to a list of sources, see
# clangd/tool/CMakeLists.txt for an example.
add_clang_library(clangDaemonTweaks OBJECT
Dummy.cpp # FIXME: to avoid CMake errors due to empty inputs, remove when a
# first tweak lands.
)

View File

@ -0,0 +1,9 @@
//===--- Dummy.cpp -----------------------------------------------*- C++-*-===//
//
// The LLVM Compiler Infrastructure
//
// This file is distributed under the University of Illinois Open Source
// License. See LICENSE.TXT for details.
//
//===----------------------------------------------------------------------===//
// Does nothing, only here to avoid cmake errors for empty libraries.

View File

@ -3,6 +3,7 @@ include_directories(${CMAKE_CURRENT_BINARY_DIR}/..)
add_clang_tool(clangd add_clang_tool(clangd
ClangdMain.cpp ClangdMain.cpp
$<TARGET_OBJECTS:obj.clangDaemonTweaks>
) )
set(LLVM_LINK_COMPONENTS set(LLVM_LINK_COMPONENTS

View File

@ -23,7 +23,7 @@
# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" # CHECK-NEXT: "uri": "file://{{.*}}/foo.c"
# CHECK-NEXT: } # CHECK-NEXT: }
--- ---
{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":104,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 37}},"severity":2,"message":"Using the result of an assignment as a condition without parentheses"}]}}} {"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":0,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 37}},"severity":2,"message":"Using the result of an assignment as a condition without parentheses"}]}}}
# CHECK: "id": 2, # CHECK: "id": 2,
# CHECK-NEXT: "jsonrpc": "2.0", # CHECK-NEXT: "jsonrpc": "2.0",
# CHECK-NEXT: "result": [ # CHECK-NEXT: "result": [
@ -92,7 +92,7 @@
# CHECK-NEXT: } # CHECK-NEXT: }
# CHECK-NEXT: ] # CHECK-NEXT: ]
--- ---
{"jsonrpc":"2.0","id":3,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":104,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 37}},"severity":2,"message":"Using the result of an assignment as a condition without parentheses"}]}}} {"jsonrpc":"2.0","id":3,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///foo.c"},"range":{"start":{"line":0,"character":13},"end":{"line":0,"character":35}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 32}, "end": {"line": 0, "character": 37}},"severity":2,"message":"Using the result of an assignment as a condition without parentheses"}]}}}
# Make sure unused "code" and "source" fields ignored gracefully # Make sure unused "code" and "source" fields ignored gracefully
# CHECK: "id": 3, # CHECK: "id": 3,
# CHECK-NEXT: "jsonrpc": "2.0", # CHECK-NEXT: "jsonrpc": "2.0",

View File

@ -25,7 +25,8 @@
# CHECK-NEXT: "documentSymbolProvider": true, # CHECK-NEXT: "documentSymbolProvider": true,
# CHECK-NEXT: "executeCommandProvider": { # CHECK-NEXT: "executeCommandProvider": {
# CHECK-NEXT: "commands": [ # CHECK-NEXT: "commands": [
# CHECK-NEXT: "clangd.applyFix" # CHECK-NEXT: "clangd.applyFix",
# CHECK-NEXT: "clangd.applyTweak"
# CHECK-NEXT: ] # CHECK-NEXT: ]
# CHECK-NEXT: }, # CHECK-NEXT: },
# CHECK-NEXT: "hoverProvider": true, # CHECK-NEXT: "hoverProvider": true,