Index: clang-tools-extra/clangd/CMakeLists.txt =================================================================== --- clang-tools-extra/clangd/CMakeLists.txt +++ clang-tools-extra/clangd/CMakeLists.txt @@ -70,6 +70,8 @@ index/dex/PostingList.cpp index/dex/Trigram.cpp + refactor/Tweak.cpp + LINK_LIBS clangAST clangASTMatchers @@ -107,6 +109,7 @@ ${CLANGD_ATOMIC_LIB} ) +add_subdirectory(refactor/tweaks) if( LLVM_LIB_FUZZING_ENGINE OR LLVM_USE_SANITIZE_COVERAGE ) add_subdirectory(fuzzer) endif() Index: clang-tools-extra/clangd/ClangdLSPServer.cpp =================================================================== --- clang-tools-extra/clangd/ClangdLSPServer.cpp +++ clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -8,11 +8,14 @@ #include "ClangdLSPServer.h" #include "Diagnostics.h" +#include "Protocol.h" #include "SourceCode.h" #include "Trace.h" #include "URI.h" +#include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/Support/Errc.h" +#include "llvm/Support/Error.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/Path.h" #include "llvm/Support/ScopedPrinter.h" @@ -30,6 +33,28 @@ } }; +/// 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 Syms, SymbolKindBitset Kinds) { for (auto &S : Syms) { @@ -337,7 +362,9 @@ {"referencesProvider", true}, {"executeCommandProvider", llvm::json::Object{ - {"commands", {ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND}}, + {"commands", + {ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND, + ExecuteCommandParams::CLANGD_APPLY_TWEAK}}, }}, }}}}); } @@ -399,7 +426,7 @@ void ClangdLSPServer::onCommand(const ExecuteCommandParams &Params, Callback Reply) { - auto ApplyEdit = [&](WorkspaceEdit WE) { + auto ApplyEdit = [this](WorkspaceEdit WE) { ApplyWorkspaceEditParams Edit; Edit.edit = std::move(WE); // Ideally, we would wait for the response and if there is no error, we @@ -419,6 +446,32 @@ Reply("Fix applied."); 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 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 { // We should not get here because ExecuteCommandParams would not have // parsed in the first place and this handler should not be called. But if @@ -600,28 +653,50 @@ void ClangdLSPServer::onCodeAction(const CodeActionParams &Params, Callback Reply) { - auto Code = DraftMgr.getDraft(Params.textDocument.uri.file()); + URIForFile File = Params.textDocument.uri; + auto Code = DraftMgr.getDraft(File.file()); if (!Code) return Reply(llvm::make_error( "onCodeAction called for non-added file", ErrorCode::InvalidParams)); // We provide a code action for Fixes on the specified diagnostics. - std::vector Actions; + std::vector FixIts; for (const Diagnostic &D : Params.context.diagnostics) { - for (auto &F : getFixes(Params.textDocument.uri.file(), D)) { - Actions.push_back(toCodeAction(F, Params.textDocument.uri)); - Actions.back().diagnostics = {D}; + for (auto &F : getFixes(File.file(), D)) { + FixIts.push_back(toCodeAction(F, Params.textDocument.uri)); + FixIts.back().diagnostics = {D}; } } - if (SupportsCodeAction) - Reply(llvm::json::Array(Actions)); - else { - std::vector Commands; - for (const auto &Action : Actions) - if (auto Command = asCommand(Action)) - Commands.push_back(std::move(*Command)); - Reply(llvm::json::Array(Commands)); - } + // Now enumerate the semantic code actions. + auto ConsumeActions = + [this](decltype(Reply) Reply, URIForFile File, std::string Code, + Range Selection, std::vector FixIts, + llvm::Expected> Tweaks) { + if (!Tweaks) { + elog("error while getting semantic code actions: {0}", + Tweaks.takeError()); + return Reply(llvm::json::Array(FixIts)); + } + + std::vector 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 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, Index: clang-tools-extra/clangd/ClangdServer.h =================================================================== --- clang-tools-extra/clangd/ClangdServer.h +++ clang-tools-extra/clangd/ClangdServer.h @@ -20,8 +20,10 @@ #include "index/Background.h" #include "index/FileIndex.h" #include "index/Index.h" +#include "refactor/Tweak.h" #include "clang/Tooling/CompilationDatabase.h" #include "clang/Tooling/Core/Replacement.h" +#include "llvm/ADT/FunctionExtras.h" #include "llvm/ADT/IntrusiveRefCntPtr.h" #include "llvm/ADT/Optional.h" #include "llvm/ADT/StringRef.h" @@ -201,6 +203,18 @@ void rename(PathRef File, Position Pos, llvm::StringRef NewName, Callback> 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> CB); + + /// Apply the code tweak with a specified \p ID. + void applyTweak(PathRef File, Range Sel, TweakID ID, + Callback CB); + /// Only for testing purposes. /// 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. Index: clang-tools-extra/clangd/ClangdServer.cpp =================================================================== --- clang-tools-extra/clangd/ClangdServer.cpp +++ clang-tools-extra/clangd/ClangdServer.cpp @@ -15,11 +15,13 @@ #include "XRefs.h" #include "index/FileIndex.h" #include "index/Merge.h" +#include "refactor/Tweak.h" #include "clang/Format/Format.h" #include "clang/Frontend/CompilerInstance.h" #include "clang/Frontend/CompilerInvocation.h" #include "clang/Lex/Preprocessor.h" #include "clang/Tooling/CompilationDatabase.h" +#include "clang/Tooling/Core/Replacement.h" #include "clang/Tooling/Refactoring/RefactoringResultConsumer.h" #include "clang/Tooling/Refactoring/Rename/RenamingAction.h" #include "llvm/ADT/ArrayRef.h" @@ -27,10 +29,12 @@ #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringRef.h" #include "llvm/Support/Errc.h" +#include "llvm/Support/Error.h" #include "llvm/Support/FileSystem.h" #include "llvm/Support/Path.h" #include "llvm/Support/raw_ostream.h" #include +#include #include namespace clang { @@ -321,6 +325,56 @@ "Rename", File, Bind(Action, File.str(), NewName.str(), std::move(CB))); } +void ClangdServer::enumerateTweaks(PathRef File, Range Sel, + Callback> CB) { + auto Action = [Sel](decltype(CB) CB, std::string File, + Expected 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 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 CB) { + auto Action = [ID, Sel](decltype(CB) CB, std::string File, + Expected 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, llvm::unique_function Callback) { auto Action = [](decltype(Callback) Callback, Index: clang-tools-extra/clangd/Protocol.h =================================================================== --- clang-tools-extra/clangd/Protocol.h +++ clang-tools-extra/clangd/Protocol.h @@ -631,6 +631,14 @@ bool fromJSON(const llvm::json::Value &, WorkspaceEdit &); llvm::json::Value toJSON(const WorkspaceEdit &WE); +struct TweakArgs { + URIForFile file; + std::string tweakID; + Range selection; +}; +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 /// 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 @@ -642,12 +650,15 @@ struct ExecuteCommandParams { // Command to apply fix-its. Uses WorkspaceEdit as argument. 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 std::string command; // Arguments llvm::Optional workspaceEdit; + llvm::Optional tweakArgs; }; bool fromJSON(const llvm::json::Value &, ExecuteCommandParams &); @@ -669,6 +680,7 @@ /// Used to filter code actions. llvm::Optional kind; const static llvm::StringLiteral QUICKFIX_KIND; + const static llvm::StringLiteral REFACTOR_KIND; /// The diagnostics that this code action resolves. llvm::Optional> diagnostics; Index: clang-tools-extra/clangd/Protocol.cpp =================================================================== --- clang-tools-extra/clangd/Protocol.cpp +++ clang-tools-extra/clangd/Protocol.cpp @@ -421,6 +421,9 @@ const llvm::StringLiteral ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND = "clangd.applyFix"; +const llvm::StringLiteral ExecuteCommandParams::CLANGD_APPLY_TWEAK = + "clangd.applyCodeAction"; + bool fromJSON(const llvm::json::Value &Params, ExecuteCommandParams &R) { llvm::json::ObjectMapper O(Params); if (!O || !O.map("command", R.command)) @@ -431,6 +434,9 @@ return Args && Args->size() == 1 && 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. } @@ -497,10 +503,13 @@ auto Cmd = llvm::json::Object{{"title", C.title}, {"command", C.command}}; if (C.workspaceEdit) Cmd["arguments"] = {*C.workspaceEdit}; + if (C.tweakArgs) + Cmd["arguments"] = {*C.tweakArgs}; return std::move(Cmd); } const llvm::StringLiteral CodeAction::QUICKFIX_KIND = "quickfix"; +const llvm::StringLiteral CodeAction::REFACTOR_KIND = "refactor"; llvm::json::Value toJSON(const CodeAction &CA) { auto CodeAction = llvm::json::Object{{"title", CA.title}}; @@ -544,6 +553,17 @@ 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("tweakID", A.tweakID) && + O.map("selection", A.selection) && O.map("file", A.file); +} + +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) { return llvm::json::Object{{"edit", Params.edit}}; } Index: clang-tools-extra/clangd/SourceCode.h =================================================================== --- clang-tools-extra/clangd/SourceCode.h +++ clang-tools-extra/clangd/SourceCode.h @@ -54,6 +54,11 @@ /// FIXME: This should return an error if the location is invalid. 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 sourceLocationInMainFile(const SourceManager &SM, + Position P); + // Converts a half-open clang source range to an LSP range. // Note that clang also uses closed source ranges, which this can't handle! Range halfOpenToRange(const SourceManager &SM, CharSourceRange R); Index: clang-tools-extra/clangd/SourceCode.cpp =================================================================== --- clang-tools-extra/clangd/SourceCode.cpp +++ clang-tools-extra/clangd/SourceCode.cpp @@ -141,6 +141,16 @@ return P; } +llvm::Expected 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) { // Clang is 1-based, LSP uses 0-based indexes. Position Begin = sourceLocToPosition(SM, R.getBegin()); Index: clang-tools-extra/clangd/refactor/Tweak.h =================================================================== --- /dev/null +++ clang-tools-extra/clangd/refactor/Tweak.h @@ -0,0 +1,99 @@ +//===--- Tweak.h -------------------------------------------------*- C++-*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +// 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 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 \ + 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> +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> prepareTweak(TweakID ID, + const Tweak::Selection &S); + +} // namespace clangd +} // namespace clang + +#endif Index: clang-tools-extra/clangd/refactor/Tweak.cpp =================================================================== --- /dev/null +++ clang-tools-extra/clangd/refactor/Tweak.cpp @@ -0,0 +1,70 @@ +//===--- Tweak.cpp --------------------------------------*- C++-*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// +#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 +#include + +LLVM_INSTANTIATE_REGISTRY(llvm::Registry); + +namespace clang { +namespace clangd { + +/// A handy typedef to save some typing. +typedef llvm::Registry TweakRegistry; + +std::vector> prepareTweaks(const Tweak::Selection &S) { +#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 + + std::vector> Available; + for (const auto &E : TweakRegistry::entries()) { + std::unique_ptr 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 &L, + const std::unique_ptr &R) { return L->id() < R->id(); }); + return Available; +} + +llvm::Expected> 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 T = It->instantiate(); + if (!T->prepare(S)) + return llvm::createStringError(llvm::inconvertibleErrorCode(), + "failed to prepare() a check"); + return T; +} + +} // namespace clangd +} // namespace clang Index: clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt =================================================================== --- /dev/null +++ clang-tools-extra/clangd/refactor/tweaks/CMakeLists.txt @@ -0,0 +1,11 @@ +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 +# $ to a list of sources, see +# clangd/tool/CMakeLists.txt for an example. +add_clang_library(clangDaemonTweaks OBJECT + ) Index: clang-tools-extra/clangd/tool/CMakeLists.txt =================================================================== --- clang-tools-extra/clangd/tool/CMakeLists.txt +++ clang-tools-extra/clangd/tool/CMakeLists.txt @@ -3,6 +3,7 @@ add_clang_tool(clangd ClangdMain.cpp + $ ) set(LLVM_LINK_COMPONENTS Index: clang-tools-extra/test/clangd/initialize-params.test =================================================================== --- clang-tools-extra/test/clangd/initialize-params.test +++ clang-tools-extra/test/clangd/initialize-params.test @@ -25,7 +25,8 @@ # CHECK-NEXT: "documentSymbolProvider": true, # CHECK-NEXT: "executeCommandProvider": { # CHECK-NEXT: "commands": [ -# CHECK-NEXT: "clangd.applyFix" +# CHECK-NEXT: "clangd.applyFix", +# CHECK-NEXT: "clangd.applyCodeAction" # CHECK-NEXT: ] # CHECK-NEXT: }, # CHECK-NEXT: "hoverProvider": true,