1 /*
2  * CompletionManagerBase.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 java.util.ArrayList;
18 import java.util.List;
19 
20 import org.rstudio.core.client.Invalidation;
21 import org.rstudio.core.client.Rectangle;
22 import org.rstudio.core.client.StringUtil;
23 import org.rstudio.core.client.command.KeyboardHelper;
24 import org.rstudio.core.client.command.KeyboardShortcut;
25 import org.rstudio.core.client.events.SelectionCommitEvent;
26 import org.rstudio.studio.client.RStudioGinjector;
27 import org.rstudio.studio.client.application.events.EventBus;
28 import org.rstudio.studio.client.common.codetools.CodeToolsServerOperations;
29 import org.rstudio.studio.client.common.codetools.Completions;
30 import org.rstudio.studio.client.common.codetools.RCompletionType;
31 import org.rstudio.studio.client.workbench.prefs.model.UserPrefs;
32 import org.rstudio.studio.client.workbench.snippets.SnippetHelper;
33 import org.rstudio.studio.client.workbench.views.console.shell.assist.CompletionRequester.QualifiedName;
34 import org.rstudio.studio.client.workbench.views.source.editors.text.AceEditor;
35 import org.rstudio.studio.client.workbench.views.source.editors.text.CompletionContext;
36 import org.rstudio.studio.client.workbench.views.source.editors.text.DocDisplay;
37 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.AceEditorCommandEvent;
38 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position;
39 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Range;
40 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Token;
41 import org.rstudio.studio.client.workbench.views.source.editors.text.ace.TokenIterator;
42 import org.rstudio.studio.client.workbench.views.source.editors.text.events.DocumentChangedEvent;
43 import org.rstudio.studio.client.workbench.views.source.editors.text.events.PasteEvent;
44 
45 import com.google.gwt.core.client.Scheduler;
46 import com.google.gwt.dom.client.NativeEvent;
47 import com.google.gwt.event.dom.client.BlurEvent;
48 import com.google.gwt.event.dom.client.ClickEvent;
49 import com.google.gwt.event.dom.client.KeyCodes;
50 import com.google.gwt.event.dom.client.MouseDownEvent;
51 import com.google.gwt.event.logical.shared.AttachEvent;
52 import com.google.gwt.event.logical.shared.SelectionEvent;
53 import com.google.gwt.event.shared.HandlerRegistration;
54 import com.google.gwt.user.client.Timer;
55 import com.google.inject.Inject;
56 
57 public abstract class CompletionManagerBase
58       implements CompletionRequestContext.Host
59 {
goToHelp()60    public abstract void goToHelp();
goToDefinition()61    public abstract void goToDefinition();
showAdditionalHelp(QualifiedName completion)62    public abstract void showAdditionalHelp(QualifiedName completion);
getCompletions(String line, CompletionRequestContext context)63    public abstract boolean getCompletions(String line, CompletionRequestContext context);
64 
65    public interface Callback
66    {
onToken(TokenIterator it, Token token)67       public void onToken(TokenIterator it, Token token);
68    }
69 
CompletionManagerBase(CompletionPopupDisplay popup, DocDisplay docDisplay, CodeToolsServerOperations server, CompletionContext context)70    protected CompletionManagerBase(CompletionPopupDisplay popup,
71                                 DocDisplay docDisplay,
72                                 CodeToolsServerOperations server,
73                                 CompletionContext context)
74    {
75       RStudioGinjector.INSTANCE.injectMembers(this);
76 
77       popup_ = popup;
78       docDisplay_ = docDisplay;
79       server_ = server;
80 
81       invalidation_ = new Invalidation();
82       completionCache_ = new CompletionCache();
83       suggestTimer_ = new SuggestionTimer();
84       helpTimer_ = new HelpTimer();
85       handlers_ = new ArrayList<>();
86       snippets_ = new SnippetHelper((AceEditor) docDisplay, context.getId());
87    }
88 
89    @Inject
initialize(EventBus events, UserPrefs uiPrefs, HelpStrategy helpStrategy)90    private void initialize(EventBus events,
91                            UserPrefs uiPrefs,
92                            HelpStrategy helpStrategy)
93    {
94       events_ = events;
95       userPrefs_ = uiPrefs;
96       helpStrategy_ = helpStrategy;
97    }
98 
onPopupSelection(QualifiedName completion)99    protected void onPopupSelection(QualifiedName completion)
100    {
101       helpTimer_.cancel();
102       if (popup_.isHelpVisible())
103          showPopupHelp(completion);
104       else
105          showPopupHelpDeferred(completion);
106    }
107 
onPopupSelectionCommit(QualifiedName completion)108    protected void onPopupSelectionCommit(QualifiedName completion)
109    {
110       if (completion.type == RCompletionType.SNIPPET)
111          onSelection(snippetToken_, completion);
112       else
113          onSelection(completionToken_, completion);
114    }
115 
116    @Override
onCompletionResponseReceived(CompletionRequestContext.Data data, Completions completions)117    public void onCompletionResponseReceived(CompletionRequestContext.Data data,
118                                             Completions completions)
119    {
120       // if the cursor has moved to a different line, discard this completion request
121       boolean positionChanged =
122             docDisplay_.getCursorPosition().getRow() !=
123             data.getPosition().getRow();
124 
125       if (positionChanged)
126          return;
127 
128       // cache context data (to be used by popup during active completion session)
129       contextData_ = data;
130 
131       String line = data.getLine();
132       if (completions.isCacheable())
133          completionCache_.store(line, completions);
134 
135       int n = completions.getCompletions().length();
136       List<QualifiedName> names = new ArrayList<>();
137       for (int i = 0; i < n; i++)
138       {
139          names.add(new QualifiedName(
140                completions.getCompletions().get(i),
141                completions.getPackages().get(i),
142                false,
143                completions.getType().get(i),
144                completions.getMeta().get(i),
145                completions.getHelpHandler(),
146                completions.getLanguage()));
147       }
148 
149       if (userPrefs_.enableSnippets().getValue())
150       {
151          String[] parts = line.split("\\s+");
152          if (parts.length > 0)
153          {
154             snippetToken_ = parts[parts.length - 1];
155             ArrayList<String> snippets = snippets_.getAvailableSnippets();
156             for (String snippet : snippets)
157                if (snippet.startsWith(snippetToken_))
158                   names.add(QualifiedName.createSnippet(snippet));
159          }
160       }
161 
162       addExtraCompletions(completions.getToken(), names);
163 
164       QualifiedName[] results = new QualifiedName[names.size()];
165       results = names.toArray(results);
166 
167       if (results.length == 0)
168       {
169          popup_.clearCompletions();
170 
171          if (data.autoAcceptSingleCompletionResult())
172          {
173             popup_.showErrorMessage(
174                   "(No matches)",
175                   new PopupPositioner(docDisplay_.getCursorBounds(), popup_));
176          }
177          else
178          {
179             popup_.placeOffscreen();
180          }
181 
182          return;
183       }
184 
185       // if the token we have matches all available completions, we should
186       // implicitly accept it (handle cases where multiple completions with
187       // the same value but different types are received)
188       boolean shouldImplicitlyAccept = true;
189       for (int i = 0; i < results.length; i++)
190       {
191          if (!StringUtil.equals(completions.getToken(), results[i].name))
192          {
193             shouldImplicitlyAccept = false;
194             break;
195          }
196       }
197 
198       if (shouldImplicitlyAccept)
199       {
200          QualifiedName completion = results[0];
201          if (data.autoAcceptSingleCompletionResult() && completion.type == RCompletionType.SNIPPET)
202          {
203             snippets_.applySnippet(completions.getToken(), completion.name);
204             popup_.hide();
205          }
206          else
207          {
208             popup_.placeOffscreen();
209          }
210 
211          // because we swallow tab keypresses for displaying completion popups,
212          // if the user tried to press tab and the completion engine returned
213          // only a single completion with the same value as the token, we need
214          // to insert a literal tab to 'play' the tab key back into the document
215          if (data.isTabTriggeredCompletion())
216             docDisplay_.insertCode("\t");
217 
218          return;
219       }
220 
221       String token = completions.getToken();
222 
223       boolean shouldAutoAccept =
224             results.length == 1 &&
225             data.autoAcceptSingleCompletionResult() &&
226             results[0].type != RCompletionType.DIRECTORY;
227 
228       if (shouldAutoAccept)
229       {
230          onSelection(token, results[0]);
231          return;
232       }
233 
234       Position tokenPos = docDisplay_.getSelectionStart().movedLeft(token.length());
235       Rectangle tokenBounds = docDisplay_.getPositionBounds(tokenPos);
236       completionToken_ = token;
237       popup_.showCompletionValues(
238             results,
239             new PopupPositioner(tokenBounds, popup_),
240             false);
241    }
242 
243    @Override
onCompletionRequestError(String message)244    public void onCompletionRequestError(String message)
245    {
246       contextData_ = null;
247    }
248 
onCompletionCommit()249    public void onCompletionCommit()
250    {
251       QualifiedName value = popup_.getSelectedValue();
252       if (value == null)
253          return;
254 
255       onPopupSelectionCommit(value);
256    }
257 
beginSuggest()258    public boolean beginSuggest()
259    {
260       return beginSuggest(true, false, true);
261    }
262 
beginSuggest(boolean flushCache, boolean isTabTriggered, boolean canAutoAccept)263    public boolean beginSuggest(boolean flushCache,
264                                boolean isTabTriggered,
265                                boolean canAutoAccept)
266    {
267       invalidatePendingRequests(flushCache, false);
268 
269       String line = docDisplay_.getCurrentLineUpToCursor();
270 
271       Token token = docDisplay_.getTokenAt(docDisplay_.getCursorPosition());
272       if (token != null)
273       {
274          // don't complete within comments
275          if (token.hasType("comment"))
276             return false;
277 
278          // don't complete within multi-line strings
279          if (token.hasType("string"))
280          {
281             String cursorTokenValue = token.getValue();
282             boolean isSingleLineString =
283                   cursorTokenValue.startsWith("'") ||
284                   cursorTokenValue.startsWith("\"");
285             if (!isSingleLineString)
286                return false;
287          }
288       }
289 
290       CompletionRequestContext.Data data = new CompletionRequestContext.Data(
291             line,
292             docDisplay_.getCursorPosition(),
293             isTabTriggered,
294             canAutoAccept);
295 
296       CompletionRequestContext context = new CompletionRequestContext(this, data);
297       if (completionCache_.satisfyRequest(line, context))
298          return true;
299 
300       boolean canComplete = getCompletions(line, context);
301 
302       // if tab was used to trigger the completion, but no completions
303       // are available in that context, then insert a literal tab
304       if (!canComplete && isTabTriggered)
305          docDisplay_.insertCode("\t");
306 
307       return canComplete;
308    }
309 
getInvalidationToken()310    public Invalidation.Token getInvalidationToken()
311    {
312       return invalidation_.getInvalidationToken();
313    }
314 
invalidatePendingRequests()315    public void invalidatePendingRequests()
316    {
317       invalidatePendingRequests(true, true);
318    }
319 
invalidatePendingRequests(boolean flushCache, boolean hidePopup)320    public void invalidatePendingRequests(boolean flushCache,
321                                          boolean hidePopup)
322    {
323       invalidation_.invalidate();
324 
325       if (hidePopup && popup_.isShowing())
326       {
327          popup_.hide();
328          popup_.clearHelp(false);
329       }
330 
331       if (flushCache)
332          completionCache_.flush();
333    }
334 
335    // Subclasses should override this to provide extra (e.g. context) completions.
addExtraCompletions(String token, List<QualifiedName> completions)336    protected void addExtraCompletions(String token, List<QualifiedName> completions)
337    {
338    }
339 
340    // Subclasses can override this if they want different behavior in
341    // amending completions appropriate to their type
onCompletionSelected(QualifiedName requestedCompletion)342    protected String onCompletionSelected(QualifiedName requestedCompletion)
343    {
344       String value = requestedCompletion.name;
345       int type = requestedCompletion.type;
346 
347       // add trailing '/' for directory completions
348       if (type == RCompletionType.DIRECTORY)
349          value += "/";
350 
351       // add '()' for function completions
352       boolean insertParensAfterCompletion =
353             RCompletionType.isFunctionType(type) &&
354             userPrefs_.insertParensAfterFunctionCompletion().getValue();
355 
356       if (insertParensAfterCompletion)
357          value += "()";
358 
359       return value;
360    }
361 
362    // Subclasses can override to perform post-completion-insertion actions,
363    // e.g. displaying a tooltip or similar
onCompletionInserted(QualifiedName completion)364    protected void onCompletionInserted(QualifiedName completion)
365    {
366       int type = completion.type;
367       if (!RCompletionType.isFunctionType(type))
368          return;
369 
370       boolean insertParensAfterCompletion =
371             RCompletionType.isFunctionType(type) &&
372             userPrefs_.insertParensAfterFunctionCompletion().getValue();
373 
374       if (insertParensAfterCompletion)
375          docDisplay_.moveCursorBackward();
376    }
377 
378    // Subclasses can override depending on what characters are typically
379    // considered part of identifiers / are relevant to a completion context.
isBoundaryCharacter(char ch)380    protected boolean isBoundaryCharacter(char ch)
381    {
382       boolean valid =
383             Character.isLetterOrDigit(ch) ||
384             ch == '.' ||
385             ch == '_';
386 
387       return !valid;
388    }
389 
390    // Subclasses can override based on what characters might want to trigger
391    // completions, or force a new completion request.
isTriggerCharacter(char ch)392    protected boolean isTriggerCharacter(char ch)
393    {
394       return false;
395    }
396 
codeCompletion()397    public void codeCompletion()
398    {
399       beginSuggest(true, false, true);
400    }
401 
onPaste(PasteEvent event)402    public void onPaste(PasteEvent event)
403    {
404       invalidatePendingRequests();
405    }
406 
close()407    public void close()
408    {
409       invalidatePendingRequests();
410    }
411 
detach()412    public void detach()
413    {
414       removeHandlers();
415       suggestTimer_.cancel();
416       snippets_.detach();
417       invalidatePendingRequests();
418    }
419 
previewKeyDown(NativeEvent event)420    public boolean previewKeyDown(NativeEvent event)
421    {
422       suggestTimer_.cancel();
423 
424       if (isDisabled())
425          return false;
426 
427       int keyCode = event.getKeyCode();
428       int modifier = KeyboardShortcut.getModifierValue(event);
429       if (KeyboardHelper.isModifierKey(keyCode))
430          return false;
431 
432       if (popup_.isShowing())
433       {
434          // attempts to move the cursor left or right should be treated
435          // as requests to cancel the current completion session
436          switch (keyCode)
437          {
438          case KeyCodes.KEY_LEFT:
439          case KeyCodes.KEY_RIGHT:
440             invalidatePendingRequests();
441             return false;
442          }
443 
444          switch (modifier)
445          {
446 
447          case KeyboardShortcut.CTRL:
448          {
449             switch (keyCode)
450             {
451             case KeyCodes.KEY_P: popup_.selectPrev(); return true;
452             case KeyCodes.KEY_N: popup_.selectNext(); return true;
453             }
454 
455             break;
456          }
457 
458          case KeyboardShortcut.NONE:
459          {
460             switch (keyCode)
461             {
462             case KeyCodes.KEY_UP:        popup_.selectPrev();               return true;
463             case KeyCodes.KEY_DOWN:      popup_.selectNext();               return true;
464             case KeyCodes.KEY_PAGEUP:    popup_.selectPrevPage();           return true;
465             case KeyCodes.KEY_PAGEDOWN:  popup_.selectNextPage();           return true;
466             case KeyCodes.KEY_HOME:      popup_.selectFirst();              return true;
467             case KeyCodes.KEY_END:       popup_.selectLast();               return true;
468             case KeyCodes.KEY_ESCAPE:    invalidatePendingRequests();       return true;
469             case KeyCodes.KEY_ENTER:     return onPopupEnter();
470             case KeyCodes.KEY_TAB:       return onPopupTab();
471             case KeyCodes.KEY_F1:        return onPopupAdditionalHelp();
472             }
473 
474             // cancel the current completion session if the cursor
475             // has been moved before the completion start position,
476             // or to a new line. this ensures that backspace can
477             if (contextData_ != null)
478             {
479                Position cursorPos = docDisplay_.getCursorPosition();
480                Position completionPos = contextData_.getPosition();
481 
482                boolean dismiss =
483                      cursorPos.getRow() != completionPos.getRow() ||
484                      cursorPos.getColumn() < completionPos.getColumn();
485 
486                if (dismiss)
487                {
488                   invalidatePendingRequests();
489                   return false;
490                }
491             }
492 
493             // handle backspace specially -- allow it to continue
494             // the current completion session after taking its
495             // associated action
496             if (keyCode == KeyCodes.KEY_BACKSPACE)
497             {
498                Scheduler.get().scheduleDeferred(() ->
499                {
500                   beginSuggest(false, false, false);
501                });
502 
503                return false;
504             }
505 
506             break;
507          }
508          }
509 
510          return false;
511       }
512       else
513       {
514          switch (modifier)
515          {
516 
517          case KeyboardShortcut.NONE:
518          {
519             switch (keyCode)
520             {
521             case KeyCodes.KEY_F1:  goToHelp();       return true;
522             case KeyCodes.KEY_F2:  goToDefinition(); return true;
523             case KeyCodes.KEY_TAB: return onTab();
524             }
525 
526             break;
527          }
528 
529          case KeyboardShortcut.CTRL:
530          {
531             switch (keyCode)
532             {
533             case KeyCodes.KEY_SPACE:
534                if (docDisplay_.isEmacsModeOn())
535                   return false;
536 
537                beginSuggest();
538                return true;
539             }
540 
541             break;
542          }
543 
544          case KeyboardShortcut.SHIFT:
545          {
546             switch (keyCode)
547             {
548             case KeyCodes.KEY_TAB:
549                return snippets_.attemptSnippetInsertion(true);
550             }
551 
552             break;
553          }
554 
555          }
556       }
557 
558       return false;
559    }
560 
previewKeyPress(char charCode)561    public boolean previewKeyPress(char charCode)
562    {
563       if (isDisabled())
564          return false;
565 
566       if (popup_.isShowing())
567       {
568          if (canContinueCompletions(charCode))
569             Scheduler.get().scheduleDeferred(() -> beginSuggest(false, false, false));
570          else
571             invalidatePendingRequests();
572       }
573       else
574       {
575          if (canAutoPopup(charCode, userPrefs_.codeCompletionCharacters().getValue() - 1))
576          {
577             invalidatePendingRequests();
578             suggestTimer_.schedule(true, false);
579          }
580       }
581 
582       return false;
583    }
584 
isDisabled()585    protected boolean isDisabled()
586    {
587       if (docDisplay_.isSnippetsTabStopManagerActive())
588          return true;
589 
590       return false;
591    }
592 
canContinueCompletions(char ch)593    protected boolean canContinueCompletions(char ch)
594    {
595       // NOTE: We allow users to continue a completion 'session' in the case where
596       // a character was mistyped; e.g. imagine the user requested completions with
597       // the token 'rn' and got back:
598       //
599       //    - rnbinom
600       //    - rnorm
601       //
602       // and accidentally typed a 'z'. while no completion item will match, we should
603       // keep the completion session 'live' so that hitting backspace will continue
604       // to show completions with the original 'rn' token.
605       switch (ch)
606       {
607 
608       case ' ':
609       {
610          // for spaces, only continue the completion session if this does indeed match
611          // an existing completion item in the popup
612          String token = completionToken_ + " ";
613          if (popup_.hasCompletions())
614          {
615             for (QualifiedName item : popup_.getItems())
616                if (StringUtil.isSubsequence(item.name, token, false))
617                   return true;
618          }
619 
620          return false;
621       }
622 
623       }
624 
625       return true;
626    }
627 
canAutoPopup(char ch, int lookbackLimit)628    protected boolean canAutoPopup(char ch, int lookbackLimit)
629    {
630       String codeComplete = userPrefs_.codeCompletion().getValue();
631 
632       if (isTriggerCharacter(ch) && !StringUtil.equals(codeComplete, UserPrefs.CODE_COMPLETION_MANUAL))
633          return true;
634 
635       if (!StringUtil.equals(codeComplete, UserPrefs.CODE_COMPLETION_ALWAYS))
636          return false;
637 
638       if (docDisplay_.isVimModeOn() && !docDisplay_.isVimInInsertMode())
639          return false;
640 
641       if (docDisplay_.isCursorInSingleLineString())
642          return false;
643 
644       if (!isBoundaryCharacter(docDisplay_.getCharacterAtCursor()))
645          return false;
646 
647       String currentLine = docDisplay_.getCurrentLine();
648       Position cursorPos = docDisplay_.getCursorPosition();
649       int cursorColumn = cursorPos.getColumn();
650 
651       boolean canAutoPopup =
652             currentLine.length() >= lookbackLimit &&
653             !isBoundaryCharacter(ch);
654 
655       if (!canAutoPopup)
656          return false;
657 
658       for (int i = 0; i < lookbackLimit; i++)
659       {
660          int index = cursorColumn - i - 1;
661          if (isBoundaryCharacter(StringUtil.charAt(currentLine, index)))
662             return false;
663       }
664 
665       return true;
666    }
667 
onSelection(String completionToken, QualifiedName completion)668    private void onSelection(String completionToken,
669                             QualifiedName completion)
670    {
671       invalidatePendingRequests();
672       suggestTimer_.cancel();
673 
674       popup_.clearHelp(false);
675       popup_.setHelpVisible(false);
676 
677       int type = completion.type;
678       if (type == RCompletionType.SNIPPET)
679       {
680          snippets_.applySnippet(completionToken, completion.name);
681       }
682       else
683       {
684          String value = onCompletionSelected(completion);
685 
686          // compute an appropriate offset for completion --
687          // this is necessary in case the user has typed in the interval
688          // between when completions were requested, and the completion
689          // RPC response was received.
690          int offset = 0;
691          if (contextData_ != null)
692          {
693             Position cursorPos = docDisplay_.getCursorPosition();
694             Position completionPos = contextData_.getPosition();
695             offset =
696                   completionToken.length() +
697                   cursorPos.getColumn() -
698                   completionPos.getColumn();
699          }
700 
701          Range[] ranges = docDisplay_.getNativeSelection().getAllRanges();
702          for (Range range : ranges)
703          {
704             Position replaceStart = range.getEnd().movedLeft(offset);
705             Position replaceEnd = range.getEnd();
706             docDisplay_.replaceRange(Range.fromPoints(replaceStart, replaceEnd), value);
707          }
708 
709          onCompletionInserted(completion);
710       }
711 
712       docDisplay_.setFocus(true);
713    }
714 
onPopupEnter()715    private boolean onPopupEnter()
716    {
717       if (popup_.isOffscreen())
718          return false;
719 
720       QualifiedName completion = popup_.getSelectedValue();
721       if (completion == null)
722       {
723          popup_.hide();
724          return false;
725       }
726 
727       onPopupSelectionCommit(completion);
728       return true;
729    }
730 
onPopupTab()731    private boolean onPopupTab()
732    {
733       if (popup_.isOffscreen())
734          return false;
735 
736       QualifiedName completion = popup_.getSelectedValue();
737       if (completion == null)
738       {
739          popup_.hide();
740          return false;
741       }
742 
743       onPopupSelectionCommit(completion);
744       return true;
745    }
746 
onPopupAdditionalHelp()747    private boolean onPopupAdditionalHelp()
748    {
749       if (popup_.isOffscreen())
750          return false;
751 
752       QualifiedName completion = popup_.getSelectedValue();
753       if (completion == null)
754          return false;
755 
756       showAdditionalHelp(completion);
757       return false;
758    }
759 
onDocumentChanged(DocumentChangedEvent event)760    private void onDocumentChanged(DocumentChangedEvent event)
761    {
762       if (!popup_.isShowing())
763          return;
764 
765       if (docDisplay_.inMultiSelectMode())
766          return;
767 
768       if (!event.getEvent().getAction().contentEquals("removeText"))
769          return;
770 
771       Scheduler.get().scheduleDeferred(() -> {
772 
773          int cursorColumn = docDisplay_.getCursorPosition().getColumn();
774          if (cursorColumn == 0)
775          {
776             invalidatePendingRequests();
777             return;
778          }
779 
780          String line = docDisplay_.getCurrentLine();
781          char ch = StringUtil.charAt(line, cursorColumn - 1);
782          if (isBoundaryCharacter(ch))
783          {
784             invalidatePendingRequests();
785             return;
786          }
787 
788          beginSuggest(false, false, false);
789 
790       });
791    }
792 
onTab()793    private boolean onTab()
794    {
795       // Don't auto complete if tab auto completion was disabled
796       if (!userPrefs_.tabCompletion().getValue() || userPrefs_.tabKeyMoveFocus().getValue())
797          return false;
798 
799       // if the line is blank, don't request completions unless
800       // the user has explicitly opted in
801       String line = docDisplay_.getCurrentLineUpToCursor();
802       if (!userPrefs_.tabMultilineCompletion().getValue())
803       {
804          if (line.matches("^\\s*"))
805             return false;
806       }
807 
808       return beginSuggest(true, true, true);
809    }
810 
showPopupHelp(QualifiedName completion)811    private void showPopupHelp(QualifiedName completion)
812    {
813       if (completion.type == RCompletionType.SNIPPET)
814          popup_.displaySnippetHelp(snippets_.getSnippetContents(completion.name));
815       else
816          helpStrategy_.showHelp(completion, popup_);
817    }
818 
showPopupHelpDeferred(QualifiedName completion)819    private void showPopupHelpDeferred(QualifiedName completion)
820    {
821       helpTimer_.schedule(completion);
822    }
823 
toggleHandlers(boolean enable)824    protected void toggleHandlers(boolean enable)
825    {
826       if (enable)
827          addHandlers();
828       else
829          removeHandlers();
830    }
831 
addHandlers()832    private void addHandlers()
833    {
834       removeHandlers();
835 
836       HandlerRegistration[] ownHandlers = defaultHandlers();
837       for (HandlerRegistration handler : ownHandlers)
838          handlers_.add(handler);
839 
840       HandlerRegistration[] userHandlers = handlers();
841       if (userHandlers != null)
842       {
843          for (HandlerRegistration handler : userHandlers)
844             handlers_.add(handler);
845       }
846    }
847 
removeHandlers()848    private void removeHandlers()
849    {
850       for (HandlerRegistration handler : handlers_)
851          handler.removeHandler();
852       handlers_.clear();
853    }
854 
handlers()855    protected HandlerRegistration[] handlers()
856    {
857       return null;
858    }
859 
defaultHandlers()860    private HandlerRegistration[] defaultHandlers()
861    {
862       return new HandlerRegistration[] {
863 
864             docDisplay_.addAttachHandler((AttachEvent event) -> {
865                toggleHandlers(event.isAttached());
866             }),
867 
868             docDisplay_.addBlurHandler((BlurEvent event) -> {
869                onBlur();
870             }),
871 
872             docDisplay_.addClickHandler((ClickEvent event) -> {
873                invalidatePendingRequests();
874             }),
875 
876             docDisplay_.addDocumentChangedHandler((DocumentChangedEvent event) -> {
877                onDocumentChanged(event);
878             }),
879 
880             popup_.addMouseDownHandler((MouseDownEvent event) -> {
881                ignoreNextBlur_ = true;
882             }),
883 
884             popup_.addAttachHandler((AttachEvent event) -> {
885                docDisplay_.setPopupVisible(event.isAttached());
886             }),
887 
888             popup_.addSelectionHandler((SelectionEvent<QualifiedName> event) -> {
889                onPopupSelection(event.getSelectedItem());
890             }),
891 
892             popup_.addSelectionCommitHandler((SelectionCommitEvent<QualifiedName> event) -> {
893                onPopupSelectionCommit(event.getSelectedItem());
894             }),
895 
896             events_.addHandler(AceEditorCommandEvent.TYPE, (AceEditorCommandEvent event) -> {
897                invalidatePendingRequests();
898             })
899 
900       };
901    }
902 
903    private void onBlur()
904    {
905       if (ignoreNextBlur_)
906       {
907          ignoreNextBlur_ = false;
908          return;
909       }
910 
911       invalidatePendingRequests();
912    }
913 
914    private class SuggestionTimer
915    {
916       public SuggestionTimer()
917       {
918          timer_ = new Timer()
919          {
920             @Override
921             public void run()
922             {
923                beginSuggest(flushCache_, false, canAutoInsert_);
924             }
925          };
926       }
927 
928       public void schedule(boolean flushCache,
929                            boolean canAutoInsert)
930       {
931          flushCache_ = flushCache;
932          canAutoInsert_ = canAutoInsert;
933 
934          timer_.schedule(userPrefs_.codeCompletionDelay().getValue());
935       }
936 
937       public void cancel()
938       {
939          timer_.cancel();
940       }
941 
942       private final Timer timer_;
943 
944       private boolean flushCache_;
945       private boolean canAutoInsert_;
946    }
947 
948    private class HelpTimer
949    {
950       public HelpTimer()
951       {
952          timer_ = new Timer()
953          {
954             @Override
955             public void run()
956             {
957                showPopupHelp(completion_);
958             }
959          };
960       }
961 
962       public void schedule(QualifiedName completion)
963       {
964          completion_ = completion;
965 
966          timer_.schedule(600);
967       }
968 
969       public void cancel()
970       {
971          timer_.cancel();
972       }
973 
974       private QualifiedName completion_;
975 
976       private final Timer timer_;
977    }
978 
979    protected final CompletionPopupDisplay popup_;
980    protected final DocDisplay docDisplay_;
981    protected final CodeToolsServerOperations server_;
982 
983    private final Invalidation invalidation_;
984    private final CompletionCache completionCache_;
985    private final SuggestionTimer suggestTimer_;
986    private final HelpTimer helpTimer_;
987    private final SnippetHelper snippets_;
988 
989    private final List<HandlerRegistration> handlers_;
990 
991    private String completionToken_;
992    private String snippetToken_;
993    private boolean ignoreNextBlur_;
994 
995    private CompletionRequestContext.Data contextData_;
996    private HelpStrategy helpStrategy_;
997 
998    protected EventBus events_;
999    protected UserPrefs userPrefs_;
1000 }
1001