Index: clangd/ClangdLSPServer.h =================================================================== --- clangd/ClangdLSPServer.h +++ clangd/ClangdLSPServer.h @@ -34,6 +34,7 @@ /// loaded only from \p CompileCommandsDir. Otherwise, clangd will look /// for compile_commands.json in all parent directories of each file. ClangdLSPServer(JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts, + const clangd::DiagnosticOptions &DiagOpts, llvm::Optional CompileCommandsDir, bool ShouldUseInMemoryCDB, const ClangdServer::Options &Opts); @@ -155,6 +156,8 @@ RealFileSystemProvider FSProvider; /// Options used for code completion clangd::CodeCompleteOptions CCOpts; + /// Options used for diagnostics. + clangd::DiagnosticOptions DiagOpts; /// The supported kinds of the client. SymbolKindBitset SupportedSymbolKinds; Index: clangd/ClangdLSPServer.cpp =================================================================== --- clangd/ClangdLSPServer.cpp +++ clangd/ClangdLSPServer.cpp @@ -436,13 +436,15 @@ ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts, + const clangd::DiagnosticOptions &DiagOpts, llvm::Optional CompileCommandsDir, bool ShouldUseInMemoryCDB, const ClangdServer::Options &Opts) : Out(Out), CDB(ShouldUseInMemoryCDB ? CompilationDB::makeInMemory() : CompilationDB::makeDirectoryBased( std::move(CompileCommandsDir))), - CCOpts(CCOpts), SupportedSymbolKinds(defaultSymbolKinds()), + CCOpts(CCOpts), DiagOpts(DiagOpts), + SupportedSymbolKinds(defaultSymbolKinds()), Server(CDB.getCDB(), FSProvider, /*DiagConsumer=*/*this, Opts) {} bool ClangdLSPServer::run(std::FILE *In, JSONStreamStyle InputStyle) { @@ -485,17 +487,18 @@ DiagnosticToReplacementMap LocalFixIts; // Temporary storage for (auto &Diag : Diagnostics) { - toLSPDiags(Diag, [&](clangd::Diagnostic Diag, llvm::ArrayRef Fixes) { - DiagnosticsJSON.push_back(json::Object{ - {"range", Diag.range}, - {"severity", Diag.severity}, - {"message", Diag.message}, - }); - - auto &FixItsForDiagnostic = LocalFixIts[Diag]; - std::copy(Fixes.begin(), Fixes.end(), - std::back_inserter(FixItsForDiagnostic)); - }); + toLSPDiags(Diag, DiagOpts, + [&](clangd::Diagnostic Diag, llvm::ArrayRef Fixes) { + DiagnosticsJSON.push_back(json::Object{ + {"range", Diag.range}, + {"severity", Diag.severity}, + {"message", Diag.message}, + }); + + auto &FixItsForDiagnostic = LocalFixIts[Diag]; + std::copy(Fixes.begin(), Fixes.end(), + std::back_inserter(FixItsForDiagnostic)); + }); } // Cache FixIts Index: clangd/ClangdUnit.cpp =================================================================== --- clangd/ClangdUnit.cpp +++ clangd/ClangdUnit.cpp @@ -279,7 +279,7 @@ // reporting them. IgnoreDiagnostics IgnoreDiagnostics; IntrusiveRefCntPtr CommandLineDiagsEngine = - CompilerInstance::createDiagnostics(new DiagnosticOptions, + CompilerInstance::createDiagnostics(new clang::DiagnosticOptions, &IgnoreDiagnostics, false); std::unique_ptr CI = createInvocationFromCommandLine( ArgStrs, CommandLineDiagsEngine, Inputs.FS); Index: clangd/Diagnostics.h =================================================================== --- clangd/Diagnostics.h +++ clangd/Diagnostics.h @@ -23,6 +23,11 @@ namespace clang { namespace clangd { +struct DiagnosticOptions { + /// Capitalize the first character in the diagnostic's message. + bool CapitalizeDiagnosticText = false; +}; + /// Contains basic information about a diagnostic. struct DiagBase { std::string Message; @@ -65,7 +70,7 @@ /// file do not have a corresponding LSP diagnostic, but can still be included /// as part of their main diagnostic's message. void toLSPDiags( - const Diag &D, + const Diag &D, const DiagnosticOptions &DiagOpts, llvm::function_ref)> OutFn); /// Convert from clang diagnostic level to LSP severity. Index: clangd/Diagnostics.cpp =================================================================== --- clangd/Diagnostics.cpp +++ clangd/Diagnostics.cpp @@ -145,6 +145,14 @@ OS << diagLeveltoString(D.Severity) << ": " << D.Message; } +/// Applies common text transformations to a diagnostic's string. +std::string capitalizeIfNeeded(std::string Message, + const DiagnosticOptions &DiagOpts) { + if (DiagOpts.CapitalizeDiagnosticText && !Message.empty()) + Message[0] = llvm::toUpper(Message[0]); + return Message; +} + /// Returns a message sent to LSP for the main diagnostic in \p D. /// The message includes all the notes with their corresponding locations. /// However, notes with fix-its are excluded as those usually only contain a @@ -157,7 +165,7 @@ /// /// dir1/dir2/dir3/../../dir4/header.h:12:23 /// note: candidate function not viable: requires 3 arguments -std::string mainMessage(const Diag &D) { +std::string mainMessage(const Diag &D, const DiagnosticOptions &DiagOpts) { std::string Result; llvm::raw_string_ostream OS(Result); OS << D.Message; @@ -166,20 +174,21 @@ printDiag(OS, Note); } OS.flush(); - return Result; + return capitalizeIfNeeded(std::move(Result), DiagOpts); } /// Returns a message sent to LSP for the note of the main diagnostic. /// The message includes the main diagnostic to provide the necessary context /// for the user to understand the note. -std::string noteMessage(const Diag &Main, const DiagBase &Note) { +std::string noteMessage(const Diag &Main, const DiagBase &Note, + const DiagnosticOptions &DiagOpts) { std::string Result; llvm::raw_string_ostream OS(Result); OS << Note.Message; OS << "\n\n"; printDiag(OS, Main); OS.flush(); - return Result; + return capitalizeIfNeeded(std::move(Result), DiagOpts); } } // namespace @@ -222,7 +231,7 @@ } void toLSPDiags( - const Diag &D, + const Diag &D, const DiagnosticOptions &DiagOpts, llvm::function_ref)> OutFn) { auto FillBasicFields = [](const DiagBase &D) -> clangd::Diagnostic { clangd::Diagnostic Res; @@ -233,7 +242,7 @@ { clangd::Diagnostic Main = FillBasicFields(D); - Main.message = mainMessage(D); + Main.message = mainMessage(D, DiagOpts); OutFn(std::move(Main), D.Fixes); } @@ -241,7 +250,7 @@ if (!Note.InsideMainFile) continue; clangd::Diagnostic Res = FillBasicFields(Note); - Res.message = noteMessage(D, Note); + Res.message = noteMessage(D, Note, DiagOpts); OutFn(std::move(Res), llvm::ArrayRef()); } } Index: clangd/tool/ClangdMain.cpp =================================================================== --- clangd/tool/ClangdMain.cpp +++ clangd/tool/ClangdMain.cpp @@ -185,6 +185,12 @@ "'compile_commands.json' files")), llvm::cl::init(FilesystemCompileArgs), llvm::cl::Hidden); +static llvm::cl::opt CapitalizeDiagnosticText( + "capitialize-diagnostic-text", + llvm::cl::desc( + "Capitalize the first character in the diagnostic's message"), + llvm::cl::init(false)); + int main(int argc, char *argv[]) { llvm::sys::PrintStackTraceOnErrorSignal(argv[0]); llvm::cl::SetVersionPrinter([](llvm::raw_ostream &OS) { @@ -300,9 +306,12 @@ CCOpts.IncludeIndicator.NoInsert.clear(); } + clangd::DiagnosticOptions DiagOpts; + DiagOpts.CapitalizeDiagnosticText = CapitalizeDiagnosticText; + // Initialize and run ClangdLSPServer. ClangdLSPServer LSPServer( - Out, CCOpts, CompileCommandsDirPath, + Out, CCOpts, DiagOpts, CompileCommandsDirPath, /*ShouldUseInMemoryCDB=*/CompileArgsFrom == LSPCompileArgs, Opts); constexpr int NoShutdownRequestErrorCode = 1; llvm::set_thread_name("clangd.main"); Index: test/clangd/capitalize-diagnostics.test =================================================================== --- /dev/null +++ test/clangd/capitalize-diagnostics.test @@ -0,0 +1,42 @@ +# RUN: clangd -capitialize-diagnostic-text -lit-test < %s | FileCheck -strict-whitespace %s +{"jsonrpc":"2.0","id":0,"method":"initialize","params":{"processId":123,"rootPath":"clangd","capabilities":{},"trace":"off"}} +--- +{"jsonrpc":"2.0","method":"textDocument/didOpen","params":{"textDocument":{"uri":"test:///foo.c","languageId":"c","version":1,"text":"void foo() { }\nvoid foo() { }"}}} +# CHECK: "method": "textDocument/publishDiagnostics", +# CHECK-NEXT: "params": { +# CHECK-NEXT: "diagnostics": [ +# CHECK-NEXT: { +# CHECK-NEXT: "message": "Redefinition of 'foo'\n\nfoo.c:1:6: note: previous definition is here", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 8, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 5, +# CHECK-NEXT: "line": 1 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 1 +# CHECK-NEXT: }, +# CHECK-NEXT: { +# CHECK-NEXT: "message": "Previous definition is here\n\nfoo.c:2:6: error: redefinition of 'foo'", +# CHECK-NEXT: "range": { +# CHECK-NEXT: "end": { +# CHECK-NEXT: "character": 8, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: }, +# CHECK-NEXT: "start": { +# CHECK-NEXT: "character": 5, +# CHECK-NEXT: "line": 0 +# CHECK-NEXT: } +# CHECK-NEXT: }, +# CHECK-NEXT: "severity": 3 +# CHECK-NEXT: } +# CHECK-NEXT: ], +# CHECK-NEXT: "uri": "file://{{.*}}/foo.c" +# CHECK-NEXT: } +--- +{"jsonrpc":"2.0","id":5,"method":"shutdown"} +--- +{"jsonrpc":"2.0","method":"exit"}