1 /*
2 * CodeCompletion.cpp
3 *
4 * Copyright (C) 2021 by RStudio, PBC
5 *
6 * Unless you have received this program directly from RStudio pursuant
7 * to the terms of a commercial license agreement with RStudio, then
8 * this program is licensed to you under the terms of version 3 of the
9 * GNU Affero General Public License. This program is distributed WITHOUT
10 * ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
11 * MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
12 * AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
13 *
14 */
15
16 #include "CodeCompletion.hpp"
17
18 #include <iostream>
19
20 #include <core/Debug.hpp>
21 #include <shared_core/Error.hpp>
22 #include <core/FileSerializer.hpp>
23 #include <core/system/Process.hpp>
24 #include <core/RegexUtils.hpp>
25
26 #include <r/RExec.hpp>
27
28 #include <session/projects/SessionProjects.hpp>
29 #include <session/SessionModuleContext.hpp>
30
31 #include "RCompilationDatabase.hpp"
32 #include "RSourceIndex.hpp"
33 #include "Diagnostics.hpp"
34
35 using namespace rstudio::core;
36 using namespace rstudio::core::libclang;
37
38 #ifndef _WIN32
39 # define kDevNull "/dev/null"
40 #else
41 # define kDevNull "NUL"
42 #endif
43
44 namespace rstudio {
45 namespace session {
46 namespace modules {
47 namespace clang {
48
49 namespace {
50
51 // cache our system include paths
52 std::vector<std::string> s_systemIncludePaths;
53
friendlyCompletionText(const CodeCompleteResult & result)54 json::Object friendlyCompletionText(const CodeCompleteResult& result)
55 {
56 // transform text
57 std::string text = result.getText();
58 boost::algorithm::replace_all(
59 text,
60 "std::basic_string<char, std::char_traits<char>, std::allocator<char> >",
61 "std::string");
62
63 // creat text object
64 json::Object textJson;
65 textJson["text"] = text;
66 textJson["comment"] = result.getComment();
67
68 return textJson;
69 }
70
71 const int kCompletionUnknown = 0;
72 const int kCompletionVariable = 1;
73 const int kCompletionFunction = 2;
74 const int kCompletionConstructor = 3;
75 const int kCompletionDestructor = 4;
76 const int kCompletionClass = 5;
77 const int kCompletionStruct = 6;
78 const int kCompletionNamespace = 7;
79 const int kCompletionEnum = 8;
80 const int kCompletionEnumValue = 9;
81 const int kCompletionKeyword = 10;
82 const int kCompletionMacro = 11;
83 const int kCompletionFile = 12;
84 const int kCompletionDirectory = 13;
85
completionType(CXCursorKind kind)86 int completionType(CXCursorKind kind)
87 {
88 switch(kind)
89 {
90 case CXCursor_UnexposedDecl:
91 return kCompletionVariable;
92 case CXCursor_StructDecl:
93 case CXCursor_UnionDecl:
94 return kCompletionStruct;
95 case CXCursor_ClassDecl:
96 return kCompletionClass;
97 case CXCursor_EnumDecl:
98 return kCompletionEnum;
99 case CXCursor_FieldDecl:
100 return kCompletionVariable;
101 case CXCursor_EnumConstantDecl:
102 return kCompletionEnumValue;
103 case CXCursor_FunctionDecl:
104 return kCompletionFunction;
105 case CXCursor_VarDecl:
106 case CXCursor_ParmDecl:
107 return kCompletionVariable;
108 case CXCursor_ObjCInterfaceDecl:
109 case CXCursor_ObjCCategoryDecl:
110 case CXCursor_ObjCProtocolDecl:
111 return kCompletionClass;
112 case CXCursor_ObjCPropertyDecl:
113 case CXCursor_ObjCIvarDecl:
114 return kCompletionVariable;
115 case CXCursor_ObjCInstanceMethodDecl:
116 case CXCursor_ObjCClassMethodDecl:
117 return kCompletionFunction;
118 case CXCursor_ObjCImplementationDecl:
119 case CXCursor_ObjCCategoryImplDecl:
120 return kCompletionClass;
121 case CXCursor_TypedefDecl: // while these are typically classes, we don't
122 // have access to the underlying cursor for the
123 // completion (just a CXCursorKind) so there is
124 // no way to know for sure
125
126 return kCompletionClass;
127 case CXCursor_CXXMethod:
128 return kCompletionFunction;
129 case CXCursor_Namespace:
130 return kCompletionNamespace;
131 case CXCursor_LinkageSpec:
132 return kCompletionKeyword;
133 case CXCursor_Constructor:
134 return kCompletionConstructor;
135 case CXCursor_Destructor:
136 return kCompletionDestructor;
137 case CXCursor_ConversionFunction:
138 return kCompletionFunction;
139 case CXCursor_TemplateTypeParameter:
140 case CXCursor_NonTypeTemplateParameter:
141 return kCompletionVariable;
142 case CXCursor_FunctionTemplate:
143 return kCompletionFunction;
144 case CXCursor_ClassTemplate:
145 case CXCursor_ClassTemplatePartialSpecialization:
146 return kCompletionClass;
147 case CXCursor_NamespaceAlias:
148 case CXCursor_UsingDirective:
149 case CXCursor_UsingDeclaration:
150 case CXCursor_TypeAliasDecl:
151 return kCompletionVariable;
152 case CXCursor_ObjCSynthesizeDecl:
153 case CXCursor_ObjCDynamicDecl:
154 case CXCursor_CXXAccessSpecifier:
155 return kCompletionKeyword;
156 case CXCursor_MacroDefinition:
157 return kCompletionMacro;
158 default:
159 return kCompletionUnknown;
160 }
161 }
162
toJson(const CodeCompleteResult & result)163 core::json::Object toJson(const CodeCompleteResult& result)
164 {
165 json::Object resultJson;
166 resultJson["type"] = completionType(result.getKind());
167 resultJson["typed_text"] = result.getTypedText();
168 json::Array textJson;
169 textJson.push_back(friendlyCompletionText(result));
170 resultJson["text"] = textJson;
171 return resultJson;
172 }
173
discoverTranslationUnitIncludePaths(const FilePath & filePath,std::vector<std::string> * pIncludePaths)174 void discoverTranslationUnitIncludePaths(const FilePath& filePath,
175 std::vector<std::string>* pIncludePaths)
176 {
177 std::vector<std::string> args =
178 rCompilationDatabase().compileArgsForTranslationUnit(
179 filePath.getAbsolutePathNative(), false);
180
181 for (const std::string& arg : args)
182 {
183 if (boost::algorithm::starts_with(arg, "-I"))
184 {
185 // skip RStudio's libclang builtin header paths
186 if (arg.find("libclang/builtin-headers") != std::string::npos)
187 continue;
188
189 pIncludePaths->push_back(
190 string_utils::strippedOfQuotes(arg.substr(2)));
191 }
192 }
193 }
194
195 } // end anonymous namespace
196
discoverSystemIncludePaths(std::vector<std::string> * pIncludePaths)197 void discoverSystemIncludePaths(std::vector<std::string>* pIncludePaths)
198 {
199 // if we've cached results already, just use that
200 if (!s_systemIncludePaths.empty())
201 {
202 pIncludePaths->insert(
203 pIncludePaths->end(),
204 s_systemIncludePaths.begin(),
205 s_systemIncludePaths.end());
206 return;
207 }
208
209 core::system::ProcessOptions processOptions;
210 processOptions.redirectStdErrToStdOut = true;
211
212 // add Rtools to PATH if necessary
213 core::system::Options environment;
214 core::system::environment(&environment);
215
216 std::string warning;
217 module_context::addRtoolsToPathIfNecessary(&environment, &warning);
218 processOptions.environment = environment;
219
220 // get the CXX compiler by asking R
221 std::string compilerPath;
222
223 #ifdef _WIN32
224 {
225 core::system::ProcessResult result;
226 Error error = core::system::runCommand("where.exe gcc.exe", processOptions, &result);
227 if (error)
228 LOG_ERROR(error);
229 else if (result.exitStatus != EXIT_SUCCESS)
230 LOG_ERROR_MESSAGE("Error querying CXX compiler: " + result.stdOut);
231 else
232 compilerPath = string_utils::trimWhitespace(result.stdOut);
233 }
234 #else
235 {
236 Error error;
237
238 // resolve R CMD location for shell command
239 FilePath rBinDir;
240 error = module_context::rBinDir(&rBinDir);
241 if (error)
242 {
243 LOG_ERROR(error);
244 return;
245 }
246
247 shell_utils::ShellCommand rCmd = module_context::rCmd(rBinDir);
248
249 core::system::ProcessResult result;
250 rCmd << "config";
251 rCmd << "CXX";
252 error = core::system::runCommand(rCmd, processOptions, &result);
253 if (error)
254 LOG_ERROR(error);
255 else if (result.exitStatus != EXIT_SUCCESS)
256 LOG_ERROR_MESSAGE("Error querying CXX compiler: " + result.stdOut);
257 else
258 compilerPath = string_utils::trimWhitespace(result.stdOut);
259 }
260 #endif
261
262 if (compilerPath.empty())
263 return;
264
265 // it is likely that R is configured to use a compiler that exists
266 // on the PATH; however, when invoked through 'runCommand()' this
267 // can fail unless we explicitly find and resolve that path. also
268 // note that one can configure CXX with multiple arguments
269 // (e.g. as 'g++ -std=c++17') so we must also tear off only the
270 // compiler name. we hence assume that the supplemental arguments
271 // to CXX do not influence the compiler include paths.
272 #ifndef _WIN32
273 {
274 std::string compilerName = compilerPath;
275 std::size_t index = compilerPath.find(' ');
276 if (index != std::string::npos)
277 {
278 compilerName = compilerPath.substr(0, index);
279 }
280 else
281 {
282 compilerName = compilerPath;
283 }
284
285 core::system::ProcessResult result;
286 std::vector<std::string> args = { compilerName };
287 Error error = core::system::runProgram("/usr/bin/which", args, "", processOptions, &result);
288 if (error)
289 LOG_ERROR(error);
290 else if (result.exitStatus != EXIT_SUCCESS)
291 LOG_ERROR_MESSAGE("Error qualifying CXX compiler path: " + result.stdOut);
292 else
293 compilerPath = string_utils::trimWhitespace(result.stdOut);
294 }
295 #endif
296
297 // ask the compiler what the system include paths are (note that both
298 // gcc and clang accept the same command)
299 std::string includePathOutput;
300 {
301 core::system::ProcessResult result;
302 std::string cmd = compilerPath + " -E -x c++ - -v < " kDevNull;
303
304 Error error = core::system::runCommand(cmd, processOptions, &result);
305 if (error)
306 LOG_ERROR(error);
307 else if (result.exitStatus != EXIT_SUCCESS)
308 LOG_ERROR_MESSAGE("Error retrieving system include paths: " + result.stdOut);
309 else
310 includePathOutput = string_utils::trimWhitespace(result.stdOut);
311 }
312 if (includePathOutput.empty())
313 return;
314
315 // strip out the include paths from the output
316 std::string startString = "#include <...> search starts here:";
317 std::string endString = "End of search list.";
318
319 std::string::size_type startPos = includePathOutput.find(startString);
320 if (startPos == std::string::npos)
321 return;
322
323 std::string::size_type endPos = includePathOutput.find(endString, startPos);
324 if (endPos == std::string::npos)
325 return;
326
327 std::string includePathsString = string_utils::trimWhitespace(
328 string_utils::substring(includePathOutput,
329 startPos + startString.size(),
330 endPos));
331
332 // split on newlines
333 std::vector<std::string> includePaths = core::algorithm::split(includePathsString, "\n");
334
335 // remove framework directories
336 core::algorithm::expel_if(includePaths, string_utils::Contains("(framework directory)"));
337
338 // trim whitespace
339 for (std::size_t i = 0, n = includePaths.size(); i < n; ++i)
340 includePaths[i] = string_utils::trimWhitespace(includePaths[i]);
341
342 // update cache
343 s_systemIncludePaths = includePaths;
344
345 // fill result vector
346 pIncludePaths->insert(
347 pIncludePaths->end(),
348 includePaths.begin(),
349 includePaths.end());
350 }
351
352 namespace {
353
discoverRelativeIncludePaths(const FilePath & filePath,const std::string & parentDir,std::vector<std::string> * pIncludePaths)354 void discoverRelativeIncludePaths(const FilePath& filePath,
355 const std::string& parentDir,
356 std::vector<std::string>* pIncludePaths)
357 {
358 // Construct the directory in which to search for includes
359 FilePath targetPath = filePath.getParent().completePath(parentDir);
360 if (!targetPath.exists())
361 return;
362
363 pIncludePaths->push_back(targetPath.getAbsolutePath());
364 }
365
jsonHeaderCompletionResult(const std::string & name,const std::string & source,int completionType)366 json::Object jsonHeaderCompletionResult(const std::string& name,
367 const std::string& source,
368 int completionType)
369 {
370 json::Object completionJson;
371 completionJson["type"] = completionType;
372 completionJson["typed_text"] = name;
373
374 json::Array textJson;
375 json::Object objectJson;
376 objectJson["text"] = name;
377 objectJson["comment"] = source;
378 textJson.push_back(objectJson);
379 completionJson["text"] = textJson;
380
381 return completionJson;
382 }
383
getHeaderCompletionsImpl(const std::string & token,const std::string & parentDir,const FilePath & filePath,const std::string & docId,bool systemHeadersOnly,const core::json::JsonRpcRequest & request,core::json::JsonRpcResponse * pResponse)384 Error getHeaderCompletionsImpl(const std::string& token,
385 const std::string& parentDir,
386 const FilePath& filePath,
387 const std::string& docId,
388 bool systemHeadersOnly,
389 const core::json::JsonRpcRequest& request,
390 core::json::JsonRpcResponse* pResponse)
391 {
392 std::vector<std::string> includePaths;
393
394 // discover the system headers
395 discoverSystemIncludePaths(&includePaths);
396
397 // discover TU-related include paths
398 discoverTranslationUnitIncludePaths(filePath, &includePaths);
399
400 // discover local include paths
401 if (!systemHeadersOnly)
402 discoverRelativeIncludePaths(filePath, parentDir, &includePaths);
403
404 // remove dupes
405 std::sort(includePaths.begin(), includePaths.end());
406 includePaths.erase(std::unique(includePaths.begin(), includePaths.end()),
407 includePaths.end());
408
409 // loop through header include paths and return paths that match token
410 std::set<std::string> discoveredEntries;
411 json::Array completionsJson;
412
413 for (const std::string& path : includePaths)
414 {
415 FilePath includePath(path);
416 if (!includePath.exists())
417 continue;
418
419 FilePath targetPath = includePath.completePath(parentDir);
420 if (!targetPath.exists())
421 continue;
422
423 std::vector<FilePath> children;
424 Error error = targetPath.getChildren(children);
425 if (error)
426 LOG_ERROR(error);
427
428 for (const FilePath& childPath : children)
429 {
430 std::string name = childPath.getFilename();
431 if (discoveredEntries.count(name))
432 continue;
433
434 std::string extension = childPath.getExtensionLowerCase();
435 if (!(extension == ".h" || extension == ".hpp" || extension == ""))
436 continue;
437
438 if (string_utils::isSubsequence(name, token, true))
439 {
440 int type = childPath.isDirectory() ? kCompletionDirectory : kCompletionFile;
441 completionsJson.push_back(jsonHeaderCompletionResult(name,
442 childPath.getAbsolutePath(),
443 type));
444 }
445
446 discoveredEntries.insert(name);
447 }
448 }
449
450 // construct completion result
451 json::Object resultJson;
452 resultJson["completions"] = completionsJson;
453 pResponse->setResult(resultJson);
454 return Success();
455 }
456
getHeaderCompletions(std::string line,const FilePath & filePath,const std::string & docId,const core::json::JsonRpcRequest & request,core::json::JsonRpcResponse * pResponse)457 Error getHeaderCompletions(std::string line,
458 const FilePath& filePath,
459 const std::string& docId,
460 const core::json::JsonRpcRequest& request,
461 core::json::JsonRpcResponse* pResponse)
462 {
463 // extract portion of the path that the user has produced
464 std::string::size_type idx = line.find_first_of("<\"");
465 if (idx == std::string::npos)
466 return Success();
467
468 bool isSystemHeader = line[idx] == '<';
469 std::string pathString = line.substr(idx + 1);
470
471 // split path into 'parentDir' + 'token', e.g.
472 //
473 // #include <foo/bar/ba
474 // ^^^^^^^|^^
475 std::string parentDir, token;
476 std::string::size_type pathDelimIdx = pathString.find_last_of("/");
477 if (pathDelimIdx == std::string::npos)
478 {
479 token = pathString;
480 }
481 else
482 {
483 parentDir = pathString.substr(0, pathDelimIdx);
484 token = pathString.substr(pathDelimIdx + 1);
485 }
486
487 return getHeaderCompletionsImpl(token,
488 parentDir,
489 filePath,
490 docId,
491 isSystemHeader,
492 request,
493 pResponse);
494 }
495
496
497 } // anonymous namespace
498
499
getCppCompletions(const core::json::JsonRpcRequest & request,core::json::JsonRpcResponse * pResponse)500 Error getCppCompletions(const core::json::JsonRpcRequest& request,
501 core::json::JsonRpcResponse* pResponse)
502 {
503 // empty response by default
504 pResponse->setResult(json::Value());
505
506 // get params
507 std::string line, docPath, docId, userText;
508 int row, column;
509 Error error = json::readParams(request.params,
510 &line,
511 &docPath,
512 &docId,
513 &row,
514 &column,
515 &userText);
516 if (error)
517 return error;
518
519 // resolve the docPath if it's aliased
520 FilePath filePath = module_context::resolveAliasedPath(docPath);
521
522 // if it looks like we're requesting autocompletions for an '#include'
523 // statement, take a separate completion path
524 static const boost::regex reInclude("^\\s*#+\\s*include");
525 if (regex_utils::textMatches(line, reInclude, true, true))
526 return getHeaderCompletions(line, filePath, docId, request, pResponse);
527
528 // get the translation unit and do the code completion
529 std::string filename = filePath.getAbsolutePath();
530 TranslationUnit tu = rSourceIndex().getTranslationUnit(filename);
531
532 if (!tu.empty())
533 {
534 std::string lastTypedText;
535 json::Array completionsJson;
536 boost::shared_ptr<CodeCompleteResults> pResults =
537 tu.codeCompleteAt(filename, row, column);
538 if (!pResults->empty())
539 {
540 // get results
541 for (unsigned i = 0; i<pResults->getNumResults(); i++)
542 {
543 CodeCompleteResult result = pResults->getResult(i);
544
545 // filter on user text if we have it
546 if (!userText.empty() &&
547 !boost::algorithm::starts_with(result.getTypedText(), userText))
548 {
549 continue;
550 }
551
552 // check whether this completion is valid and bail if not
553 if (result.getAvailability() != CXAvailability_Available)
554 {
555 continue;
556 }
557
558 std::string typedText = result.getTypedText();
559
560 // if we have the same typed text then just ammend previous result
561 if ((typedText == lastTypedText) && !completionsJson.isEmpty())
562 {
563 json::Object res = completionsJson.getBack().getObject();
564 json::Array text = res["text"].getArray();
565 text.push_back(friendlyCompletionText(result));
566 }
567 else
568 {
569 completionsJson.push_back(toJson(result));
570 }
571
572 lastTypedText = typedText;
573 }
574 }
575
576 json::Object resultJson;
577 resultJson["completions"] = completionsJson.clone();
578 pResponse->setResult(resultJson);
579 }
580
581 return Success();
582 }
583
584 } // namespace clang
585 } // namespace modules
586 } // namespace session
587 } // namespace rstudio
588
589