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