1 /* 2 * CompletionRequester.java 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 package org.rstudio.studio.client.workbench.views.console.shell.assist; 16 17 import com.google.gwt.core.client.JsArray; 18 import com.google.gwt.core.client.JsArrayBoolean; 19 import com.google.gwt.core.client.JsArrayInteger; 20 import com.google.gwt.core.client.JsArrayString; 21 import com.google.gwt.resources.client.ImageResource; 22 import com.google.gwt.safehtml.shared.SafeHtmlBuilder; 23 import com.google.inject.Inject; 24 25 import org.rstudio.core.client.resources.ImageResource2x; 26 import org.rstudio.core.client.SafeHtmlUtil; 27 import org.rstudio.core.client.StringUtil; 28 import org.rstudio.core.client.js.JsUtil; 29 import org.rstudio.core.client.regex.Pattern; 30 import org.rstudio.studio.client.RStudioGinjector; 31 import org.rstudio.studio.client.common.codetools.CodeToolsServerOperations; 32 import org.rstudio.studio.client.common.codetools.Completions; 33 import org.rstudio.studio.client.common.codetools.RCompletionType; 34 import org.rstudio.studio.client.common.filetypes.FileTypeRegistry; 35 import org.rstudio.studio.client.common.icons.code.CodeIcons; 36 import org.rstudio.studio.client.server.ServerError; 37 import org.rstudio.studio.client.server.ServerRequestCallback; 38 import org.rstudio.studio.client.workbench.codesearch.CodeSearchOracle; 39 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs; 40 import org.rstudio.studio.client.workbench.snippets.SnippetHelper; 41 import org.rstudio.studio.client.workbench.views.console.shell.ConsoleLanguageTracker; 42 import org.rstudio.studio.client.workbench.views.console.shell.assist.RCompletionManager.AutocompletionContext; 43 import org.rstudio.studio.client.workbench.views.source.editors.text.AceEditor; 44 import org.rstudio.studio.client.workbench.views.source.editors.text.DocDisplay; 45 import org.rstudio.studio.client.workbench.views.source.editors.text.RFunction; 46 import org.rstudio.studio.client.workbench.views.source.editors.text.ScopeFunction; 47 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.CodeModel; 48 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.DplyrJoinContext; 49 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position; 50 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.RScopeObject; 51 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.TokenCursor; 52 import org.rstudio.studio.client.workbench.views.source.model.RnwChunkOptions; 53 import org.rstudio.studio.client.workbench.views.source.model.RnwChunkOptions.RnwOptionCompletionResult; 54 import org.rstudio.studio.client.workbench.views.source.model.RnwCompletionContext; 55 56 import java.util.ArrayList; 57 import java.util.Arrays; 58 import java.util.Comparator; 59 import java.util.HashMap; 60 import java.util.List; 61 62 public class CompletionRequester 63 { 64 private CodeToolsServerOperations server_; 65 private UserPrefs uiPrefs_; 66 private final DocDisplay docDisplay_; 67 private final SnippetHelper snippets_; 68 69 private String cachedLinePrefix_; 70 private HashMap<String, CompletionResult> cachedCompletions_ = new HashMap<>(); 71 private RnwCompletionContext rnwContext_; 72 CompletionRequester(RnwCompletionContext rnwContext, DocDisplay docDisplay, SnippetHelper snippets)73 public CompletionRequester(RnwCompletionContext rnwContext, 74 DocDisplay docDisplay, 75 SnippetHelper snippets) 76 { 77 rnwContext_ = rnwContext; 78 docDisplay_ = docDisplay; 79 snippets_ = snippets; 80 RStudioGinjector.INSTANCE.injectMembers(this); 81 } 82 83 @Inject initialize(CodeToolsServerOperations server, UserPrefs uiPrefs)84 void initialize(CodeToolsServerOperations server, UserPrefs uiPrefs) 85 { 86 server_ = server; 87 uiPrefs_ = uiPrefs; 88 } 89 usingCache( String token, final ServerRequestCallback<CompletionResult> callback)90 private boolean usingCache( 91 String token, 92 final ServerRequestCallback<CompletionResult> callback) 93 { 94 return usingCache(token, false, callback); 95 } 96 usingCache( String token, boolean isHelpCompletion, final ServerRequestCallback<CompletionResult> callback)97 private boolean usingCache( 98 String token, 99 boolean isHelpCompletion, 100 final ServerRequestCallback<CompletionResult> callback) 101 { 102 if (isHelpCompletion) 103 token = token.substring(token.lastIndexOf(':') + 1); 104 105 if (cachedLinePrefix_ == null) 106 return false; 107 108 CompletionResult cachedResult = cachedCompletions_.get(""); 109 if (cachedResult == null) 110 return false; 111 112 if (token.toLowerCase().startsWith(cachedLinePrefix_.toLowerCase())) 113 { 114 String diff = token.substring(cachedLinePrefix_.length()); 115 116 // if we already have a cached result for this diff, use it 117 CompletionResult cached = cachedCompletions_.get(diff); 118 if (cached != null) 119 { 120 callback.onResponseReceived(cached); 121 return true; 122 } 123 124 // otherwise, produce a new completion list 125 if (diff.length() > 0 && !diff.endsWith("::")) 126 { 127 callback.onResponseReceived(narrow(cachedResult.token + diff, diff, cachedResult)); 128 return true; 129 } 130 } 131 132 return false; 133 } 134 basename(String absolutePath)135 private String basename(String absolutePath) 136 { 137 return absolutePath.substring(absolutePath.lastIndexOf('/') + 1); 138 } 139 filterStartsWithDot(String item, String token)140 private boolean filterStartsWithDot(String item, 141 String token) 142 { 143 return !(!token.startsWith(".") && item.startsWith(".")); 144 } 145 fuzzy(String string)146 private static final native String fuzzy(String string) /*-{ 147 return string.replace(/(?!^)[._]/g, ""); 148 }-*/; 149 narrow(final String token, final String diff, CompletionResult cachedResult)150 private CompletionResult narrow(final String token, 151 final String diff, 152 CompletionResult cachedResult) 153 { 154 ArrayList<QualifiedName> newCompletions = new ArrayList<>(); 155 newCompletions.ensureCapacity(cachedResult.completions.size()); 156 157 // For completions that are files or directories, we need to post-process 158 // the token and the qualified name to strip out just the basename (filename). 159 // Note that we normalize the paths such that files will have no trailing slash, 160 // while directories will have one trailing slash (but we defend against multiple 161 // trailing slashes) 162 163 // Transform the token once beforehand for completions. 164 final String tokenSub = token.substring(token.lastIndexOf('/') + 1); 165 final String tokenFuzzy = fuzzy(tokenSub); 166 167 for (QualifiedName qname : cachedResult.completions) 168 { 169 // File types are narrowed only by the file name 170 if (RCompletionType.isFileType(qname.type)) 171 { 172 if (StringUtil.isSubsequence(basename(qname.name), tokenFuzzy, true)) 173 newCompletions.add(qname); 174 } 175 else 176 { 177 if (StringUtil.isSubsequence(qname.name, tokenFuzzy, true) && 178 filterStartsWithDot(qname.name, token)) 179 newCompletions.add(qname); 180 } 181 } 182 183 newCompletions.sort(new Comparator<QualifiedName>() 184 { 185 186 @Override 187 public int compare(QualifiedName lhs, QualifiedName rhs) 188 { 189 int lhsScore = RCompletionType.isFileType(lhs.type) 190 ? CodeSearchOracle.scoreMatch(basename(lhs.name), tokenSub, true) 191 : CodeSearchOracle.scoreMatch(lhs.name, token, false); 192 193 int rhsScore = RCompletionType.isFileType(rhs.type) 194 ? CodeSearchOracle.scoreMatch(basename(rhs.name), tokenSub, true) 195 : CodeSearchOracle.scoreMatch(rhs.name, token, false); 196 197 // Place arguments higher (give less penalty) 198 if (lhs.type == RCompletionType.ARGUMENT) lhsScore -= 3; 199 if (rhs.type == RCompletionType.ARGUMENT) rhsScore -= 3; 200 201 if (lhsScore == rhsScore) 202 return lhs.compareTo(rhs); 203 204 return lhsScore < rhsScore ? -1 : 1; 205 } 206 }); 207 208 CompletionResult result = new CompletionResult( 209 token, 210 newCompletions, 211 cachedResult.guessedFunctionName, 212 cachedResult.suggestOnAccept, 213 cachedResult.dontInsertParens); 214 215 cachedCompletions_.put(diff, result); 216 return result; 217 } 218 getDplyrJoinCompletionsString( final String token, final String string, final String cursorPos, final boolean implicit, final ServerRequestCallback<CompletionResult> callback)219 public void getDplyrJoinCompletionsString( 220 final String token, 221 final String string, 222 final String cursorPos, 223 final boolean implicit, 224 final ServerRequestCallback<CompletionResult> callback) 225 { 226 if (usingCache(token, callback)) 227 return; 228 229 server_.getDplyrJoinCompletionsString( 230 token, 231 string, 232 cursorPos, 233 new ServerRequestCallback<Completions>() { 234 235 @Override 236 public void onResponseReceived(Completions response) 237 { 238 cachedLinePrefix_ = token; 239 fillCompletionResult(response, implicit, callback); 240 } 241 242 @Override 243 public void onError(ServerError error) 244 { 245 callback.onError(error); 246 } 247 248 }); 249 } 250 getDplyrJoinCompletions( final DplyrJoinContext joinContext, final boolean implicit, final ServerRequestCallback<CompletionResult> callback)251 public void getDplyrJoinCompletions( 252 final DplyrJoinContext joinContext, 253 final boolean implicit, 254 final ServerRequestCallback<CompletionResult> callback) 255 { 256 final String token = joinContext.getToken(); 257 if (usingCache(token, callback)) 258 return; 259 260 server_.getDplyrJoinCompletions( 261 joinContext.getToken(), 262 joinContext.getLeftData(), 263 joinContext.getRightData(), 264 joinContext.getVerb(), 265 joinContext.getCursorPos(), 266 new ServerRequestCallback<Completions>() { 267 268 @Override 269 public void onError(ServerError error) 270 { 271 callback.onError(error); 272 } 273 274 @Override 275 public void onResponseReceived(Completions response) 276 { 277 cachedLinePrefix_ = token; 278 fillCompletionResult(response, implicit, callback); 279 } 280 281 }); 282 } 283 fillCompletionResult( Completions response, boolean implicit, ServerRequestCallback<CompletionResult> callback)284 private void fillCompletionResult( 285 Completions response, 286 boolean implicit, 287 ServerRequestCallback<CompletionResult> callback) 288 { 289 JsArrayString comp = response.getCompletions(); 290 JsArrayString pkgs = response.getPackages(); 291 JsArrayBoolean quote = response.getQuote(); 292 JsArrayInteger type = response.getType(); 293 JsArrayString meta = response.getMeta(); 294 ArrayList<QualifiedName> newComp = new ArrayList<>(); 295 for (int i = 0; i < comp.length(); i++) 296 { 297 newComp.add(new QualifiedName(comp.get(i), pkgs.get(i), quote.get(i), type.get(i), meta.get(i), response.getHelpHandler(), response.getLanguage())); 298 } 299 300 CompletionResult result = new CompletionResult( 301 response.getToken(), 302 newComp, 303 response.getGuessedFunctionName(), 304 response.getSuggestOnAccept(), 305 response.getOverrideInsertParens()); 306 307 if (response.isCacheable()) 308 { 309 cachedCompletions_.put("", result); 310 } 311 312 if (!implicit || result.completions.size() != 0) 313 callback.onResponseReceived(result); 314 315 } 316 317 private static final Pattern RE_EXTRACTION = Pattern.create("[$@:]", ""); isTopLevelCompletionRequest()318 private boolean isTopLevelCompletionRequest() 319 { 320 String line = docDisplay_.getCurrentLineUpToCursor(); 321 return !RE_EXTRACTION.test(line); 322 } 323 getCompletions( final String token, final List<String> assocData, final List<Integer> dataType, final List<Integer> numCommas, final String functionCallString, final String chainDataName, final JsArrayString chainAdditionalArgs, final JsArrayString chainExcludeArgs, final boolean chainExcludeArgsFromObject, final String filePath, final String documentId, final String line, final boolean isConsole, final boolean implicit, final ServerRequestCallback<CompletionResult> callback)324 public void getCompletions( 325 final String token, 326 final List<String> assocData, 327 final List<Integer> dataType, 328 final List<Integer> numCommas, 329 final String functionCallString, 330 final String chainDataName, 331 final JsArrayString chainAdditionalArgs, 332 final JsArrayString chainExcludeArgs, 333 final boolean chainExcludeArgsFromObject, 334 final String filePath, 335 final String documentId, 336 final String line, 337 final boolean isConsole, 338 final boolean implicit, 339 final ServerRequestCallback<CompletionResult> callback) 340 { 341 boolean isHelp = dataType.size() > 0 && 342 dataType.get(0) == AutocompletionContext.TYPE_HELP; 343 344 if (usingCache(token, isHelp, callback)) 345 return; 346 347 doGetCompletions( 348 token, 349 assocData, 350 dataType, 351 numCommas, 352 functionCallString, 353 chainDataName, 354 chainAdditionalArgs, 355 chainExcludeArgs, 356 chainExcludeArgsFromObject, 357 filePath, 358 documentId, 359 line, 360 isConsole, 361 new ServerRequestCallback<Completions>() 362 { 363 @Override 364 public void onError(ServerError error) 365 { 366 callback.onError(error); 367 } 368 369 @Override 370 public void onResponseReceived(Completions response) 371 { 372 cachedLinePrefix_ = token; 373 String token = response.getToken(); 374 375 JsArrayString comp = response.getCompletions(); 376 JsArrayString pkgs = response.getPackages(); 377 JsArrayBoolean quote = response.getQuote(); 378 JsArrayInteger type = response.getType(); 379 JsArrayString meta = response.getMeta(); 380 ArrayList<QualifiedName> newComp = new ArrayList<>(); 381 382 // Get function completions from the server 383 for (int i = 0; i < comp.length(); i++) 384 if (comp.get(i).endsWith(" = ")) 385 newComp.add(new QualifiedName(comp.get(i), pkgs.get(i), quote.get(i), type.get(i), meta.get(i), response.getHelpHandler(), response.getLanguage())); 386 387 // Try getting our own function argument completions 388 if (!response.getExcludeOtherCompletions()) 389 { 390 addFunctionArgumentCompletions(token, newComp); 391 addScopedArgumentCompletions(token, newComp); 392 } 393 394 // Get variable completions from the current scope 395 if (!response.getExcludeOtherCompletions()) 396 { 397 addScopedCompletions(token, newComp, "variable"); 398 addScopedCompletions(token, newComp, "function"); 399 } 400 401 // Get other server completions 402 for (int i = 0; i < comp.length(); i++) 403 if (!comp.get(i).endsWith(" = ")) 404 newComp.add(new QualifiedName(comp.get(i), pkgs.get(i), quote.get(i), type.get(i), meta.get(i), response.getHelpHandler(), response.getLanguage())); 405 406 // Get snippet completions. Bail if this isn't a top-level 407 // completion -- TODO is to add some more context that allows us 408 // to properly ascertain this. 409 if (isTopLevelCompletionRequest()) 410 { 411 // disable snippets if Python REPL is active for now 412 boolean noSnippets = 413 isConsole && 414 !StringUtil.equals(response.getLanguage(), ConsoleLanguageTracker.LANGUAGE_R); 415 416 if (!noSnippets) 417 { 418 addSnippetCompletions(token, newComp); 419 } 420 } 421 422 // Remove duplicates 423 newComp = resolveDuplicates(newComp); 424 425 CompletionResult result = new CompletionResult( 426 response.getToken(), 427 newComp, 428 response.getGuessedFunctionName(), 429 response.getSuggestOnAccept(), 430 response.getOverrideInsertParens()); 431 432 if (response.isCacheable()) 433 { 434 cachedCompletions_.put("", result); 435 } 436 437 callback.onResponseReceived(result); 438 } 439 }); 440 } 441 442 private ArrayList<QualifiedName> resolveDuplicates(ArrayList<QualifiedName> completions)443 resolveDuplicates(ArrayList<QualifiedName> completions) 444 { 445 ArrayList<QualifiedName> result = new ArrayList<>(completions); 446 447 // sort the results by name and type for efficient processing 448 completions.sort(new Comparator<QualifiedName>() 449 { 450 @Override 451 public int compare(QualifiedName o1, QualifiedName o2) 452 { 453 int name = o1.name.compareTo(o2.name); 454 if (name != 0) 455 return name; 456 return o1.type - o2.type; 457 } 458 }); 459 460 // walk backwards through the list and remove elements which have the 461 // same name and type 462 for (int i = completions.size() - 1; i > 0; i--) 463 { 464 QualifiedName o1 = completions.get(i); 465 QualifiedName o2 = completions.get(i - 1); 466 467 // remove qualified names which have the same name and type (allow 468 // shadowing of contextual results to reduce confusion) 469 if (o1.name == o2.name && 470 (o1.type == o2.type || o1.type == RCompletionType.CONTEXT)) 471 result.remove(o1); 472 } 473 474 return result; 475 } 476 addScopedArgumentCompletions( String token, ArrayList<QualifiedName> completions)477 private void addScopedArgumentCompletions( 478 String token, 479 ArrayList<QualifiedName> completions) 480 { 481 AceEditor editor = (AceEditor) docDisplay_; 482 483 // NOTE: this will be null in the console, so protect against that 484 if (editor != null) 485 { 486 Position cursorPosition = 487 editor.getSession().getSelection().getCursor(); 488 CodeModel codeModel = editor.getSession().getMode().getRCodeModel(); 489 JsArray<RFunction> scopedFunctions = 490 codeModel.getFunctionsInScope(cursorPosition); 491 492 if (scopedFunctions.length() == 0) 493 return; 494 495 String tokenLower = token.toLowerCase(); 496 497 for (int i = 0; i < scopedFunctions.length(); i++) 498 { 499 RFunction scopedFunction = scopedFunctions.get(i); 500 String functionName = scopedFunction.getFunctionName(); 501 502 JsArrayString argNames = scopedFunction.getFunctionArgs(); 503 for (int j = 0; j < argNames.length(); j++) 504 { 505 String argName = argNames.get(j); 506 if (argName.toLowerCase().startsWith(tokenLower)) 507 { 508 if (functionName == null || functionName == "") 509 { 510 completions.add(new QualifiedName( 511 argName, 512 "<anonymous function>", 513 false, 514 RCompletionType.CONTEXT 515 )); 516 } 517 else 518 { 519 completions.add(new QualifiedName( 520 argName, 521 functionName, 522 false, 523 RCompletionType.CONTEXT 524 )); 525 } 526 } 527 } 528 } 529 } 530 } 531 addScopedCompletions( String token, ArrayList<QualifiedName> completions, String type)532 private void addScopedCompletions( 533 String token, 534 ArrayList<QualifiedName> completions, 535 String type) 536 { 537 AceEditor editor = (AceEditor) docDisplay_; 538 539 // NOTE: this will be null in the console, so protect against that 540 if (editor != null) 541 { 542 Position cursorPosition = 543 editor.getSession().getSelection().getCursor(); 544 CodeModel codeModel = editor.getSession().getMode().getRCodeModel(); 545 546 JsArray<RScopeObject> scopeVariables = 547 codeModel.getVariablesInScope(cursorPosition); 548 549 String tokenLower = token.toLowerCase(); 550 for (int i = 0; i < scopeVariables.length(); i++) 551 { 552 RScopeObject variable = scopeVariables.get(i); 553 if (variable.getType() == type && 554 variable.getToken().toLowerCase().startsWith(tokenLower)) 555 completions.add(new QualifiedName( 556 variable.getToken(), 557 variable.getType(), 558 false, 559 RCompletionType.CONTEXT 560 )); 561 } 562 } 563 } 564 addFunctionArgumentCompletions( String token, ArrayList<QualifiedName> completions)565 private void addFunctionArgumentCompletions( 566 String token, 567 ArrayList<QualifiedName> completions) 568 { 569 AceEditor editor = (AceEditor) docDisplay_; 570 571 if (editor != null) 572 { 573 Position cursorPosition = 574 editor.getSession().getSelection().getCursor(); 575 CodeModel codeModel = editor.getSession().getMode().getRCodeModel(); 576 577 // Try to see if we can find a function name 578 TokenCursor cursor = codeModel.getTokenCursor(); 579 580 // NOTE: This can fail if the document is empty 581 if (!cursor.moveToPosition(cursorPosition)) 582 return; 583 584 String tokenLower = token.toLowerCase(); 585 if (cursor.currentValue() == "(" || cursor.findOpeningBracket("(", false)) 586 { 587 if (cursor.moveToPreviousToken()) 588 { 589 // Check to see if this really is the name of a function 590 JsArray<ScopeFunction> functionsInScope = 591 codeModel.getAllFunctionScopes(); 592 593 String tokenName = cursor.currentValue(); 594 for (int i = 0; i < functionsInScope.length(); i++) 595 { 596 ScopeFunction rFunction = functionsInScope.get(i); 597 String fnName = rFunction.getFunctionName(); 598 if (tokenName == fnName) 599 { 600 JsArrayString args = rFunction.getFunctionArgs(); 601 for (int j = 0; j < args.length(); j++) 602 { 603 String arg = args.get(j); 604 if (arg.toLowerCase().startsWith(tokenLower)) 605 completions.add(new QualifiedName( 606 args.get(j) + " = ", 607 fnName, 608 false, 609 RCompletionType.CONTEXT 610 )); 611 } 612 } 613 } 614 } 615 } 616 } 617 } 618 addSnippetCompletions( String token, ArrayList<QualifiedName> completions)619 private void addSnippetCompletions( 620 String token, 621 ArrayList<QualifiedName> completions) 622 { 623 if (StringUtil.isNullOrEmpty(token)) 624 return; 625 626 if (uiPrefs_.enableSnippets().getValue()) 627 { 628 ArrayList<String> snippets = snippets_.getAvailableSnippets(); 629 String tokenLower = token.toLowerCase(); 630 for (String snippet : snippets) 631 if (snippet.toLowerCase().startsWith(tokenLower)) 632 completions.add(0, QualifiedName.createSnippet(snippet)); 633 } 634 } 635 doGetCompletions( final String token, final List<String> assocData, final List<Integer> dataType, final List<Integer> numCommas, final String functionCallString, final String chainObjectName, final JsArrayString chainAdditionalArgs, final JsArrayString chainExcludeArgs, final boolean chainExcludeArgsFromObject, final String filePath, final String documentId, final String line, final boolean isConsole, final ServerRequestCallback<Completions> requestCallback)636 private void doGetCompletions( 637 final String token, 638 final List<String> assocData, 639 final List<Integer> dataType, 640 final List<Integer> numCommas, 641 final String functionCallString, 642 final String chainObjectName, 643 final JsArrayString chainAdditionalArgs, 644 final JsArrayString chainExcludeArgs, 645 final boolean chainExcludeArgsFromObject, 646 final String filePath, 647 final String documentId, 648 final String line, 649 final boolean isConsole, 650 final ServerRequestCallback<Completions> requestCallback) 651 { 652 int optionsStartOffset; 653 if (rnwContext_ != null && 654 (optionsStartOffset = rnwContext_.getRnwOptionsStart(token, token.length())) >= 0) 655 { 656 doGetSweaveCompletions(token, optionsStartOffset, token.length(), requestCallback); 657 } 658 else 659 { 660 server_.getCompletions( 661 token, 662 assocData, 663 dataType, 664 numCommas, 665 functionCallString, 666 chainObjectName, 667 chainAdditionalArgs, 668 chainExcludeArgs, 669 chainExcludeArgsFromObject, 670 filePath, 671 documentId, 672 line, 673 isConsole, 674 requestCallback); 675 } 676 } 677 doGetSweaveCompletions( final String line, final int optionsStartOffset, final int cursorPos, final ServerRequestCallback<Completions> requestCallback)678 private void doGetSweaveCompletions( 679 final String line, 680 final int optionsStartOffset, 681 final int cursorPos, 682 final ServerRequestCallback<Completions> requestCallback) 683 { 684 rnwContext_.getChunkOptions(new ServerRequestCallback<RnwChunkOptions>() 685 { 686 @Override 687 public void onResponseReceived(RnwChunkOptions options) 688 { 689 RnwOptionCompletionResult result = options.getCompletions( 690 line, 691 optionsStartOffset, 692 cursorPos, 693 rnwContext_ == null ? null : rnwContext_.getActiveRnwWeave()); 694 695 String[] pkgNames = new String[result.completions.length()]; 696 Arrays.fill(pkgNames, "<chunk-option>"); 697 698 Completions response = Completions.createCompletions( 699 result.token, 700 result.completions, 701 JsUtil.toJsArrayString(pkgNames), 702 JsUtil.toJsArrayBoolean(new ArrayList<>(result.completions.length())), 703 JsUtil.toJsArrayInteger(new ArrayList<>(result.completions.length())), 704 JsUtil.toJsArrayString(new ArrayList<>(result.completions.length())), 705 "", 706 true, 707 false, 708 true, 709 null, 710 null); 711 712 // Unlike other completion types, Sweave completions are not 713 // guaranteed to narrow the candidate list (in particular 714 // true/false). 715 response.setCacheable(false); 716 if (result.completions.length() > 0 && 717 result.completions.get(0).endsWith("=")) 718 { 719 response.setSuggestOnAccept(true); 720 } 721 722 requestCallback.onResponseReceived(response); 723 } 724 725 @Override 726 public void onError(ServerError error) 727 { 728 requestCallback.onError(error); 729 } 730 }); 731 } 732 flushCache()733 public void flushCache() 734 { 735 cachedLinePrefix_ = null; 736 cachedCompletions_.clear(); 737 } 738 739 public static class CompletionResult 740 { CompletionResult(String token, ArrayList<QualifiedName> completions, String guessedFunctionName, boolean suggestOnAccept, boolean dontInsertParens)741 public CompletionResult(String token, 742 ArrayList<QualifiedName> completions, 743 String guessedFunctionName, 744 boolean suggestOnAccept, 745 boolean dontInsertParens) 746 { 747 this.token = token; 748 this.completions = completions; 749 this.guessedFunctionName = guessedFunctionName; 750 this.suggestOnAccept = suggestOnAccept; 751 this.dontInsertParens = dontInsertParens; 752 } 753 754 public final String token; 755 public final ArrayList<QualifiedName> completions; 756 public final String guessedFunctionName; 757 public final boolean suggestOnAccept; 758 public final boolean dontInsertParens; 759 } 760 761 public static class QualifiedName implements Comparable<QualifiedName> 762 { QualifiedName(String name, String source, boolean shouldQuote, int type)763 public QualifiedName(String name, 764 String source, 765 boolean shouldQuote, 766 int type) 767 { 768 this(name, source, shouldQuote, type, "", null, "R"); 769 } 770 QualifiedName(String name, String source)771 public QualifiedName(String name, 772 String source) 773 { 774 this(name, source, false, RCompletionType.UNKNOWN, "", null, "R"); 775 } 776 QualifiedName(String name, String source, boolean shouldQuote, int type, String meta, String helpHandler, String language)777 public QualifiedName(String name, 778 String source, 779 boolean shouldQuote, 780 int type, 781 String meta, 782 String helpHandler, 783 String language) 784 { 785 this.name = name; 786 this.source = source; 787 this.shouldQuote = shouldQuote; 788 this.type = type; 789 this.meta = meta; 790 this.helpHandler = helpHandler; 791 this.language = language; 792 } 793 createSnippet(String name)794 public static QualifiedName createSnippet(String name) 795 { 796 return new QualifiedName( 797 name, 798 "snippet", 799 false, 800 RCompletionType.SNIPPET, 801 "", 802 null, 803 "R"); 804 } 805 806 @Override toString()807 public String toString() 808 { 809 SafeHtmlBuilder sb = new SafeHtmlBuilder(); 810 811 // Get an icon for the completion 812 // We use separate styles for file icons, so we can nudge them 813 // a bit differently 814 String style = RES.styles().completionIcon(); 815 if (RCompletionType.isFileType(type)) 816 style = RES.styles().fileIcon(); 817 818 SafeHtmlUtil.appendImage( 819 sb, 820 style, 821 getIcon()); 822 823 // Get the display name. Note that for file completions this requires 824 // some munging of the 'name' and 'package' fields. 825 addDisplayName(sb); 826 827 return sb.toSafeHtml().asString(); 828 } 829 addDisplayName(SafeHtmlBuilder sb)830 private void addDisplayName(SafeHtmlBuilder sb) 831 { 832 // Handle files specially 833 if (RCompletionType.isFileType(type)) 834 doAddDisplayNameFile(sb); 835 else 836 doAddDisplayNameGeneric(sb); 837 } 838 doAddDisplayNameFile(SafeHtmlBuilder sb)839 private void doAddDisplayNameFile(SafeHtmlBuilder sb) 840 { 841 ArrayList<Integer> slashIndices = 842 StringUtil.indicesOf(name, '/'); 843 844 if (slashIndices.size() < 1) 845 { 846 SafeHtmlUtil.appendSpan( 847 sb, 848 RES.styles().completion(), 849 name); 850 } 851 else 852 { 853 int lastSlashIndex = slashIndices.get( 854 slashIndices.size() - 1); 855 856 int firstSlashIndex = 0; 857 if (slashIndices.size() > 2) 858 firstSlashIndex = slashIndices.get( 859 slashIndices.size() - 3); 860 861 String endName = name.substring(lastSlashIndex + 1); 862 String startName = ""; 863 if (slashIndices.size() > 2) 864 startName += "..."; 865 startName += name.substring(firstSlashIndex, lastSlashIndex); 866 867 SafeHtmlUtil.appendSpan( 868 sb, 869 RES.styles().completion(), 870 endName); 871 872 SafeHtmlUtil.appendSpan( 873 sb, 874 RES.styles().packageName(), 875 startName); 876 } 877 878 } 879 doAddDisplayNameGeneric(SafeHtmlBuilder sb)880 private void doAddDisplayNameGeneric(SafeHtmlBuilder sb) 881 { 882 // Get the name for the completion 883 SafeHtmlUtil.appendSpan( 884 sb, 885 RES.styles().completion(), 886 name); 887 888 // Display the source for functions and snippets (unless there 889 // is a custom helpHandler provided, indicating that the "source" 890 // isn't a package but rather some custom DollarNames scope) 891 if ((RCompletionType.isFunctionType(type) || 892 type == RCompletionType.SNIPPET || 893 type == RCompletionType.DATASET) && 894 helpHandler == null) 895 { 896 SafeHtmlUtil.appendSpan( 897 sb, 898 RES.styles().packageName(), 899 "{" + source.replaceAll("package:", "") + "}"); 900 } 901 } 902 getIcon()903 private ImageResource getIcon() 904 { 905 if (RCompletionType.isFunctionType(type)) 906 return new ImageResource2x(ICONS.function2x()); 907 908 switch(type) 909 { 910 case RCompletionType.UNKNOWN: 911 return new ImageResource2x(ICONS.variable2x()); 912 case RCompletionType.VECTOR: 913 return new ImageResource2x(ICONS.variable2x()); 914 case RCompletionType.ARGUMENT: 915 return new ImageResource2x(ICONS.variable2x()); 916 case RCompletionType.ARRAY: 917 case RCompletionType.DATAFRAME: 918 return new ImageResource2x(ICONS.dataFrame2x()); 919 case RCompletionType.LIST: 920 return new ImageResource2x(ICONS.clazz2x()); 921 case RCompletionType.ENVIRONMENT: 922 return new ImageResource2x(ICONS.environment2x()); 923 case RCompletionType.S4_CLASS: 924 case RCompletionType.S4_OBJECT: 925 case RCompletionType.R5_CLASS: 926 case RCompletionType.R5_OBJECT: 927 return new ImageResource2x(ICONS.clazz2x()); 928 case RCompletionType.FILE: 929 return getIconForFilename(name); 930 case RCompletionType.DIRECTORY: 931 return new ImageResource2x(ICONS.folder2x()); 932 case RCompletionType.CHUNK: 933 case RCompletionType.ROXYGEN: 934 return new ImageResource2x(ICONS.keyword2x()); 935 case RCompletionType.HELP: 936 return new ImageResource2x(ICONS.help2x()); 937 case RCompletionType.STRING: 938 return new ImageResource2x(ICONS.variable2x()); 939 case RCompletionType.PACKAGE: 940 return new ImageResource2x(ICONS.rPackage2x()); 941 case RCompletionType.KEYWORD: 942 return new ImageResource2x(ICONS.keyword2x()); 943 case RCompletionType.CONTEXT: 944 return new ImageResource2x(ICONS.context2x()); 945 case RCompletionType.SNIPPET: 946 return new ImageResource2x(ICONS.snippet2x()); 947 default: 948 return new ImageResource2x(ICONS.variable2x()); 949 } 950 } 951 getIconForFilename(String name)952 private ImageResource getIconForFilename(String name) 953 { 954 return FILE_TYPE_REGISTRY.getIconForFilename(name).getImageResource(); 955 } 956 parseFromText(String val)957 public static QualifiedName parseFromText(String val) 958 { 959 String name, pkgName = ""; 960 int idx = val.indexOf('{'); 961 if (idx < 0) 962 { 963 name = val; 964 } 965 else 966 { 967 name = val.substring(0, idx).trim(); 968 pkgName = val.substring(idx + 1, val.length() - 1); 969 } 970 971 return new QualifiedName(name, pkgName); 972 } 973 compareTo(QualifiedName o)974 public int compareTo(QualifiedName o) 975 { 976 if (name.endsWith("=") ^ o.name.endsWith("=")) 977 return name.endsWith("=") ? -1 : 1; 978 979 int result = String.CASE_INSENSITIVE_ORDER.compare(name, o.name); 980 if (result != 0) 981 return result; 982 983 String pkg = source == null ? "" : source; 984 String opkg = o.source == null ? "" : o.source; 985 return pkg.compareTo(opkg); 986 } 987 988 @Override equals(Object object)989 public boolean equals(Object object) 990 { 991 if (!(object instanceof QualifiedName)) 992 return false; 993 994 QualifiedName other = (QualifiedName) object; 995 return name.equals(other.name) && 996 type == other.type; 997 } 998 999 @Override hashCode()1000 public int hashCode() 1001 { 1002 int hash = 17; 1003 hash = 31 * hash + name.hashCode(); 1004 hash = 31 * hash + type; 1005 return hash; 1006 } 1007 1008 public final String name; 1009 public final String source; 1010 public final boolean shouldQuote; 1011 public final int type; 1012 public final String meta; 1013 public final String helpHandler; 1014 public final String language; 1015 1016 private static final FileTypeRegistry FILE_TYPE_REGISTRY = 1017 RStudioGinjector.INSTANCE.getFileTypeRegistry(); 1018 } 1019 1020 private static final CompletionRequesterResources RES = 1021 CompletionRequesterResources.INSTANCE; 1022 1023 private static final CodeIcons ICONS = CodeIcons.INSTANCE; 1024 1025 static { 1026 RES.styles().ensureInjected(); 1027 } 1028 1029 } 1030