| //===--- 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; | 
 | using namespace llvm; | 
 |  | 
 | 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<std::string> | 
 |   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<llvm::StringError>( | 
 |           "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<char, 16> 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<URI> | 
 |   uriFromAbsolutePath(llvm::StringRef AbsolutePath) const override { | 
 |     llvm_unreachable("Clangd must never create a test URI."); | 
 |   } | 
 | }; | 
 |  | 
 | static URISchemeRegistry::Add<TestScheme> | 
 |     X("test", "Test scheme for clangd lit tests."); | 
 |  | 
 | SymbolKindBitset defaultSymbolKinds() { | 
 |   SymbolKindBitset Defaults; | 
 |   for (size_t I = SymbolKindMin; I <= static_cast<size_t>(SymbolKind::Array); | 
 |        ++I) | 
 |     Defaults.set(I); | 
 |   return Defaults; | 
 | } | 
 |  | 
 | } // namespace | 
 |  | 
 | void ClangdLSPServer::onInitialize(InitializeParams &Params) { | 
 |   if (Params.initializationOptions) | 
 |     applyConfiguration(*Params.initializationOptions); | 
 |  | 
 |   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<size_t>(Kind)); | 
 |     } | 
 |   } | 
 |  | 
 |   reply(json::Object{ | 
 |       {{"capabilities", | 
 |         json::Object{ | 
 |             {"textDocumentSync", (int)TextDocumentSyncKind::Incremental}, | 
 |             {"documentFormattingProvider", true}, | 
 |             {"documentRangeFormattingProvider", true}, | 
 |             {"documentOnTypeFormattingProvider", | 
 |              json::Object{ | 
 |                  {"firstTriggerCharacter", "}"}, | 
 |                  {"moreTriggerCharacter", {}}, | 
 |              }}, | 
 |             {"codeActionProvider", true}, | 
 |             {"completionProvider", | 
 |              json::Object{ | 
 |                  {"resolveProvider", false}, | 
 |                  {"triggerCharacters", {".", ">", ":"}}, | 
 |              }}, | 
 |             {"signatureHelpProvider", | 
 |              json::Object{ | 
 |                  {"triggerCharacters", {"(", ","}}, | 
 |              }}, | 
 |             {"definitionProvider", true}, | 
 |             {"documentHighlightProvider", true}, | 
 |             {"hoverProvider", true}, | 
 |             {"renameProvider", true}, | 
 |             {"documentSymbolProvider", true}, | 
 |             {"workspaceSymbolProvider", true}, | 
 |             {"executeCommandProvider", | 
 |              json::Object{ | 
 |                  {"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) { | 
 |   PathRef File = Params.textDocument.uri.file(); | 
 |   if (Params.metadata && !Params.metadata->extraFlags.empty()) { | 
 |     NonCachedCDB.setExtraFlagsForFile(File, | 
 |                                       std::move(Params.metadata->extraFlags)); | 
 |     CDB.invalidate(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<std::string> 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); | 
 |     CDB.invalidate(File); | 
 |     elog("Failed to update {0}: {1}", File, 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<std::vector<SymbolInformation>> Items) { | 
 |         if (!Items) | 
 |           return replyError(ErrorCode::InternalError, | 
 |                             llvm::toString(Items.takeError())); | 
 |         for (auto &Sym : *Items) | 
 |           Sym.kind = adjustKindToCapability(Sym.kind, SupportedSymbolKinds); | 
 |  | 
 |         reply(json::Array(*Items)); | 
 |       }); | 
 | } | 
 |  | 
 | void ClangdLSPServer::onRename(RenameParams &Params) { | 
 |   Path File = Params.textDocument.uri.file(); | 
 |   llvm::Optional<std::string> 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<std::vector<tooling::Replacement>> 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<TextEdit> 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::Array(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::Array(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::Array(replacementsToEdits(*Code, ReplacementsOrError.get()))); | 
 |   else | 
 |     replyError(ErrorCode::UnknownErrorCode, | 
 |                llvm::toString(ReplacementsOrError.takeError())); | 
 | } | 
 |  | 
 | void ClangdLSPServer::onDocumentSymbol(DocumentSymbolParams &Params) { | 
 |   Server.documentSymbols( | 
 |       Params.textDocument.uri.file(), | 
 |       [this](llvm::Expected<std::vector<SymbolInformation>> Items) { | 
 |         if (!Items) | 
 |           return replyError(ErrorCode::InvalidParams, | 
 |                             llvm::toString(Items.takeError())); | 
 |         for (auto &Sym : *Items) | 
 |           Sym.kind = adjustKindToCapability(Sym.kind, SupportedSymbolKinds); | 
 |         reply(json::Array(*Items)); | 
 |       }); | 
 | } | 
 |  | 
 | 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::Array Commands; | 
 |   for (Diagnostic &D : Params.context.diagnostics) { | 
 |     for (auto &F : getFixes(Params.textDocument.uri.file(), D)) { | 
 |       WorkspaceEdit WE; | 
 |       std::vector<TextEdit> Edits(F.Edits.begin(), F.Edits.end()); | 
 |       WE.changes = {{Params.textDocument.uri.uri(), std::move(Edits)}}; | 
 |       Commands.push_back(json::Object{ | 
 |           {"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, | 
 |                       [this](llvm::Expected<CodeCompleteResult> List) { | 
 |                         if (!List) | 
 |                           return replyError(ErrorCode::InvalidParams, | 
 |                                             llvm::toString(List.takeError())); | 
 |                         CompletionList LSPList; | 
 |                         LSPList.isIncomplete = List->HasMore; | 
 |                         for (const auto &R : List->Completions) | 
 |                           LSPList.items.push_back(R.render(CCOpts)); | 
 |                         reply(std::move(LSPList)); | 
 |                       }); | 
 | } | 
 |  | 
 | void ClangdLSPServer::onSignatureHelp(TextDocumentPositionParams &Params) { | 
 |   Server.signatureHelp(Params.textDocument.uri.file(), Params.position, | 
 |                        [](llvm::Expected<SignatureHelp> 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<std::vector<Location>> Items) { | 
 |         if (!Items) | 
 |           return replyError(ErrorCode::InvalidParams, | 
 |                             llvm::toString(Items.takeError())); | 
 |         reply(json::Array(*Items)); | 
 |       }); | 
 | } | 
 |  | 
 | void ClangdLSPServer::onSwitchSourceHeader(TextDocumentIdentifier &Params) { | 
 |   llvm::Optional<Path> 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<std::vector<DocumentHighlight>> Highlights) { | 
 |         if (!Highlights) | 
 |           return replyError(ErrorCode::InternalError, | 
 |                             llvm::toString(Highlights.takeError())); | 
 |         reply(json::Array(*Highlights)); | 
 |       }); | 
 | } | 
 |  | 
 | void ClangdLSPServer::onHover(TextDocumentPositionParams &Params) { | 
 |   Server.findHover(Params.textDocument.uri.file(), Params.position, | 
 |                    [](llvm::Expected<llvm::Optional<Hover>> H) { | 
 |                      if (!H) { | 
 |                        replyError(ErrorCode::InternalError, | 
 |                                   llvm::toString(H.takeError())); | 
 |                        return; | 
 |                      } | 
 |  | 
 |                      reply(*H); | 
 |                    }); | 
 | } | 
 |  | 
 | void ClangdLSPServer::applyConfiguration( | 
 |     const ClangdConfigurationParamsChange &Settings) { | 
 |   // Compilation database change. | 
 |   if (Settings.compilationDatabasePath.hasValue()) { | 
 |     NonCachedCDB.setCompileCommandsDir( | 
 |         Settings.compilationDatabasePath.getValue()); | 
 |     CDB.clear(); | 
 |  | 
 |     reparseOpenedFiles(); | 
 |   } | 
 | } | 
 |  | 
 | // FIXME: This function needs to be properly tested. | 
 | void ClangdLSPServer::onChangeConfiguration( | 
 |     DidChangeConfigurationParams &Params) { | 
 |   applyConfiguration(Params.settings); | 
 | } | 
 |  | 
 | ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, | 
 |                                  const clangd::CodeCompleteOptions &CCOpts, | 
 |                                  llvm::Optional<Path> CompileCommandsDir, | 
 |                                  const ClangdServer::Options &Opts) | 
 |     : Out(Out), NonCachedCDB(std::move(CompileCommandsDir)), CDB(NonCachedCDB), | 
 |       CCOpts(CCOpts), SupportedSymbolKinds(defaultSymbolKinds()), | 
 |       Server(CDB, FSProvider, /*DiagConsumer=*/*this, Opts) {} | 
 |  | 
 | bool ClangdLSPServer::run(std::FILE *In, JSONStreamStyle InputStyle) { | 
 |   assert(!IsDone && "Run was called before"); | 
 |  | 
 |   // Set up JSONRPCDispatcher. | 
 |   JSONRPCDispatcher Dispatcher([](const json::Value &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<Fix> ClangdLSPServer::getFixes(StringRef File, | 
 |                                            const clangd::Diagnostic &D) { | 
 |   std::lock_guard<std::mutex> 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<Diag> Diagnostics) { | 
 |   json::Array DiagnosticsJSON; | 
 |  | 
 |   DiagnosticToReplacementMap LocalFixIts; // Temporary storage | 
 |   for (auto &Diag : Diagnostics) { | 
 |     toLSPDiags(Diag, [&](clangd::Diagnostic Diag, llvm::ArrayRef<Fix> 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 | 
 |   { | 
 |     // FIXME(ibiryukov): should be deleted when documents are removed | 
 |     std::lock_guard<std::mutex> Lock(FixItsMutex); | 
 |     FixItsMap[File] = LocalFixIts; | 
 |   } | 
 |  | 
 |   // Publish diagnostics. | 
 |   Out.writeMessage(json::Object{ | 
 |       {"jsonrpc", "2.0"}, | 
 |       {"method", "textDocument/publishDiagnostics"}, | 
 |       {"params", | 
 |        json::Object{ | 
 |            {"uri", URIForFile{File}}, | 
 |            {"diagnostics", std::move(DiagnosticsJSON)}, | 
 |        }}, | 
 |   }); | 
 | } | 
 |  | 
 | void ClangdLSPServer::reparseOpenedFiles() { | 
 |   for (const Path &FilePath : DraftMgr.getActiveFiles()) | 
 |     Server.addDocument(FilePath, *DraftMgr.getDraft(FilePath), | 
 |                        WantDiagnostics::Auto); | 
 | } |