diff --git a/clang-tools-extra/clangd/ClangdLSPServer.cpp b/clang-tools-extra/clangd/ClangdLSPServer.cpp --- a/clang-tools-extra/clangd/ClangdLSPServer.cpp +++ b/clang-tools-extra/clangd/ClangdLSPServer.cpp @@ -13,6 +13,7 @@ #include "Diagnostics.h" #include "Feature.h" #include "GlobalCompilationDatabase.h" +#include "IncludeCleaner.h" #include "LSPBinder.h" #include "Protocol.h" #include "SemanticHighlighting.h" @@ -27,6 +28,7 @@ #include "clang/Tooling/Core/Replacement.h" #include "llvm/ADT/ArrayRef.h" #include "llvm/ADT/FunctionExtras.h" +#include "llvm/ADT/STLExtras.h" #include "llvm/ADT/ScopeExit.h" #include "llvm/ADT/StringRef.h" #include "llvm/ADT/Twine.h" @@ -646,7 +648,8 @@ ? llvm::json::Object{{"codeActionKinds", {CodeAction::QUICKFIX_KIND, CodeAction::REFACTOR_KIND, - CodeAction::INFO_KIND}}} + CodeAction::INFO_KIND, + CodeAction::SOURCE_ORGANIZE_IMPORT}}} : llvm::json::Value(true); @@ -1029,6 +1032,48 @@ } } } + if (KindAllowed(CodeAction::SOURCE_ORGANIZE_IMPORT)) { + std::lock_guard Lock(FixItsMutex); + if (auto FileFixItsIter = FixItsMap.find(File.file()); + FileFixItsIter != FixItsMap.end()) { + auto UnusedDiag = + llvm::find_if(FileFixItsIter->second, [](const auto &DiagFixes) { + return DiagFixes.first.code == "unused-includes"; + }); + auto MissingDiag = + llvm::find_if(FileFixItsIter->second, [](const auto &DiagFixes) { + return DiagFixes.first.code == "missing-include"; + }); + clang::clangd::CodeAction *RemoveAllUnused = nullptr; + clang::clangd::CodeAction *AddAllMissing = nullptr; + clang::clangd::CodeAction *FixAll = nullptr; + if (UnusedDiag != FileFixItsIter->second.end()) { + for (auto &Fix : UnusedDiag->second) { + if (Fix.title == RemoveAllUnusedMessage) + RemoveAllUnused = &Fix; + else if (Fix.title == FixAllIncludesMessage) + FixAll = &Fix; + } + } + if (MissingDiag != FileFixItsIter->second.end()) { + for (auto &Fix : MissingDiag->second) { + if (Fix.title == AddAllMissingIncludesMessage) + AddAllMissing = &Fix; + else if (Fix.title == FixAllIncludesMessage) + FixAll = &Fix; + } + } + auto AddFix = [&](const clang::clangd::CodeAction *Fix) { + if (!Fix) + return; + FixIts.push_back(*Fix); + FixIts.back().kind = CodeAction::SOURCE_ORGANIZE_IMPORT; + }; + AddFix(RemoveAllUnused); + AddFix(AddAllMissing); + AddFix(FixAll); + } + } // Now enumerate the semantic code actions. auto ConsumeActions = diff --git a/clang-tools-extra/clangd/IncludeCleaner.h b/clang-tools-extra/clangd/IncludeCleaner.h --- a/clang-tools-extra/clangd/IncludeCleaner.h +++ b/clang-tools-extra/clangd/IncludeCleaner.h @@ -23,6 +23,7 @@ #include "clang-include-cleaner/Types.h" #include "clang/Tooling/Syntax/Tokens.h" #include "llvm/ADT/DenseSet.h" +#include "llvm/ADT/StringRef.h" #include "llvm/ADT/StringSet.h" #include #include @@ -46,6 +47,11 @@ std::vector UnusedIncludes; std::vector MissingIncludes; }; +inline constexpr llvm::StringLiteral RemoveAllUnusedMessage = + "remove all unused includes"; +inline constexpr llvm::StringLiteral AddAllMissingIncludesMessage = + "add all missing includes"; +inline constexpr llvm::StringLiteral FixAllIncludesMessage = "fix all includes"; /// Retrieves headers that are referenced from the main file but not used. /// In unclear cases, headers are not marked as unused. diff --git a/clang-tools-extra/clangd/IncludeCleaner.cpp b/clang-tools-extra/clangd/IncludeCleaner.cpp --- a/clang-tools-extra/clangd/IncludeCleaner.cpp +++ b/clang-tools-extra/clangd/IncludeCleaner.cpp @@ -440,7 +440,7 @@ assert(!UnusedIncludes.empty()); Fix RemoveAll; - RemoveAll.Message = "remove all unused includes"; + RemoveAll.Message = RemoveAllUnusedMessage; for (const auto &Diag : UnusedIncludes) { assert(Diag.Fixes.size() == 1 && "Expected exactly one fix."); RemoveAll.Edits.insert(RemoveAll.Edits.end(), @@ -465,7 +465,7 @@ assert(!MissingIncludeDiags.empty()); Fix AddAllMissing; - AddAllMissing.Message = "add all missing includes"; + AddAllMissing.Message = AddAllMissingIncludesMessage; // A map to deduplicate the edits with the same new text. // newText (#include "my_missing_header.h") -> TextEdit. llvm::StringMap Edits; @@ -493,7 +493,7 @@ } Fix fixAll(const Fix& RemoveAllUnused, const Fix& AddAllMissing) { Fix FixAll; - FixAll.Message = "fix all includes"; + FixAll.Message = FixAllIncludesMessage; for (const auto &F : RemoveAllUnused.Edits) FixAll.Edits.push_back(F); diff --git a/clang-tools-extra/clangd/Protocol.h b/clang-tools-extra/clangd/Protocol.h --- a/clang-tools-extra/clangd/Protocol.h +++ b/clang-tools-extra/clangd/Protocol.h @@ -1076,6 +1076,7 @@ const static llvm::StringLiteral QUICKFIX_KIND; const static llvm::StringLiteral REFACTOR_KIND; const static llvm::StringLiteral INFO_KIND; + const static llvm::StringLiteral SOURCE_ORGANIZE_IMPORT; /// The diagnostics that this code action resolves. std::optional> diagnostics; diff --git a/clang-tools-extra/clangd/Protocol.cpp b/clang-tools-extra/clangd/Protocol.cpp --- a/clang-tools-extra/clangd/Protocol.cpp +++ b/clang-tools-extra/clangd/Protocol.cpp @@ -867,6 +867,7 @@ const llvm::StringLiteral CodeAction::QUICKFIX_KIND = "quickfix"; const llvm::StringLiteral CodeAction::REFACTOR_KIND = "refactor"; const llvm::StringLiteral CodeAction::INFO_KIND = "info"; +const llvm::StringLiteral CodeAction::SOURCE_ORGANIZE_IMPORT = "source.organizeImports"; llvm::json::Value toJSON(const CodeAction &CA) { auto CodeAction = llvm::json::Object{{"title", CA.title}}; diff --git a/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test b/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test --- a/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test +++ b/clang-tools-extra/clangd/test/include-cleaner-batch-fix.test @@ -112,7 +112,7 @@ # CHECK-NEXT: "version": 0 # CHECK-NEXT: } --- -{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///simple.cpp"},"range":{"start":{"line":2,"character":1},"end":{"line":2,"character":4}},"context":{"diagnostics":[{"range":{"start": {"line": 2, "character": 1}, "end": {"line": 2, "character": 4}},"severity":2,"message":"No header providing \"Foo\" is directly included (fixes available)", "code": "missing-includes", "source": "clangd"}]}}} +{"jsonrpc":"2.0","id":2,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///simple.cpp"},"range":{"start":{"line":2,"character":1},"end":{"line":2,"character":4}},"context":{"only":["quickfix"], "diagnostics":[{"range":{"start": {"line": 2, "character": 1}, "end": {"line": 2, "character": 4}},"severity":2,"message":"No header providing \"Foo\" is directly included (fixes available)", "code": "missing-includes", "source": "clangd"}]}}} # CHECK: "id": 2, # CHECK-NEXT: "jsonrpc": "2.0", # CHECK-NEXT: "result": [ @@ -297,7 +297,7 @@ # CHECK-NEXT: } # CHECK-NEXT: ] --- -{"jsonrpc":"2.0","id":3,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///simple.cpp"},"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":17}},"context":{"diagnostics":[{"range":{"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 17}},"severity":2,"message":"Included header all1.h is not used directly (fixes available)", "code": "unused-includes", "source": "clangd"}]}}} +{"jsonrpc":"2.0","id":3,"method":"textDocument/codeAction","params":{"textDocument":{"uri":"test:///simple.cpp"},"range":{"start":{"line":0,"character":0},"end":{"line":0,"character":17}},"context":{"only":["quickfix"], "diagnostics":[{"range":{"start": {"line": 0, "character": 0}, "end": {"line": 0, "character": 17}},"severity":2,"message":"Included header all1.h is not used directly (fixes available)", "code": "unused-includes", "source": "clangd"}]}}} # CHECK: "id": 3, # CHECK-NEXT: "jsonrpc": "2.0", # CHECK-NEXT: "result": [