| //===--- 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); |
| } |