//===--- ClangdLSPServer.cpp - LSP server ------------------------*- C++-*-===// // // The LLVM Compiler Infrastructure // // This file is distributed under the University of Illinois Open Source // License. See LICENSE.TXT for details. // //===---------------------------------------------------------------------===// #include "ClangdLSPServer.h" #include "Diagnostics.h" #include "JSONRPCDispatcher.h" #include "SourceCode.h" #include "URI.h" #include "llvm/Support/Errc.h" #include "llvm/Support/FormatVariadic.h" #include "llvm/Support/Path.h" using namespace clang::clangd; using namespace clang; namespace { /// \brief Supports a test URI scheme with relaxed constraints for lit tests. /// The path in a test URI will be combined with a platform-specific fake /// directory to form an absolute path. For example, test:///a.cpp is resolved /// C:\clangd-test\a.cpp on Windows and /clangd-test/a.cpp on Unix. class TestScheme : public URIScheme { public: llvm::Expected getAbsolutePath(llvm::StringRef /*Authority*/, llvm::StringRef Body, llvm::StringRef /*HintPath*/) const override { using namespace llvm::sys; // Still require "/" in body to mimic file scheme, as we want lengths of an // equivalent URI in both schemes to be the same. if (!Body.startswith("/")) return llvm::make_error( "Expect URI body to be an absolute path starting with '/': " + Body, llvm::inconvertibleErrorCode()); Body = Body.ltrim('/'); #ifdef _WIN32 constexpr char TestDir[] = "C:\\clangd-test"; #else constexpr char TestDir[] = "/clangd-test"; #endif llvm::SmallVector Path(Body.begin(), Body.end()); path::native(Path); auto Err = fs::make_absolute(TestDir, Path); if (Err) llvm_unreachable("Failed to make absolute path in test scheme."); return std::string(Path.begin(), Path.end()); } llvm::Expected uriFromAbsolutePath(llvm::StringRef AbsolutePath) const override { llvm_unreachable("Clangd must never create a test URI."); } }; static URISchemeRegistry::Add X("test", "Test scheme for clangd lit tests."); SymbolKindBitset defaultSymbolKinds() { SymbolKindBitset Defaults; for (size_t I = SymbolKindMin; I <= static_cast(SymbolKind::Array); ++I) Defaults.set(I); return Defaults; } } // namespace void ClangdLSPServer::onInitialize(InitializeParams &Params) { if (Params.rootUri && *Params.rootUri) Server.setRootPath(Params.rootUri->file()); else if (Params.rootPath && !Params.rootPath->empty()) Server.setRootPath(*Params.rootPath); CCOpts.EnableSnippets = Params.capabilities.textDocument.completion.completionItem.snippetSupport; if (Params.capabilities.workspace && Params.capabilities.workspace->symbol && Params.capabilities.workspace->symbol->symbolKind) { for (SymbolKind Kind : *Params.capabilities.workspace->symbol->symbolKind->valueSet) { SupportedSymbolKinds.set(static_cast(Kind)); } } reply(json::obj{ {{"capabilities", json::obj{ {"textDocumentSync", (int)TextDocumentSyncKind::Incremental}, {"documentFormattingProvider", true}, {"documentRangeFormattingProvider", true}, {"documentOnTypeFormattingProvider", json::obj{ {"firstTriggerCharacter", "}"}, {"moreTriggerCharacter", {}}, }}, {"codeActionProvider", true}, {"completionProvider", json::obj{ {"resolveProvider", false}, {"triggerCharacters", {".", ">", ":"}}, }}, {"signatureHelpProvider", json::obj{ {"triggerCharacters", {"(", ","}}, }}, {"definitionProvider", true}, {"documentHighlightProvider", true}, {"hoverProvider", true}, {"renameProvider", true}, {"workspaceSymbolProvider", true}, {"executeCommandProvider", json::obj{ {"commands", {ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND}}, }}, }}}}); } void ClangdLSPServer::onShutdown(ShutdownParams &Params) { // Do essentially nothing, just say we're ready to exit. ShutdownRequestReceived = true; reply(nullptr); } void ClangdLSPServer::onExit(ExitParams &Params) { IsDone = true; } void ClangdLSPServer::onDocumentDidOpen(DidOpenTextDocumentParams &Params) { if (Params.metadata && !Params.metadata->extraFlags.empty()) CDB.setExtraFlagsForFile(Params.textDocument.uri.file(), std::move(Params.metadata->extraFlags)); PathRef File = Params.textDocument.uri.file(); std::string &Contents = Params.textDocument.text; DraftMgr.addDraft(File, Contents); Server.addDocument(File, Contents, WantDiagnostics::Yes); } void ClangdLSPServer::onDocumentDidChange(DidChangeTextDocumentParams &Params) { auto WantDiags = WantDiagnostics::Auto; if (Params.wantDiagnostics.hasValue()) WantDiags = Params.wantDiagnostics.getValue() ? WantDiagnostics::Yes : WantDiagnostics::No; PathRef File = Params.textDocument.uri.file(); llvm::Expected Contents = DraftMgr.updateDraft(File, Params.contentChanges); if (!Contents) { // If this fails, we are most likely going to be not in sync anymore with // the client. It is better to remove the draft and let further operations // fail rather than giving wrong results. DraftMgr.removeDraft(File); Server.removeDocument(File); log(llvm::toString(Contents.takeError())); return; } Server.addDocument(File, *Contents, WantDiags); } void ClangdLSPServer::onFileEvent(DidChangeWatchedFilesParams &Params) { Server.onFileEvent(Params); } void ClangdLSPServer::onCommand(ExecuteCommandParams &Params) { auto ApplyEdit = [](WorkspaceEdit WE) { ApplyWorkspaceEditParams Edit; Edit.edit = std::move(WE); // We don't need the response so id == 1 is OK. // Ideally, we would wait for the response and if there is no error, we // would reply success/failure to the original RPC. call("workspace/applyEdit", Edit); }; if (Params.command == ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND && Params.workspaceEdit) { // The flow for "apply-fix" : // 1. We publish a diagnostic, including fixits // 2. The user clicks on the diagnostic, the editor asks us for code actions // 3. We send code actions, with the fixit embedded as context // 4. The user selects the fixit, the editor asks us to apply it // 5. We unwrap the changes and send them back to the editor // 6. The editor applies the changes (applyEdit), and sends us a reply (but // we ignore it) reply("Fix applied."); ApplyEdit(*Params.workspaceEdit); } 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 // more commands are added, this will be here has a safe guard. replyError( ErrorCode::InvalidParams, llvm::formatv("Unsupported command \"{0}\".", Params.command).str()); } } void ClangdLSPServer::onWorkspaceSymbol(WorkspaceSymbolParams &Params) { Server.workspaceSymbols( Params.query, CCOpts.Limit, [this](llvm::Expected> Items) { if (!Items) return replyError(ErrorCode::InternalError, llvm::toString(Items.takeError())); for (auto &Sym : *Items) Sym.kind = adjustKindToCapability(Sym.kind, SupportedSymbolKinds); reply(json::ary(*Items)); }); } void ClangdLSPServer::onRename(RenameParams &Params) { Path File = Params.textDocument.uri.file(); llvm::Optional Code = DraftMgr.getDraft(File); if (!Code) return replyError(ErrorCode::InvalidParams, "onRename called for non-added file"); Server.rename( File, Params.position, Params.newName, [File, Code, Params](llvm::Expected> Replacements) { if (!Replacements) return replyError(ErrorCode::InternalError, llvm::toString(Replacements.takeError())); // Turn the replacements into the format specified by the Language // Server Protocol. Fuse them into one big JSON array. std::vector Edits; for (const auto &R : *Replacements) Edits.push_back(replacementToEdit(*Code, R)); WorkspaceEdit WE; WE.changes = {{Params.textDocument.uri.uri(), Edits}}; reply(WE); }); } void ClangdLSPServer::onDocumentDidClose(DidCloseTextDocumentParams &Params) { PathRef File = Params.textDocument.uri.file(); DraftMgr.removeDraft(File); Server.removeDocument(File); } void ClangdLSPServer::onDocumentOnTypeFormatting( DocumentOnTypeFormattingParams &Params) { auto File = Params.textDocument.uri.file(); auto Code = DraftMgr.getDraft(File); if (!Code) return replyError(ErrorCode::InvalidParams, "onDocumentOnTypeFormatting called for non-added file"); auto ReplacementsOrError = Server.formatOnType(*Code, File, Params.position); if (ReplacementsOrError) reply(json::ary(replacementsToEdits(*Code, ReplacementsOrError.get()))); else replyError(ErrorCode::UnknownErrorCode, llvm::toString(ReplacementsOrError.takeError())); } void ClangdLSPServer::onDocumentRangeFormatting( DocumentRangeFormattingParams &Params) { auto File = Params.textDocument.uri.file(); auto Code = DraftMgr.getDraft(File); if (!Code) return replyError(ErrorCode::InvalidParams, "onDocumentRangeFormatting called for non-added file"); auto ReplacementsOrError = Server.formatRange(*Code, File, Params.range); if (ReplacementsOrError) reply(json::ary(replacementsToEdits(*Code, ReplacementsOrError.get()))); else replyError(ErrorCode::UnknownErrorCode, llvm::toString(ReplacementsOrError.takeError())); } void ClangdLSPServer::onDocumentFormatting(DocumentFormattingParams &Params) { auto File = Params.textDocument.uri.file(); auto Code = DraftMgr.getDraft(File); if (!Code) return replyError(ErrorCode::InvalidParams, "onDocumentFormatting called for non-added file"); auto ReplacementsOrError = Server.formatFile(*Code, File); if (ReplacementsOrError) reply(json::ary(replacementsToEdits(*Code, ReplacementsOrError.get()))); else replyError(ErrorCode::UnknownErrorCode, llvm::toString(ReplacementsOrError.takeError())); } void ClangdLSPServer::onCodeAction(CodeActionParams &Params) { // We provide a code action for each diagnostic at the requested location // which has FixIts available. auto Code = DraftMgr.getDraft(Params.textDocument.uri.file()); if (!Code) return replyError(ErrorCode::InvalidParams, "onCodeAction called for non-added file"); json::ary Commands; for (Diagnostic &D : Params.context.diagnostics) { for (auto &F : getFixes(Params.textDocument.uri.file(), D)) { WorkspaceEdit WE; std::vector Edits(F.Edits.begin(), F.Edits.end()); WE.changes = {{Params.textDocument.uri.uri(), std::move(Edits)}}; Commands.push_back(json::obj{ {"title", llvm::formatv("Apply fix: {0}", F.Message)}, {"command", ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND}, {"arguments", {WE}}, }); } } reply(std::move(Commands)); } void ClangdLSPServer::onCompletion(TextDocumentPositionParams &Params) { Server.codeComplete(Params.textDocument.uri.file(), Params.position, CCOpts, [](llvm::Expected List) { if (!List) return replyError(ErrorCode::InvalidParams, llvm::toString(List.takeError())); reply(*List); }); } void ClangdLSPServer::onSignatureHelp(TextDocumentPositionParams &Params) { Server.signatureHelp(Params.textDocument.uri.file(), Params.position, [](llvm::Expected SignatureHelp) { if (!SignatureHelp) return replyError( ErrorCode::InvalidParams, llvm::toString(SignatureHelp.takeError())); reply(*SignatureHelp); }); } void ClangdLSPServer::onGoToDefinition(TextDocumentPositionParams &Params) { Server.findDefinitions( Params.textDocument.uri.file(), Params.position, [](llvm::Expected> Items) { if (!Items) return replyError(ErrorCode::InvalidParams, llvm::toString(Items.takeError())); reply(json::ary(*Items)); }); } void ClangdLSPServer::onSwitchSourceHeader(TextDocumentIdentifier &Params) { llvm::Optional Result = Server.switchSourceHeader(Params.uri.file()); reply(Result ? URI::createFile(*Result).toString() : ""); } void ClangdLSPServer::onDocumentHighlight(TextDocumentPositionParams &Params) { Server.findDocumentHighlights( Params.textDocument.uri.file(), Params.position, [](llvm::Expected> Highlights) { if (!Highlights) return replyError(ErrorCode::InternalError, llvm::toString(Highlights.takeError())); reply(json::ary(*Highlights)); }); } void ClangdLSPServer::onHover(TextDocumentPositionParams &Params) { Server.findHover(Params.textDocument.uri.file(), Params.position, [](llvm::Expected H) { if (!H) { replyError(ErrorCode::InternalError, llvm::toString(H.takeError())); return; } reply(*H); }); } // FIXME: This function needs to be properly tested. void ClangdLSPServer::onChangeConfiguration( DidChangeConfigurationParams &Params) { ClangdConfigurationParamsChange &Settings = Params.settings; // Compilation database change. if (Settings.compilationDatabasePath.hasValue()) { CDB.setCompileCommandsDir(Settings.compilationDatabasePath.getValue()); reparseOpenedFiles(); } } ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, const clangd::CodeCompleteOptions &CCOpts, llvm::Optional CompileCommandsDir, const ClangdServer::Options &Opts) : Out(Out), CDB(std::move(CompileCommandsDir)), CCOpts(CCOpts), SupportedSymbolKinds(defaultSymbolKinds()), Server(CDB, FSProvider, /*DiagConsumer=*/*this, Opts) {} bool ClangdLSPServer::run(std::istream &In, JSONStreamStyle InputStyle) { assert(!IsDone && "Run was called before"); // Set up JSONRPCDispatcher. JSONRPCDispatcher Dispatcher([](const json::Expr &Params) { replyError(ErrorCode::MethodNotFound, "method not found"); }); registerCallbackHandlers(Dispatcher, /*Callbacks=*/*this); // Run the Language Server loop. runLanguageServerLoop(In, Out, InputStyle, Dispatcher, IsDone); // Make sure IsDone is set to true after this method exits to ensure assertion // at the start of the method fires if it's ever executed again. IsDone = true; return ShutdownRequestReceived; } std::vector ClangdLSPServer::getFixes(StringRef File, const clangd::Diagnostic &D) { std::lock_guard Lock(FixItsMutex); auto DiagToFixItsIter = FixItsMap.find(File); if (DiagToFixItsIter == FixItsMap.end()) return {}; const auto &DiagToFixItsMap = DiagToFixItsIter->second; auto FixItsIter = DiagToFixItsMap.find(D); if (FixItsIter == DiagToFixItsMap.end()) return {}; return FixItsIter->second; } void ClangdLSPServer::onDiagnosticsReady(PathRef File, std::vector Diagnostics) { json::ary DiagnosticsJSON; DiagnosticToReplacementMap LocalFixIts; // Temporary storage for (auto &Diag : Diagnostics) { toLSPDiags(Diag, [&](clangd::Diagnostic Diag, llvm::ArrayRef Fixes) { DiagnosticsJSON.push_back(json::obj{ {"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 { // FIXME(ibiryukov): should be deleted when documents are removed std::lock_guard Lock(FixItsMutex); FixItsMap[File] = LocalFixIts; } // Publish diagnostics. Out.writeMessage(json::obj{ {"jsonrpc", "2.0"}, {"method", "textDocument/publishDiagnostics"}, {"params", json::obj{ {"uri", URIForFile{File}}, {"diagnostics", std::move(DiagnosticsJSON)}, }}, }); } void ClangdLSPServer::reparseOpenedFiles() { for (const Path &FilePath : DraftMgr.getActiveFiles()) Server.addDocument(FilePath, *DraftMgr.getDraft(FilePath), WantDiagnostics::Auto, /*SkipCache=*/true); }