1 //===--- ClangdLSPServer.cpp - LSP server ------------------------*- C++-*-===//
2 //
3 //                     The LLVM Compiler Infrastructure
4 //
5 // This file is distributed under the University of Illinois Open Source
6 // License. See LICENSE.TXT for details.
7 //
8 //===---------------------------------------------------------------------===//
9 
10 #include "ClangdLSPServer.h"
11 #include "JSONRPCDispatcher.h"
12 #include "SourceCode.h"
13 #include "URI.h"
14 #include "llvm/Support/FormatVariadic.h"
15 #include "llvm/Support/Path.h"
16 
17 using namespace clang::clangd;
18 using namespace clang;
19 
20 namespace {
21 
22 /// \brief Supports a test URI scheme with relaxed constraints for lit tests.
23 /// The path in a test URI will be combined with a platform-specific fake
24 /// directory to form an absolute path. For example, test:///a.cpp is resolved
25 /// C:\clangd-test\a.cpp on Windows and /clangd-test/a.cpp on Unix.
26 class TestScheme : public URIScheme {
27 public:
28   llvm::Expected<std::string>
29   getAbsolutePath(llvm::StringRef /*Authority*/, llvm::StringRef Body,
30                   llvm::StringRef /*HintPath*/) const override {
31     using namespace llvm::sys;
32     // Still require "/" in body to mimic file scheme, as we want lengths of an
33     // equivalent URI in both schemes to be the same.
34     if (!Body.startswith("/"))
35       return llvm::make_error<llvm::StringError>(
36           "Expect URI body to be an absolute path starting with '/': " + Body,
37           llvm::inconvertibleErrorCode());
38     Body = Body.ltrim('/');
39 #ifdef LLVM_ON_WIN32
40     constexpr char TestDir[] = "C:\\clangd-test";
41 #else
42     constexpr char TestDir[] = "/clangd-test";
43 #endif
44     llvm::SmallVector<char, 16> Path(Body.begin(), Body.end());
45     path::native(Path);
46     auto Err = fs::make_absolute(TestDir, Path);
47     if (Err)
48       llvm_unreachable("Failed to make absolute path in test scheme.");
49     return std::string(Path.begin(), Path.end());
50   }
51 
52   llvm::Expected<URI>
53   uriFromAbsolutePath(llvm::StringRef AbsolutePath) const override {
54     llvm_unreachable("Clangd must never create a test URI.");
55   }
56 };
57 
58 static URISchemeRegistry::Add<TestScheme>
59     X("test", "Test scheme for clangd lit tests.");
60 
61 TextEdit replacementToEdit(StringRef Code, const tooling::Replacement &R) {
62   Range ReplacementRange = {
63       offsetToPosition(Code, R.getOffset()),
64       offsetToPosition(Code, R.getOffset() + R.getLength())};
65   return {ReplacementRange, R.getReplacementText()};
66 }
67 
68 std::vector<TextEdit>
69 replacementsToEdits(StringRef Code,
70                     const std::vector<tooling::Replacement> &Replacements) {
71   // Turn the replacements into the format specified by the Language Server
72   // Protocol. Fuse them into one big JSON array.
73   std::vector<TextEdit> Edits;
74   for (const auto &R : Replacements)
75     Edits.push_back(replacementToEdit(Code, R));
76   return Edits;
77 }
78 
79 std::vector<TextEdit> replacementsToEdits(StringRef Code,
80                                           const tooling::Replacements &Repls) {
81   std::vector<TextEdit> Edits;
82   for (const auto &R : Repls)
83     Edits.push_back(replacementToEdit(Code, R));
84   return Edits;
85 }
86 
87 } // namespace
88 
89 void ClangdLSPServer::onInitialize(InitializeParams &Params) {
90   if (Params.rootUri && *Params.rootUri)
91     Server.setRootPath(Params.rootUri->file());
92   else if (Params.rootPath && !Params.rootPath->empty())
93     Server.setRootPath(*Params.rootPath);
94 
95   CCOpts.EnableSnippets =
96       Params.capabilities.textDocument.completion.completionItem.snippetSupport;
97 
98   reply(json::obj{
99       {{"capabilities",
100         json::obj{
101             {"textDocumentSync", 1},
102             {"documentFormattingProvider", true},
103             {"documentRangeFormattingProvider", true},
104             {"documentOnTypeFormattingProvider",
105              json::obj{
106                  {"firstTriggerCharacter", "}"},
107                  {"moreTriggerCharacter", {}},
108              }},
109             {"codeActionProvider", true},
110             {"completionProvider",
111              json::obj{
112                  {"resolveProvider", false},
113                  {"triggerCharacters", {".", ">", ":"}},
114              }},
115             {"signatureHelpProvider",
116              json::obj{
117                  {"triggerCharacters", {"(", ","}},
118              }},
119             {"definitionProvider", true},
120             {"documentHighlightProvider", true},
121             {"hoverProvider", true},
122             {"renameProvider", true},
123             {"executeCommandProvider",
124              json::obj{
125                  {"commands",
126                   {ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND,
127                    ExecuteCommandParams::CLANGD_INSERT_HEADER_INCLUDE}},
128              }},
129         }}}});
130 }
131 
132 void ClangdLSPServer::onShutdown(ShutdownParams &Params) {
133   // Do essentially nothing, just say we're ready to exit.
134   ShutdownRequestReceived = true;
135   reply(nullptr);
136 }
137 
138 void ClangdLSPServer::onExit(ExitParams &Params) { IsDone = true; }
139 
140 void ClangdLSPServer::onDocumentDidOpen(DidOpenTextDocumentParams &Params) {
141   if (Params.metadata && !Params.metadata->extraFlags.empty())
142     CDB.setExtraFlagsForFile(Params.textDocument.uri.file(),
143                              std::move(Params.metadata->extraFlags));
144   Server.addDocument(Params.textDocument.uri.file(), Params.textDocument.text,
145                      WantDiagnostics::Yes);
146 }
147 
148 void ClangdLSPServer::onDocumentDidChange(DidChangeTextDocumentParams &Params) {
149   if (Params.contentChanges.size() != 1)
150     return replyError(ErrorCode::InvalidParams,
151                       "can only apply one change at a time");
152   auto WantDiags = WantDiagnostics::Auto;
153   if (Params.wantDiagnostics.hasValue())
154     WantDiags = Params.wantDiagnostics.getValue() ? WantDiagnostics::Yes
155                                                   : WantDiagnostics::No;
156   // We only support full syncing right now.
157   Server.addDocument(Params.textDocument.uri.file(),
158                      Params.contentChanges[0].text, WantDiags);
159 }
160 
161 void ClangdLSPServer::onFileEvent(DidChangeWatchedFilesParams &Params) {
162   Server.onFileEvent(Params);
163 }
164 
165 void ClangdLSPServer::onCommand(ExecuteCommandParams &Params) {
166   auto ApplyEdit = [](WorkspaceEdit WE) {
167     ApplyWorkspaceEditParams Edit;
168     Edit.edit = std::move(WE);
169     // We don't need the response so id == 1 is OK.
170     // Ideally, we would wait for the response and if there is no error, we
171     // would reply success/failure to the original RPC.
172     call("workspace/applyEdit", Edit);
173   };
174   if (Params.command == ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND &&
175       Params.workspaceEdit) {
176     // The flow for "apply-fix" :
177     // 1. We publish a diagnostic, including fixits
178     // 2. The user clicks on the diagnostic, the editor asks us for code actions
179     // 3. We send code actions, with the fixit embedded as context
180     // 4. The user selects the fixit, the editor asks us to apply it
181     // 5. We unwrap the changes and send them back to the editor
182     // 6. The editor applies the changes (applyEdit), and sends us a reply (but
183     // we ignore it)
184 
185     reply("Fix applied.");
186     ApplyEdit(*Params.workspaceEdit);
187   } else if (Params.command ==
188              ExecuteCommandParams::CLANGD_INSERT_HEADER_INCLUDE) {
189     auto &FileURI = Params.includeInsertion->textDocument.uri;
190     auto Code = Server.getDocument(FileURI.file());
191     if (!Code)
192       return replyError(ErrorCode::InvalidParams,
193                         ("command " +
194                          ExecuteCommandParams::CLANGD_INSERT_HEADER_INCLUDE +
195                          " called on non-added file " + FileURI.file())
196                             .str());
197     auto Replaces = Server.insertInclude(FileURI.file(), *Code,
198                                          Params.includeInsertion->header);
199     if (!Replaces) {
200       std::string ErrMsg =
201           ("Failed to generate include insertion edits for adding " +
202            Params.includeInsertion->header + " into " + FileURI.file())
203               .str();
204       log(ErrMsg + ":" + llvm::toString(Replaces.takeError()));
205       replyError(ErrorCode::InternalError, ErrMsg);
206       return;
207     }
208     auto Edits = replacementsToEdits(*Code, *Replaces);
209     WorkspaceEdit WE;
210     WE.changes = {{FileURI.uri(), Edits}};
211 
212     reply("Inserted header " + Params.includeInsertion->header);
213     ApplyEdit(std::move(WE));
214   } else {
215     // We should not get here because ExecuteCommandParams would not have
216     // parsed in the first place and this handler should not be called. But if
217     // more commands are added, this will be here has a safe guard.
218     replyError(
219         ErrorCode::InvalidParams,
220         llvm::formatv("Unsupported command \"{0}\".", Params.command).str());
221   }
222 }
223 
224 void ClangdLSPServer::onRename(RenameParams &Params) {
225   Path File = Params.textDocument.uri.file();
226   llvm::Optional<std::string> Code = Server.getDocument(File);
227   if (!Code)
228     return replyError(ErrorCode::InvalidParams,
229                       "onRename called for non-added file");
230 
231   Server.rename(
232       File, Params.position, Params.newName,
233       [File, Code,
234        Params](llvm::Expected<std::vector<tooling::Replacement>> Replacements) {
235         if (!Replacements)
236           return replyError(ErrorCode::InternalError,
237                             llvm::toString(Replacements.takeError()));
238 
239         std::vector<TextEdit> Edits = replacementsToEdits(*Code, *Replacements);
240         WorkspaceEdit WE;
241         WE.changes = {{Params.textDocument.uri.uri(), Edits}};
242         reply(WE);
243       });
244 }
245 
246 void ClangdLSPServer::onDocumentDidClose(DidCloseTextDocumentParams &Params) {
247   Server.removeDocument(Params.textDocument.uri.file());
248 }
249 
250 void ClangdLSPServer::onDocumentOnTypeFormatting(
251     DocumentOnTypeFormattingParams &Params) {
252   auto File = Params.textDocument.uri.file();
253   auto Code = Server.getDocument(File);
254   if (!Code)
255     return replyError(ErrorCode::InvalidParams,
256                       "onDocumentOnTypeFormatting called for non-added file");
257 
258   auto ReplacementsOrError = Server.formatOnType(*Code, File, Params.position);
259   if (ReplacementsOrError)
260     reply(json::ary(replacementsToEdits(*Code, ReplacementsOrError.get())));
261   else
262     replyError(ErrorCode::UnknownErrorCode,
263                llvm::toString(ReplacementsOrError.takeError()));
264 }
265 
266 void ClangdLSPServer::onDocumentRangeFormatting(
267     DocumentRangeFormattingParams &Params) {
268   auto File = Params.textDocument.uri.file();
269   auto Code = Server.getDocument(File);
270   if (!Code)
271     return replyError(ErrorCode::InvalidParams,
272                       "onDocumentRangeFormatting called for non-added file");
273 
274   auto ReplacementsOrError = Server.formatRange(*Code, File, Params.range);
275   if (ReplacementsOrError)
276     reply(json::ary(replacementsToEdits(*Code, ReplacementsOrError.get())));
277   else
278     replyError(ErrorCode::UnknownErrorCode,
279                llvm::toString(ReplacementsOrError.takeError()));
280 }
281 
282 void ClangdLSPServer::onDocumentFormatting(DocumentFormattingParams &Params) {
283   auto File = Params.textDocument.uri.file();
284   auto Code = Server.getDocument(File);
285   if (!Code)
286     return replyError(ErrorCode::InvalidParams,
287                       "onDocumentFormatting called for non-added file");
288 
289   auto ReplacementsOrError = Server.formatFile(*Code, File);
290   if (ReplacementsOrError)
291     reply(json::ary(replacementsToEdits(*Code, ReplacementsOrError.get())));
292   else
293     replyError(ErrorCode::UnknownErrorCode,
294                llvm::toString(ReplacementsOrError.takeError()));
295 }
296 
297 void ClangdLSPServer::onCodeAction(CodeActionParams &Params) {
298   // We provide a code action for each diagnostic at the requested location
299   // which has FixIts available.
300   auto Code = Server.getDocument(Params.textDocument.uri.file());
301   if (!Code)
302     return replyError(ErrorCode::InvalidParams,
303                       "onCodeAction called for non-added file");
304 
305   json::ary Commands;
306   for (Diagnostic &D : Params.context.diagnostics) {
307     auto Edits = getFixIts(Params.textDocument.uri.file(), D);
308     if (!Edits.empty()) {
309       WorkspaceEdit WE;
310       WE.changes = {{Params.textDocument.uri.uri(), std::move(Edits)}};
311       Commands.push_back(json::obj{
312           {"title", llvm::formatv("Apply FixIt {0}", D.message)},
313           {"command", ExecuteCommandParams::CLANGD_APPLY_FIX_COMMAND},
314           {"arguments", {WE}},
315       });
316     }
317   }
318   reply(std::move(Commands));
319 }
320 
321 void ClangdLSPServer::onCompletion(TextDocumentPositionParams &Params) {
322   Server.codeComplete(Params.textDocument.uri.file(), Params.position, CCOpts,
323                       [](Tagged<CompletionList> List) { reply(List.Value); });
324 }
325 
326 void ClangdLSPServer::onSignatureHelp(TextDocumentPositionParams &Params) {
327   Server.signatureHelp(Params.textDocument.uri.file(), Params.position,
328                        [](llvm::Expected<Tagged<SignatureHelp>> SignatureHelp) {
329                          if (!SignatureHelp)
330                            return replyError(
331                                ErrorCode::InvalidParams,
332                                llvm::toString(SignatureHelp.takeError()));
333                          reply(SignatureHelp->Value);
334                        });
335 }
336 
337 void ClangdLSPServer::onGoToDefinition(TextDocumentPositionParams &Params) {
338   Server.findDefinitions(
339       Params.textDocument.uri.file(), Params.position,
340       [](llvm::Expected<Tagged<std::vector<Location>>> Items) {
341         if (!Items)
342           return replyError(ErrorCode::InvalidParams,
343                             llvm::toString(Items.takeError()));
344         reply(json::ary(Items->Value));
345       });
346 }
347 
348 void ClangdLSPServer::onSwitchSourceHeader(TextDocumentIdentifier &Params) {
349   llvm::Optional<Path> Result = Server.switchSourceHeader(Params.uri.file());
350   reply(Result ? URI::createFile(*Result).toString() : "");
351 }
352 
353 void ClangdLSPServer::onDocumentHighlight(TextDocumentPositionParams &Params) {
354   Server.findDocumentHighlights(
355       Params.textDocument.uri.file(), Params.position,
356       [](llvm::Expected<Tagged<std::vector<DocumentHighlight>>> Highlights) {
357         if (!Highlights)
358           return replyError(ErrorCode::InternalError,
359                             llvm::toString(Highlights.takeError()));
360         reply(json::ary(Highlights->Value));
361       });
362 }
363 
364 void ClangdLSPServer::onHover(TextDocumentPositionParams &Params) {
365   Server.findHover(Params.textDocument.uri.file(), Params.position,
366                    [](llvm::Expected<Tagged<Hover>> H) {
367                      if (!H) {
368                        replyError(ErrorCode::InternalError,
369                                   llvm::toString(H.takeError()));
370                        return;
371                      }
372 
373                      reply(H->Value);
374                    });
375 }
376 
377 // FIXME: This function needs to be properly tested.
378 void ClangdLSPServer::onChangeConfiguration(
379     DidChangeConfigurationParams &Params) {
380   ClangdConfigurationParamsChange &Settings = Params.settings;
381 
382   // Compilation database change.
383   if (Settings.compilationDatabasePath.hasValue()) {
384     CDB.setCompileCommandsDir(Settings.compilationDatabasePath.getValue());
385     Server.reparseOpenedFiles();
386   }
387 }
388 
389 ClangdLSPServer::ClangdLSPServer(JSONOutput &Out, unsigned AsyncThreadsCount,
390                                  bool StorePreamblesInMemory,
391                                  const clangd::CodeCompleteOptions &CCOpts,
392                                  llvm::Optional<StringRef> ResourceDir,
393                                  llvm::Optional<Path> CompileCommandsDir,
394                                  bool BuildDynamicSymbolIndex,
395                                  SymbolIndex *StaticIdx)
396     : Out(Out), CDB(std::move(CompileCommandsDir)), CCOpts(CCOpts),
397       Server(CDB, /*DiagConsumer=*/*this, FSProvider, AsyncThreadsCount,
398              StorePreamblesInMemory, BuildDynamicSymbolIndex, StaticIdx,
399              ResourceDir) {}
400 
401 bool ClangdLSPServer::run(std::istream &In, JSONStreamStyle InputStyle) {
402   assert(!IsDone && "Run was called before");
403 
404   // Set up JSONRPCDispatcher.
405   JSONRPCDispatcher Dispatcher([](const json::Expr &Params) {
406     replyError(ErrorCode::MethodNotFound, "method not found");
407   });
408   registerCallbackHandlers(Dispatcher, Out, /*Callbacks=*/*this);
409 
410   // Run the Language Server loop.
411   runLanguageServerLoop(In, Out, InputStyle, Dispatcher, IsDone);
412 
413   // Make sure IsDone is set to true after this method exits to ensure assertion
414   // at the start of the method fires if it's ever executed again.
415   IsDone = true;
416 
417   return ShutdownRequestReceived;
418 }
419 
420 std::vector<TextEdit> ClangdLSPServer::getFixIts(StringRef File,
421                                                  const clangd::Diagnostic &D) {
422   std::lock_guard<std::mutex> Lock(FixItsMutex);
423   auto DiagToFixItsIter = FixItsMap.find(File);
424   if (DiagToFixItsIter == FixItsMap.end())
425     return {};
426 
427   const auto &DiagToFixItsMap = DiagToFixItsIter->second;
428   auto FixItsIter = DiagToFixItsMap.find(D);
429   if (FixItsIter == DiagToFixItsMap.end())
430     return {};
431 
432   return FixItsIter->second;
433 }
434 
435 void ClangdLSPServer::onDiagnosticsReady(
436     PathRef File, Tagged<std::vector<DiagWithFixIts>> Diagnostics) {
437   json::ary DiagnosticsJSON;
438 
439   DiagnosticToReplacementMap LocalFixIts; // Temporary storage
440   for (auto &DiagWithFixes : Diagnostics.Value) {
441     auto Diag = DiagWithFixes.Diag;
442     DiagnosticsJSON.push_back(json::obj{
443         {"range", Diag.range},
444         {"severity", Diag.severity},
445         {"message", Diag.message},
446     });
447     // We convert to Replacements to become independent of the SourceManager.
448     auto &FixItsForDiagnostic = LocalFixIts[Diag];
449     std::copy(DiagWithFixes.FixIts.begin(), DiagWithFixes.FixIts.end(),
450               std::back_inserter(FixItsForDiagnostic));
451   }
452 
453   // Cache FixIts
454   {
455     // FIXME(ibiryukov): should be deleted when documents are removed
456     std::lock_guard<std::mutex> Lock(FixItsMutex);
457     FixItsMap[File] = LocalFixIts;
458   }
459 
460   // Publish diagnostics.
461   Out.writeMessage(json::obj{
462       {"jsonrpc", "2.0"},
463       {"method", "textDocument/publishDiagnostics"},
464       {"params",
465        json::obj{
466            {"uri", URIForFile{File}},
467            {"diagnostics", std::move(DiagnosticsJSON)},
468        }},
469   });
470 }
471