/*
* RCompletionManager.java
*
* Copyright (C) 2009-12 by RStudio, Inc.
*
* Unless you have received this program directly from RStudio pursuant
* to the terms of a commercial license agreement with RStudio, then
* this program is licensed to you under the terms of version 3 of the
* GNU Affero General Public License. This program is distributed WITHOUT
* ANY EXPRESS OR IMPLIED WARRANTY, INCLUDING THOSE OF NON-INFRINGEMENT,
* MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Please refer to the
* AGPL (http://www.gnu.org/licenses/agpl-3.0.txt) for more details.
*
*/
package org.rstudio.studio.client.workbench.views.console.shell.assist;
import com.google.gwt.core.client.JsArray;
import com.google.gwt.core.client.Scheduler;
import com.google.gwt.core.client.Scheduler.ScheduledCommand;
import com.google.gwt.dom.client.NativeEvent;
import com.google.gwt.event.dom.client.*;
import com.google.gwt.event.logical.shared.AttachEvent;
import com.google.gwt.event.logical.shared.CloseEvent;
import com.google.gwt.event.logical.shared.CloseHandler;
import com.google.gwt.event.logical.shared.SelectionEvent;
import com.google.gwt.event.logical.shared.SelectionHandler;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Timer;
import com.google.gwt.user.client.Event.NativePreviewEvent;
import com.google.gwt.user.client.Event.NativePreviewHandler;
import com.google.gwt.user.client.ui.PopupPanel;
import com.google.inject.Inject;
import org.rstudio.core.client.HandlerRegistrations;
import org.rstudio.core.client.Invalidation;
import org.rstudio.core.client.Rectangle;
import org.rstudio.core.client.RegexUtil;
import org.rstudio.core.client.StringUtil;
import org.rstudio.core.client.command.KeyboardHelper;
import org.rstudio.core.client.command.KeyboardShortcut;
import org.rstudio.core.client.events.SelectionCommitEvent;
import org.rstudio.core.client.events.SelectionCommitHandler;
import org.rstudio.core.client.files.FileSystemItem;
import org.rstudio.core.client.regex.Pattern;
import org.rstudio.studio.client.RStudioGinjector;
import org.rstudio.studio.client.application.events.EventBus;
import org.rstudio.studio.client.common.GlobalDisplay;
import org.rstudio.studio.client.common.GlobalProgressDelayer;
import org.rstudio.studio.client.common.SimpleRequestCallback;
import org.rstudio.studio.client.common.codetools.CodeToolsServerOperations;
import org.rstudio.studio.client.common.codetools.RCompletionType;
import org.rstudio.studio.client.common.filetypes.DocumentMode;
import org.rstudio.studio.client.common.filetypes.FileTypeRegistry;
import org.rstudio.studio.client.server.ServerError;
import org.rstudio.studio.client.server.ServerRequestCallback;
import org.rstudio.studio.client.server.Void;
import org.rstudio.studio.client.workbench.codesearch.model.DataDefinition;
import org.rstudio.studio.client.workbench.codesearch.model.FileFunctionDefinition;
import org.rstudio.studio.client.workbench.codesearch.model.ObjectDefinition;
import org.rstudio.studio.client.workbench.codesearch.model.SearchPathFunctionDefinition;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefs;
import org.rstudio.studio.client.workbench.prefs.model.UIPrefsAccessor;
import org.rstudio.studio.client.workbench.snippets.SnippetHelper;
import org.rstudio.studio.client.workbench.views.console.events.SendToConsoleEvent;
import org.rstudio.studio.client.workbench.views.console.shell.assist.CompletionRequester.CompletionResult;
import org.rstudio.studio.client.workbench.views.console.shell.assist.CompletionRequester.QualifiedName;
import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorDisplay;
import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorLineWithCursorPosition;
import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorPosition;
import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorSelection;
import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorUtil;
import org.rstudio.studio.client.workbench.views.source.editors.text.AceEditor;
import org.rstudio.studio.client.workbench.views.source.editors.text.DocDisplay;
import org.rstudio.studio.client.workbench.views.source.editors.text.NavigableSourceEditor;
import org.rstudio.studio.client.workbench.views.source.editors.text.RCompletionContext;
import org.rstudio.studio.client.workbench.views.source.editors.text.ScopeFunction;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.CodeModel;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.DplyrJoinContext;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Position;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.RInfixData;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Range;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Token;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.TokenCursor;
import org.rstudio.studio.client.workbench.views.source.editors.text.ace.TokenIterator;
import org.rstudio.studio.client.workbench.views.source.editors.text.events.PasteEvent;
import org.rstudio.studio.client.workbench.views.source.editors.text.r.RCompletionToolTip;
import org.rstudio.studio.client.workbench.views.source.editors.text.r.SignatureToolTipManager;
import org.rstudio.studio.client.workbench.views.source.events.CodeBrowserNavigationEvent;
import org.rstudio.studio.client.workbench.views.source.model.RnwCompletionContext;
import org.rstudio.studio.client.workbench.views.source.model.SourcePosition;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class RCompletionManager implements CompletionManager
{
// globally suppress F1 and F2 so no default browser behavior takes those
// keystrokes (e.g. Help in Chrome)
static
{
Event.addNativePreviewHandler(new NativePreviewHandler() {
@Override
public void onPreviewNativeEvent(NativePreviewEvent event)
{
if (event.getTypeInt() == Event.ONKEYDOWN)
{
int keyCode = event.getNativeEvent().getKeyCode();
if ((keyCode == 112 || keyCode == 113) &&
KeyboardShortcut.NONE ==
KeyboardShortcut.getModifierValue(event.getNativeEvent()))
{
event.getNativeEvent().preventDefault();
}
}
}
});
}
public void onPaste(PasteEvent event)
{
popup_.hide();
}
public RCompletionManager(InputEditorDisplay input,
NavigableSourceEditor navigableSourceEditor,
CompletionPopupDisplay popup,
CodeToolsServerOperations server,
InitCompletionFilter initFilter,
RCompletionContext rContext,
RnwCompletionContext rnwContext,
DocDisplay docDisplay,
boolean isConsole)
{
RStudioGinjector.INSTANCE.injectMembers(this);
input_ = input ;
navigableSourceEditor_ = navigableSourceEditor;
popup_ = popup ;
server_ = server ;
rContext_ = rContext;
initFilter_ = initFilter ;
rnwContext_ = rnwContext;
docDisplay_ = docDisplay;
isConsole_ = isConsole;
sigTipManager_ = new SignatureToolTipManager(docDisplay_);
suggestTimer_ = new SuggestionTimer(this, uiPrefs_);
snippets_ = new SnippetHelper((AceEditor) docDisplay, getSourceDocumentPath());
requester_ = new CompletionRequester(rnwContext, docDisplay, snippets_);
handlers_ = new HandlerRegistrations();
handlers_.add(input_.addBlurHandler(new BlurHandler() {
public void onBlur(BlurEvent event)
{
if (!ignoreNextInputBlur_)
invalidatePendingRequests() ;
ignoreNextInputBlur_ = false ;
}
}));
handlers_.add(input_.addClickHandler(new ClickHandler()
{
public void onClick(ClickEvent event)
{
invalidatePendingRequests();
}
}));
handlers_.add(popup_.addSelectionCommitHandler(new SelectionCommitHandler<QualifiedName>() {
public void onSelectionCommit(SelectionCommitEvent<QualifiedName> event)
{
assert context_ != null : "onSelection called but handler is null" ;
if (context_ != null)
context_.onSelection(event.getSelectedItem()) ;
}
}));
handlers_.add(popup_.addSelectionHandler(new SelectionHandler<QualifiedName>() {
public void onSelection(SelectionEvent<QualifiedName> event)
{
lastSelectedItem_ = event.getSelectedItem();
if (popup_.isHelpVisible())
context_.showHelp(lastSelectedItem_);
else
showHelpDeferred(context_, lastSelectedItem_, 600);
}
}));
handlers_.add(popup_.addMouseDownHandler(new MouseDownHandler() {
public void onMouseDown(MouseDownEvent event)
{
ignoreNextInputBlur_ = true ;
}
}));
handlers_.add(popup_.addSelectionHandler(new SelectionHandler<QualifiedName>() {
@Override
public void onSelection(SelectionEvent<QualifiedName> event)
{
docDisplay_.setPopupVisible(true);
}
}));
handlers_.add(popup_.addCloseHandler(new CloseHandler<PopupPanel>()
{
@Override
public void onClose(CloseEvent<PopupPanel> event)
{
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
@Override
public void execute()
{
docDisplay_.setPopupVisible(false);
}
});
}
}));
handlers_.add(popup_.addAttachHandler(new AttachEvent.Handler()
{
private boolean wasSigtipShowing_ = false;
@Override
public void onAttachOrDetach(AttachEvent event)
{
RCompletionToolTip toolTip = sigTipManager_.getToolTip();
if (event.isAttached())
{
if (toolTip != null && toolTip.isShowing())
{
wasSigtipShowing_ = true;
toolTip.setVisible(false);
}
else
{
wasSigtipShowing_ = false;
}
}
else
{
if (toolTip != null && wasSigtipShowing_)
toolTip.setVisible(true);
}
}
}));
}
@Inject
public void initialize(GlobalDisplay globalDisplay,
FileTypeRegistry fileTypeRegistry,
EventBus eventBus,
HelpStrategy helpStrategy,
UIPrefs uiPrefs)
{
globalDisplay_ = globalDisplay;
fileTypeRegistry_ = fileTypeRegistry;
eventBus_ = eventBus;
helpStrategy_ = helpStrategy;
uiPrefs_ = uiPrefs;
}
public void detach()
{
handlers_.removeHandler();
sigTipManager_.detach();
snippets_.detach();
popup_.hide();
}
public void close()
{
popup_.hide();
}
public void codeCompletion()
{
if (initFilter_ == null || initFilter_.shouldComplete(null))
beginSuggest(true, false, true);
}
public void goToHelp()
{
InputEditorLineWithCursorPosition linePos =
InputEditorUtil.getLineWithCursorPosition(input_);
server_.getHelpAtCursor(
linePos.getLine(), linePos.getPosition(),
new SimpleRequestCallback<Void>("Help"));
}
public void goToFunctionDefinition()
{
// check for a file-local definition (intra-file navigation -- using
// the active scope tree)
AceEditor editor = (AceEditor) docDisplay_;
if (editor != null)
{
TokenCursor cursor = editor.getSession().getMode().getRCodeModel().getTokenCursor();
if (cursor.moveToPosition(editor.getCursorPosition(), true))
{
// if the cursor is 'on' a left bracket, move back to the associated
// token (obstensibly a funciton name)
if (cursor.isLeftBracket())
cursor.moveToPreviousToken();
// if the previous token is an extraction operator, we shouldn't
// navigate (as this isn't the 'full' function name)
if (cursor.moveToPreviousToken())
{
if (cursor.isExtractionOperator())
return;
cursor.moveToNextToken();
}
// if this is a string, try resolving that string as a file name
if (cursor.hasType("string"))
{
String tokenValue = cursor.currentValue();
String path = tokenValue.substring(1, tokenValue.length() - 1);
FileSystemItem filePath = FileSystemItem.createFile(path);
// This will show a dialog error if no such file exists; this
// seems the most appropriate action in such a case.
fileTypeRegistry_.editFile(filePath);
}
String functionName = cursor.currentValue();
JsArray<ScopeFunction> scopes =
editor.getAllFunctionScopes();
for (int i = 0; i < scopes.length(); i++)
{
ScopeFunction scope = scopes.get(i);
if (scope.getFunctionName().equals(functionName))
{
navigableSourceEditor_.navigateToPosition(
SourcePosition.create(scope.getPreamble().getRow(),
scope.getPreamble().getColumn()),
true);
return;
}
}
}
}
// intra-file navigation failed -- hit the server and find a definition
// in the project index
// determine current line and cursor position
InputEditorLineWithCursorPosition lineWithPos =
InputEditorUtil.getLineWithCursorPosition(input_);
// delayed progress indicator
final GlobalProgressDelayer progress = new GlobalProgressDelayer(
globalDisplay_, 1000, "Searching for function definition...");
server_.getObjectDefinition(
lineWithPos.getLine(),
lineWithPos.getPosition(),
new ServerRequestCallback<ObjectDefinition>() {
@Override
public void onResponseReceived(ObjectDefinition def)
{
// dismiss progress
progress.dismiss();
// if we got a hit
if (def.getObjectName() != null)
{
// search locally if a function navigator was provided
if (navigableSourceEditor_ != null)
{
// try to search for the function locally
SourcePosition position =
navigableSourceEditor_.findFunctionPositionFromCursor(
def.getObjectName());
if (position != null)
{
navigableSourceEditor_.navigateToPosition(position,
true);
return; // we're done
}
}
// if we didn't satisfy the request using a function
// navigator and we got a file back from the server then
// navigate to the file/loc
if (def.getObjectType() ==
FileFunctionDefinition.OBJECT_TYPE)
{
FileFunctionDefinition fileDef =
def.getObjectData().cast();
fileTypeRegistry_.editFile(fileDef.getFile(),
fileDef.getPosition());
}
// if we didn't get a file back see if we got a
// search path definition
else if (def.getObjectType() ==
SearchPathFunctionDefinition.OBJECT_TYPE)
{
SearchPathFunctionDefinition searchDef =
def.getObjectData().cast();
eventBus_.fireEvent(
new CodeBrowserNavigationEvent(searchDef));
}
// finally, check to see if it's a data frame
else if (def.getObjectType() == DataDefinition.OBJECT_TYPE)
{
eventBus_.fireEvent(new SendToConsoleEvent(
"View(" + def.getObjectName() + ")", true, false));
}
}
}
@Override
public void onError(ServerError error)
{
progress.dismiss();
globalDisplay_.showErrorMessage("Error Searching for Function",
error.getUserMessage());
}
});
}
public boolean previewKeyDown(NativeEvent event)
{
suggestTimer_.cancel();
if (sigTipManager_.previewKeyDown(event))
return true;
if (isDisabled())
return false;
/**
* KEYS THAT MATTER
*
* When popup not showing:
* Tab - attempt completion (handled in Console.java)
*
* When popup showing:
* Esc - dismiss popup
* Enter/Tab - accept current selection
* Up-arrow/Down-arrow - change selected item
* [identifier] - narrow suggestions--or if we're lame, just dismiss
* All others - dismiss popup
*/
nativeEvent_ = event;
int keycode = event.getKeyCode();
int modifier = KeyboardShortcut.getModifierValue(event);
if (!popup_.isShowing())
{
// don't allow ctrl + space for completions in Emacs mode
if (docDisplay_.isEmacsModeOn() && event.getKeyCode() == KeyCodes.KEY_SPACE)
return false;
if (CompletionUtils.isCompletionRequest(event, modifier))
{
if (initFilter_ == null || initFilter_.shouldComplete(event))
{
// If we're in markdown mode, only autocomplete in '```{r',
// '[](', or '`r |' contexts
if (DocumentMode.isCursorInMarkdownMode(docDisplay_))
{
String currentLine = docDisplay_.getCurrentLineUpToCursor();
if (!(Pattern.create("^```{[rR]").test(currentLine) ||
Pattern.create(".*\\[.*\\]\\(").test(currentLine) ||
(Pattern.create(".*`r").test(currentLine) &&
StringUtil.countMatches(currentLine, '`') % 2 == 1)))
return false;
}
// If we're in tex mode, only provide completions in chunks
if (DocumentMode.isCursorInTexMode(docDisplay_))
{
String currentLine = docDisplay_.getCurrentLineUpToCursor();
if (!Pattern.create("^<<").test(currentLine))
return false;
}
return beginSuggest(true, false, true);
}
}
else if (event.getKeyCode() == KeyCodes.KEY_TAB &&
modifier == KeyboardShortcut.SHIFT)
{
return snippets_.attemptSnippetInsertion(true);
}
else if (keycode == 112 // F1
&& modifier == KeyboardShortcut.NONE)
{
goToHelp();
return true;
}
else if (keycode == 113 // F2
&& modifier == KeyboardShortcut.NONE)
{
goToFunctionDefinition();
return true;
}
}
else
{
// bail on modifier keys
if (KeyboardHelper.isModifierKey(keycode))
return false;
// allow emacs-style navigation of popup entries
if (modifier == KeyboardShortcut.CTRL)
{
switch (keycode)
{
case KeyCodes.KEY_P: return popup_.selectPrev();
case KeyCodes.KEY_N: return popup_.selectNext();
}
}
else if (modifier == KeyboardShortcut.NONE)
{
if (keycode == KeyCodes.KEY_ESCAPE)
{
invalidatePendingRequests() ;
return true ;
}
// NOTE: It is possible for the popup to still be showing, but
// showing offscreen with no values. We only grab these keys
// when the popup is both showing, and has completions.
// This functionality is here to ensure backspace works properly;
// e.g "stats::rna" -> "stats::rn" brings completions if the user
// had originally requested completions at e.g. "stats::".
if (popup_.hasCompletions() && !popup_.isOffscreen())
{
if (keycode == KeyCodes.KEY_ENTER)
{
QualifiedName value = popup_.getSelectedValue() ;
if (value != null)
{
context_.onSelection(value) ;
return true ;
}
}
else if (keycode == KeyCodes.KEY_TAB)
{
QualifiedName value = popup_.getSelectedValue() ;
if (value != null)
{
if (value.type == RCompletionType.DIRECTORY)
context_.suggestOnAccept_ = true;
context_.onSelection(value);
return true;
}
}
else if (keycode == KeyCodes.KEY_UP)
return popup_.selectPrev() ;
else if (keycode == KeyCodes.KEY_DOWN)
return popup_.selectNext() ;
else if (keycode == KeyCodes.KEY_PAGEUP)
return popup_.selectPrevPage() ;
else if (keycode == KeyCodes.KEY_PAGEDOWN)
return popup_.selectNextPage() ;
else if (keycode == KeyCodes.KEY_HOME)
return popup_.selectFirst();
else if (keycode == KeyCodes.KEY_END)
return popup_.selectLast();
if (keycode == 112) // F1
{
context_.showHelpTopic() ;
return true ;
}
else if (keycode == 113) // F2
{
goToFunctionDefinition();
return true;
}
}
}
if (canContinueCompletions(event))
return false;
// if we insert a '/', we're probably forming a directory --
// pop up completions
if (keycode == 191 && modifier == KeyboardShortcut.NONE)
{
input_.insertCode("/");
return beginSuggest(true, true, false);
}
// continue showing completions on backspace
if (keycode == KeyCodes.KEY_BACKSPACE && modifier == KeyboardShortcut.NONE &&
!docDisplay_.inMultiSelectMode())
{
int cursorColumn = input_.getCursorPosition().getColumn();
String currentLine = docDisplay_.getCurrentLine();
// only suggest if the character previous to the cursor is an R identifier
// also halt suggestions if we're about to remove the only character on the line
if (cursorColumn > 0)
{
char ch = currentLine.charAt(cursorColumn - 2);
char prevCh = currentLine.charAt(cursorColumn - 3);
boolean isAcceptableCharSequence = isValidForRIdentifier(ch) ||
(ch == ':' && prevCh == ':') ||
ch == '$' ||
ch == '@' ||
ch == '/'; // for file completions
if (currentLine.length() > 0 &&
cursorColumn > 0 &&
isAcceptableCharSequence)
{
// manually remove the previous character
InputEditorSelection selection = input_.getSelection();
InputEditorPosition start = selection.getStart().movePosition(-1, true);
InputEditorPosition end = selection.getStart();
if (currentLine.charAt(cursorColumn) == ')' && currentLine.charAt(cursorColumn - 1) == '(')
{
// flush cache as old completions no longer relevant
requester_.flushCache();
end = selection.getStart().movePosition(1, true);
}
input_.setSelection(new InputEditorSelection(start, end));
input_.replaceSelection("", false);
return beginSuggest(false, false, false);
}
}
else
{
invalidatePendingRequests();
return true;
}
}
invalidatePendingRequests();
return false ;
}
return false ;
}
private boolean isValidForRIdentifier(char c) {
return (c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
(c == '.') ||
(c == '_');
}
private boolean checkCanAutoPopup(char c, int lookbackLimit)
{
if (docDisplay_.isVimModeOn() &&
!docDisplay_.isVimInInsertMode())
return false;
String currentLine = docDisplay_.getCurrentLine();
Position cursorPos = input_.getCursorPosition();
int cursorColumn = cursorPos.getColumn();
// Don't auto-popup when the cursor is within a string
if (docDisplay_.isCursorInSingleLineString())
return false;
// Don't auto-popup if there is a character following the cursor
// (this implies an in-line edit and automatic popups are likely to
// be annoying)
if (isValidForRIdentifier(docDisplay_.getCharacterAtCursor()))
return false;
boolean canAutoPopup =
(currentLine.length() > lookbackLimit - 1 && isValidForRIdentifier(c));
if (isConsole_ && !uiPrefs_.alwaysCompleteInConsole().getValue())
canAutoPopup = false;
if (canAutoPopup)
{
for (int i = 0; i < lookbackLimit; i++)
{
if (!isValidForRIdentifier(currentLine.charAt(cursorColumn - i - 1)))
{
canAutoPopup = false;
break;
}
}
}
return canAutoPopup;
}
public boolean previewKeyPress(char c)
{
suggestTimer_.cancel();
if (isDisabled())
return false;
if (popup_.isShowing())
{
// If insertion of this character completes an available suggestion,
// and is not a prefix match of any other suggestion, then implicitly
// apply that.
QualifiedName selectedItem =
popup_.getSelectedValue();
// NOTE: We should strip off trailing colons so that in-line edits of
// package completions, e.g.
//
// <foo>::
//
// can also dismiss the popup on a perfect match of <foo>.
if (selectedItem != null &&
selectedItem.name.replaceAll(":", "").equals(token_ + c))
{
String fullToken = token_ + c;
// Find prefix matches -- there should only be one if we really
// want this behaviour (ie the current selection)
int prefixMatchCount = 0;
QualifiedName[] items = popup_.getItems();
for (int i = 0; i < items.length; i++)
{
if (items[i].name.startsWith(fullToken))
{
++prefixMatchCount;
if (prefixMatchCount > 1)
break;
}
}
if (prefixMatchCount == 1)
{
// We place the completion list offscreen to ensure that
// backspace events are handled later.
popup_.placeOffscreen();
return false;
}
}
if (c == ':')
{
suggestTimer_.schedule(false, true, false);
return false;
}
if (c == ' ')
return false;
// Always update the current set of completions following
// a key insertion. Defer execution so the key insertion can
// enter the document.
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
@Override
public void execute()
{
beginSuggest(false, true, false);
}
});
return false;
}
else
{
// Bail if we're not in R mode
if (!DocumentMode.isCursorInRMode(docDisplay_))
return false;
// Bail if we're in a single-line string
if (docDisplay_.isCursorInSingleLineString())
return false;
// if there's a selection, bail
if (input_.hasSelection())
return false;
// Bail if there is an alpha-numeric character
// following the cursor
if (isValidForRIdentifier(docDisplay_.getCharacterAtCursor()))
return false;
// Perform an auto-popup if a set number of R identifier characters
// have been inserted (but only if the user has allowed it in prefs)
boolean autoPopupEnabled = uiPrefs_.codeComplete().getValue().equals(
UIPrefsAccessor.COMPLETION_ALWAYS);
if (!autoPopupEnabled)
return false;
// Immediately display completions after '$', '::', etc.
char prevChar = docDisplay_.getCurrentLine().charAt(
input_.getCursorPosition().getColumn() - 1);
if (
(c == ':' && prevChar == ':') ||
(c == '$') ||
(c == '@')
)
{
// Bail if we're in Vim but not in insert mode
if (docDisplay_.isVimModeOn() &&
!docDisplay_.isVimInInsertMode())
{
return false;
}
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
@Override
public void execute()
{
beginSuggest(true, true, false);
}
});
return false;
}
// Check for a valid number of R identifier characters for autopopup
boolean canAutoPopup = checkCanAutoPopup(c, uiPrefs_.alwaysCompleteCharacters().getValue() - 1);
// Attempt to pop up completions immediately after a function call.
if (c == '(' && !isLineInComment(docDisplay_.getCurrentLine()))
{
String token = StringUtil.getToken(
docDisplay_.getCurrentLine(),
input_.getCursorPosition().getColumn(),
"[" + RegexUtil.wordCharacter() + "._]",
false,
true);
if (token.matches("^(library|require|requireNamespace|data)\\s*$"))
canAutoPopup = true;
sigTipManager_.resolveActiveFunctionAndDisplayToolTip();
}
if (
(canAutoPopup) ||
isSweaveCompletion(c))
{
// Delay suggestion to avoid auto-popup while the user is typing
suggestTimer_.schedule(true, true, false);
}
}
return false ;
}
@SuppressWarnings("unused")
private boolean isRoxygenTagValidHere()
{
if (input_.getText().matches("\\s*#+'.*"))
{
String linePart = input_.getText().substring(0, input_.getSelection().getStart().getPosition());
if (linePart.matches("\\s*#+'\\s*"))
return true;
}
return false;
}
private boolean isSweaveCompletion(char c)
{
if (rnwContext_ == null || (c != ',' && c != ' ' && c != '='))
return false;
int optionsStart = rnwContext_.getRnwOptionsStart(
input_.getText(),
input_.getSelection().getStart().getPosition());
if (optionsStart < 0)
{
return false;
}
String linePart = input_.getText().substring(
optionsStart,
input_.getSelection().getStart().getPosition());
return c != ' ' || linePart.matches(".*,\\s*");
}
private static boolean canContinueCompletions(NativeEvent event)
{
if (event.getAltKey()
|| event.getCtrlKey()
|| event.getMetaKey())
{
return false ;
}
int keyCode = event.getKeyCode() ;
if (keyCode >= 'a' && keyCode <= 'z')
return true ;
else if (keyCode >= 'A' && keyCode <= 'Z')
return true ;
else if (keyCode == ' ')
return true ;
else if (KeyboardHelper.isHyphen(event))
return true ;
else if (KeyboardHelper.isUnderscore(event))
return true;
if (event.getShiftKey())
return false ;
if (keyCode >= '0' && keyCode <= '9')
return true ;
if (keyCode == 190) // period
return true ;
return false ;
}
private void invalidatePendingRequests()
{
invalidatePendingRequests(true, true);
}
private void invalidatePendingRequests(boolean flushCache,
boolean hidePopup)
{
invalidation_.invalidate();
if (hidePopup && popup_.isShowing())
{
popup_.hide();
popup_.clearHelp(false);
}
if (flushCache)
requester_.flushCache() ;
}
// Things we need to form an appropriate autocompletion:
//
// 1. The token to the left of the cursor,
// 2. The associated function call (if any -- for arguments),
// 3. The associated data for a `[` call (if any -- completions from data object),
// 4. The associated data for a `[[` call (if any -- completions from data object)
class AutocompletionContext {
// Be sure to sync these with 'SessionCodeTools.R'!
public static final int TYPE_UNKNOWN = 0;
public static final int TYPE_FUNCTION = 1;
public static final int TYPE_SINGLE_BRACKET = 2;
public static final int TYPE_DOUBLE_BRACKET = 3;
public static final int TYPE_NAMESPACE_EXPORTED = 4;
public static final int TYPE_NAMESPACE_ALL = 5;
public static final int TYPE_DOLLAR = 6;
public static final int TYPE_AT = 7;
public static final int TYPE_FILE = 8;
public static final int TYPE_CHUNK = 9;
public static final int TYPE_ROXYGEN = 10;
public static final int TYPE_HELP = 11;
public static final int TYPE_ARGUMENT = 12;
public static final int TYPE_PACKAGE = 13;
public AutocompletionContext(
String token,
List<String> assocData,
List<Integer> dataType,
List<Integer> numCommas,
String functionCallString)
{
token_ = token;
assocData_ = assocData;
dataType_ = dataType;
numCommas_ = numCommas;
functionCallString_ = functionCallString;
}
public AutocompletionContext(
String token,
ArrayList<String> assocData,
ArrayList<Integer> dataType)
{
token_ = token;
assocData_ = assocData;
dataType_ = dataType;
numCommas_ = Arrays.asList(0);
functionCallString_ = "";
}
public AutocompletionContext(
String token,
String assocData,
int dataType)
{
token_ = token;
assocData_ = Arrays.asList(assocData);
dataType_ = Arrays.asList(dataType);
numCommas_ = Arrays.asList(0);
functionCallString_ = "";
}
public AutocompletionContext(
String token,
int dataType)
{
token_ = token;
assocData_ = Arrays.asList("");
dataType_ = Arrays.asList(dataType);
numCommas_ = Arrays.asList(0);
functionCallString_ = "";
}
public AutocompletionContext()
{
token_ = "";
assocData_ = new ArrayList<String>();
dataType_ = new ArrayList<Integer>();
numCommas_ = new ArrayList<Integer>();
functionCallString_ = "";
}
public String getToken()
{
return token_;
}
public void setToken(String token)
{
this.token_ = token;
}
public List<String> getAssocData()
{
return assocData_;
}
public void setAssocData(List<String> assocData)
{
this.assocData_ = assocData;
}
public List<Integer> getDataType()
{
return dataType_;
}
public void setDataType(List<Integer> dataType)
{
this.dataType_ = dataType;
}
public List<Integer> getNumCommas()
{
return numCommas_;
}
public void setNumCommas(List<Integer> numCommas)
{
this.numCommas_ = numCommas;
}
public String getFunctionCallString()
{
return functionCallString_;
}
public void setFunctionCallString(String functionCallString)
{
this.functionCallString_ = functionCallString;
}
public void add(String assocData, Integer dataType, Integer numCommas)
{
assocData_.add(assocData);
dataType_.add(dataType);
numCommas_.add(numCommas);
}
public void add(String assocData, Integer dataType)
{
add(assocData, dataType, 0);
}
public void add(String assocData)
{
add(assocData, AutocompletionContext.TYPE_UNKNOWN, 0);
}
private String token_;
private List<String> assocData_;
private List<Integer> dataType_;
private List<Integer> numCommas_;
private String functionCallString_;
}
private boolean isLineInRoxygenComment(String line)
{
Pattern pattern = Pattern.create("^\\s*#+'");
return pattern.test(line);
}
private boolean isLineInComment(String line)
{
return StringUtil.stripBalancedQuotes(line).contains("#");
}
/**
* If false, the suggest operation was aborted
*/
private boolean beginSuggest(boolean flushCache,
boolean implicit,
boolean canAutoInsert)
{
suggestTimer_.cancel();
if (!input_.isSelectionCollapsed())
return false ;
invalidatePendingRequests(flushCache, false);
InputEditorSelection selection = input_.getSelection() ;
if (selection == null)
return false;
int cursorCol = selection.getStart().getPosition();
String firstLine = input_.getText().substring(0, cursorCol);
// never autocomplete in (non-roxygen) comments, or at the start
// of roxygen comments (e.g. at "#' |")
if (isLineInComment(firstLine) && !isLineInRoxygenComment(firstLine))
return false;
// don't auto-complete with tab on lines with only whitespace,
// if the insertion character was a tab (unless the user has opted in)
if (!uiPrefs_.allowTabMultilineCompletion().getValue())
{
if (nativeEvent_ != null &&
nativeEvent_.getKeyCode() == KeyCodes.KEY_TAB)
if (firstLine.matches("^\\s*$"))
return false;
}
AutocompletionContext context = getAutocompletionContext();
// Fix up the context token for non-file completions -- e.g. in
//
// foo<-rn
//
// we erroneously capture '-' as part of the token name. This is awkward
// but is effectively a bandaid until the autocompletion revamp.
if (context.getToken().startsWith("-"))
context.setToken(context.getToken().substring(1));
// fix up roxygen autocompletion for case where '@' is snug against
// the comment marker
if (context.getToken().equals("'@"))
context.setToken(context.getToken().substring(1));
context_ = new CompletionRequestContext(invalidation_.getInvalidationToken(),
selection,
canAutoInsert);
RInfixData infixData = RInfixData.create();
AceEditor editor = (AceEditor) docDisplay_;
if (editor != null)
{
CodeModel codeModel = editor.getSession().getMode().getRCodeModel();
TokenCursor cursor = codeModel.getTokenCursor();
if (cursor.moveToPosition(input_.getCursorPosition()))
{
String token = "";
if (cursor.hasType("identifier"))
token = cursor.currentValue();
String cursorPos = "left";
if (cursor.currentValue() == "=")
cursorPos = "right";
TokenCursor clone = cursor.cloneCursor();
if (clone.moveToPreviousToken())
if (clone.currentValue() == "=")
cursorPos = "right";
// Try to get a dplyr join completion
DplyrJoinContext joinContext =
codeModel.getDplyrJoinContextFromInfixChain(cursor);
// If that failed, try a non-infix lookup
if (joinContext == null)
{
String joinString =
getDplyrJoinString(editor, cursor);
if (!StringUtil.isNullOrEmpty(joinString))
{
requester_.getDplyrJoinCompletionsString(
token,
joinString,
cursorPos,
implicit,
context_);
return true;
}
}
else
{
requester_.getDplyrJoinCompletions(
joinContext,
implicit,
context_);
return true;
}
// Try to see if there's an object name we should use to supplement
// completions
if (cursor.moveToPosition(input_.getCursorPosition()))
infixData = codeModel.getDataFromInfixChain(cursor);
}
}
String filePath = getSourceDocumentPath();
String docId = getSourceDocumentId();
// Provide 'line' for R custom completers
String line = docDisplay_.getCurrentLineUpToCursor();
requester_.getCompletions(
context.getToken(),
context.getAssocData(),
context.getDataType(),
context.getNumCommas(),
context.getFunctionCallString(),
infixData.getDataName(),
infixData.getAdditionalArgs(),
infixData.getExcludeArgs(),
infixData.getExcludeArgsFromObject(),
filePath,
docId,
line,
implicit,
context_);
return true ;
}
private String getDplyrJoinString(
AceEditor editor,
TokenCursor cursor)
{
while (true)
{
int commaCount = cursor.findOpeningBracketCountCommas("(", true);
if (commaCount == -1)
break;
if (!cursor.moveToPreviousToken())
return "";
if (!cursor.currentValue().matches(".*join$"))
continue;
if (commaCount < 2)
return "";
Position start = cursor.currentPosition();
if (!cursor.moveToNextToken())
return "";
if (!cursor.fwdToMatchingToken())
return "";
Position end = cursor.currentPosition();
end.setColumn(end.getColumn() + 1);
return editor.getTextForRange(Range.fromPoints(
start, end));
}
return "";
}
private void addAutocompletionContextForFile(AutocompletionContext context,
String line)
{
int index = Math.max(line.lastIndexOf('"'), line.lastIndexOf('\''));
String token = line.substring(index + 1);
context.add(token, AutocompletionContext.TYPE_FILE);
context.setToken(token);
}
private AutocompletionContext getAutocompletionContextForFileMarkdownLink(
String line)
{
int index = line.lastIndexOf('(');
String token = line.substring(index + 1);
AutocompletionContext result = new AutocompletionContext(
token,
token,
AutocompletionContext.TYPE_FILE);
// NOTE: we overload the meaning of the function call string for file
// completions, to signal whether we should generate files relative to
// the current working directory, or to the file being used for
// completions
result.setFunctionCallString("useFile");
return result;
}
private void addAutocompletionContextForNamespace(
String token,
AutocompletionContext context)
{
String[] splat = token.split(":{2,3}");
String left = "";
if (splat.length <= 0)
{
left = "";
}
else
{
left = splat[0];
}
int type = token.contains(":::") ?
AutocompletionContext.TYPE_NAMESPACE_ALL :
AutocompletionContext.TYPE_NAMESPACE_EXPORTED;
context.add(left, type);
}
private boolean addAutocompletionContextForDollar(AutocompletionContext context)
{
// Establish an evaluation context by looking backwards
AceEditor editor = (AceEditor) docDisplay_;
if (editor == null)
return false;
CodeModel codeModel = editor.getSession().getMode().getRCodeModel();
codeModel.tokenizeUpToRow(input_.getCursorPosition().getRow());
TokenCursor cursor = codeModel.getTokenCursor();
if (!cursor.moveToPosition(input_.getCursorPosition()))
return false;
// Move back to the '$'
while (cursor.currentValue() != "$" && cursor.currentValue() != "@")
if (!cursor.moveToPreviousToken())
return false;
int type = cursor.currentValue() == "$" ?
AutocompletionContext.TYPE_DOLLAR :
AutocompletionContext.TYPE_AT;
// Put a cursor here
TokenCursor contextEndCursor = cursor.cloneCursor();
// We allow for arbitrary elements previous, so we want to get e.g.
//
// env::foo()$bar()[1]$baz
// Get the string forming the context
//
//
// If this fails, we still want to report an empty evaluation context
// (the completion is still occurring in a '$' context, so we do want
// to exclude completions from other scopes)
String data = "";
if (cursor.moveToPreviousToken() && cursor.findStartOfEvaluationContext())
{
data = editor.getTextForRange(Range.fromPoints(
cursor.currentPosition(),
contextEndCursor.currentPosition()));
}
context.add(data, type);
return true;
}
private AutocompletionContext getAutocompletionContext()
{
AutocompletionContext context = new AutocompletionContext();
String firstLine = input_.getText();
int row = input_.getCursorPosition().getRow();
// trim to cursor position
firstLine = firstLine.substring(0, input_.getCursorPosition().getColumn());
// If we're in Markdown mode and have an appropriate string, try to get
// file completions
if (DocumentMode.isCursorInMarkdownMode(docDisplay_) &&
firstLine.matches(".*\\[.*\\]\\(.*"))
return getAutocompletionContextForFileMarkdownLink(firstLine);
// Get the token at the cursor position.
String tokenRegex = ".*[^" +
RegexUtil.wordCharacter() +
"._:$@'\"`-]";
String token = firstLine.replaceAll(tokenRegex, "");
// If we're completing an object within a string, assume it's a
// file-system completion. Note that we may need other contextual information
// to decide if e.g. we only want directories.
String firstLineStripped = StringUtil.stripBalancedQuotes(
StringUtil.stripRComment(firstLine));
boolean isFileCompletion = false;
if (firstLineStripped.indexOf('\'') != -1 ||
firstLineStripped.indexOf('"') != -1)
{
isFileCompletion = true;
addAutocompletionContextForFile(context, firstLine);
}
// If this line starts with '```{', then we're completing chunk options
// pass the whole line as a token
if (firstLine.startsWith("```{") || firstLine.startsWith("<<"))
return new AutocompletionContext(firstLine, AutocompletionContext.TYPE_CHUNK);
// If this line starts with a '?', assume it's a help query
if (firstLine.matches("^\\s*[?].*"))
return new AutocompletionContext(token, AutocompletionContext.TYPE_HELP);
// escape early for roxygen
if (firstLine.matches("\\s*#+'.*"))
return new AutocompletionContext(token, AutocompletionContext.TYPE_ROXYGEN);
// If the token has '$' or '@', add in the autocompletion context --
// note that we still need parent contexts to give more information
// about the appropriate completion
if (token.contains("$") || token.contains("@"))
addAutocompletionContextForDollar(context);
// If the token has '::' or ':::', add that context. Note that
// we still need outer contexts (so that e.g., if we try
// 'debug(stats::rnorm)' we know not to auto-insert parens)
if (token.contains("::"))
addAutocompletionContextForNamespace(token, context);
// If this is not a file completion, we need to further strip and
// then set the token. Note that the token will have already been
// set if this is a file completion.
token = token.replaceAll(".*[$@:]", "");
if (!isFileCompletion)
context.setToken(token);
// access to the R Code model
AceEditor editor = (AceEditor) docDisplay_;
if (editor == null)
return context;
CodeModel codeModel = editor.getSession().getMode().getRCodeModel();
// We might need to grab content from further up in the document than
// the current cursor position -- so tokenize ahead.
codeModel.tokenizeUpToRow(row + 100);
// Make a token cursor and place it at the first token previous
// to the cursor.
TokenCursor tokenCursor = codeModel.getTokenCursor();
if (!tokenCursor.moveToPosition(input_.getCursorPosition()))
return context;
// Check to see if the token following the cursor is a `::` or `:::`.
// If that's the case, then we probably only want to complete package
// names.
if (tokenCursor.moveToNextToken())
{
if (tokenCursor.currentValue() == ":" ||
tokenCursor.currentValue() == "::" ||
tokenCursor.currentValue() == ":::")
{
return new AutocompletionContext(
token,
AutocompletionContext.TYPE_PACKAGE);
}
tokenCursor.moveToPreviousToken();
}
TokenCursor startCursor = tokenCursor.cloneCursor();
// Find an opening '(' or '[' -- this provides the function or object
// for completion.
int initialNumCommas = 0;
if (tokenCursor.currentValue() != "(" && tokenCursor.currentValue() != "[")
{
int commaCount = tokenCursor.findOpeningBracketCountCommas(
new String[]{ "[", "(" }, true);
// commaCount == -1 implies we failed to find an opening bracket
if (commaCount == -1)
{
commaCount = tokenCursor.findOpeningBracketCountCommas("[", false);
if (commaCount == -1)
return context;
else
initialNumCommas = commaCount;
}
else
{
initialNumCommas = commaCount;
}
}
// Figure out whether we're looking at '(', '[', or '[[',
// and place the token cursor on the first token preceding.
TokenCursor endOfDecl = tokenCursor.cloneCursor();
int initialDataType = AutocompletionContext.TYPE_UNKNOWN;
if (tokenCursor.currentValue() == "(")
{
initialDataType = AutocompletionContext.TYPE_FUNCTION;
if (!tokenCursor.moveToPreviousToken())
return context;
}
else if (tokenCursor.currentValue() == "[")
{
if (!tokenCursor.moveToPreviousToken())
return context;
if (tokenCursor.currentValue() == "[")
{
if (!endOfDecl.moveToPreviousToken())
return context;
initialDataType = AutocompletionContext.TYPE_DOUBLE_BRACKET;
if (!tokenCursor.moveToPreviousToken())
return context;
}
else
{
initialDataType = AutocompletionContext.TYPE_SINGLE_BRACKET;
}
}
// Get the string marking the function or data
if (!tokenCursor.findStartOfEvaluationContext())
return context;
// Try to get the function call string -- either there's
// an associated closing paren we can use, or we should just go up
// to the current cursor position.
// First, attempt to determine where the closing paren is located. If
// this fails, we'll just use the start cursor's position (and later
// attempt to finish the expression to make it parsable)
Position endPos = startCursor.currentPosition();
endPos.setColumn(endPos.getColumn() + startCursor.currentValue().length());
// try to look forward for closing paren
if (endOfDecl.currentValue() == "(")
{
TokenCursor closingParenCursor = endOfDecl.cloneCursor();
if (closingParenCursor.fwdToMatchingToken())
{
endPos = closingParenCursor.currentPosition();
endPos.setColumn(endPos.getColumn() + 1);
}
}
// We can now set the function call string.
//
// We strip out the current statement under the cursor, so that
// match.call() can later properly resolve the current argument.
//
// Attempt to find the start of the current statement.
TokenCursor clone = startCursor.cloneCursor();
do
{
String value = clone.currentValue();
if (value.indexOf(",") != -1 || value.equals("("))
break;
if (clone.bwdToMatchingToken())
continue;
} while (clone.moveToPreviousToken());
Position startPosition = clone.currentPosition();
// Include the opening paren if that's what we found
if (clone.currentValue().equals("("))
startPosition.setColumn(startPosition.getColumn() + 1);
String beforeText = editor.getTextForRange(Range.fromPoints(
tokenCursor.currentPosition(),
startPosition));
// Now, attempt to find the end of the current statement.
// Look for the ',' or ')' that ends the statement for the
// currently active argument.
boolean lookupSucceeded = false;
while (clone.moveToNextToken())
{
String value = clone.currentValue();
if (value.indexOf(",") != -1 || value.equals(")"))
{
lookupSucceeded = true;
break;
}
// Bail if we find a closing paren (we should walk over matched
// pairs properly, so finding one implies that we have a parse error).
if (value.equals("]") || value.equals("}"))
break;
if (clone.fwdToMatchingToken())
continue;
}
String afterText = "";
if (lookupSucceeded)
{
afterText = editor.getTextForRange(Range.fromPoints(
clone.currentPosition(),
endPos));
}
context.setFunctionCallString(
(beforeText + afterText).trim());
// Try to identify whether we're producing autocompletions for
// a _named_ function argument; if so, produce completions tuned to
// that argument.
TokenCursor argsCursor = startCursor.cloneCursor();
do
{
String argsValue = argsCursor.currentValue();
// Bail if we encounter tokens that we don't expect as part
// of the current expression -- this implies we're not really
// within a named argument, although this isn't perfect.
if (argsValue.equals(",") ||
argsValue.equals("(") ||
argsValue.equals("$") ||
argsValue.equals("@") ||
argsValue.equals("::") ||
argsValue.equals(":::") ||
argsValue.equals("]") ||
argsValue.equals(")") ||
argsValue.equals("}"))
{
break;
}
// If we encounter an '=', we assume that this is
// a function argument.
if (argsValue.equals("=") && argsCursor.moveToPreviousToken())
{
if (!isFileCompletion)
context.setToken(token);
context.add(
argsCursor.currentValue(),
AutocompletionContext.TYPE_ARGUMENT,
0);
return context;
}
} while (argsCursor.moveToPreviousToken());
String initialData =
docDisplay_.getTextForRange(Range.fromPoints(
tokenCursor.currentPosition(),
endOfDecl.currentPosition())).trim();
// And the first context
context.add(initialData, initialDataType, initialNumCommas);
// Get the rest of the single-bracket contexts for completions as well
String assocData;
int dataType;
int numCommas;
while (true)
{
int commaCount = tokenCursor.findOpeningBracketCountCommas("[", false);
if (commaCount == -1)
break;
numCommas = commaCount;
TokenCursor declEnd = tokenCursor.cloneCursor();
if (!tokenCursor.moveToPreviousToken())
return context;
if (tokenCursor.currentValue() == "[")
{
if (!declEnd.moveToPreviousToken())
return context;
dataType = AutocompletionContext.TYPE_DOUBLE_BRACKET;
if (!tokenCursor.moveToPreviousToken())
return context;
}
else
{
dataType = AutocompletionContext.TYPE_SINGLE_BRACKET;
}
tokenCursor.findStartOfEvaluationContext();
assocData =
docDisplay_.getTextForRange(Range.fromPoints(
tokenCursor.currentPosition(),
declEnd.currentPosition())).trim();
context.add(assocData, dataType, numCommas);
}
return context;
}
private void showSnippetHelp(QualifiedName item,
CompletionPopupDisplay popup)
{
popup.displaySnippetHelp(
snippets_.getSnippetContents(item.name));
}
/**
* It's important that we create a new instance of this each time.
* It maintains state that is associated with a completion request.
*/
private final class CompletionRequestContext extends
ServerRequestCallback<CompletionResult>
{
public CompletionRequestContext(Invalidation.Token token,
InputEditorSelection selection,
boolean canAutoAccept)
{
invalidationToken_ = token ;
selection_ = selection ;
canAutoAccept_ = canAutoAccept;
}
public void showHelp(QualifiedName selectedItem)
{
if (selectedItem.type == RCompletionType.SNIPPET)
showSnippetHelp(selectedItem, popup_);
else
helpStrategy_.showHelp(selectedItem, popup_);
}
public void showHelpTopic()
{
QualifiedName selectedItem = popup_.getSelectedValue();
// TODO: Show help should navigate to snippet file?
if (selectedItem.type != RCompletionType.SNIPPET)
helpStrategy_.showHelpTopic(selectedItem);
}
@Override
public void onError(ServerError error)
{
if (invalidationToken_.isInvalid())
return ;
RCompletionManager.this.popup_.showErrorMessage(
error.getUserMessage(),
new PopupPositioner(input_.getCursorBounds(), popup_)) ;
}
@Override
public void onResponseReceived(CompletionResult completions)
{
if (invalidationToken_.isInvalid())
return ;
// Only display the top completions
final QualifiedName[] results =
completions.completions.toArray(new QualifiedName[0]);
if (results.length == 0)
{
popup_.clearCompletions();
boolean lastInputWasTab =
(nativeEvent_ != null && nativeEvent_.getKeyCode() == KeyCodes.KEY_TAB);
boolean lineIsWhitespace = docDisplay_.getCurrentLine().matches("^\\s*$");
if (lastInputWasTab && lineIsWhitespace)
{
docDisplay_.insertCode("\t");
return;
}
if (canAutoAccept_)
{
popup_.showErrorMessage(
"(No matches)",
new PopupPositioner(input_.getCursorBounds(), popup_));
}
else
{
// Show an empty popup message offscreen -- this is a hack to
// ensure that we can get completion results on backspace after a
// failed completion, e.g. 'stats::rna' -> 'stats::rn'
popup_.placeOffscreen();
}
return ;
}
// If there is only one result and the name is identical to the
// current token, then implicitly accept that completion. we hide
// the popup to ensure that backspace can re-load completions from
// the cache
if (results.length == 1 &&
completions.token.equals(results[0].name.replaceAll(":*", "")))
{
// For snippets we need to apply the completion if explicitly requested
if (results[0].type == RCompletionType.SNIPPET && canAutoAccept_)
{
snippets_.applySnippet(completions.token, results[0].name);
return;
}
popup_.placeOffscreen();
return;
}
// Move range to beginning of token; we want to place the popup there.
final String token = completions.token ;
Rectangle rect = input_.getPositionBounds(
selection_.getStart().movePosition(-token.length(), true));
token_ = token;
suggestOnAccept_ = completions.suggestOnAccept;
overrideInsertParens_ = completions.dontInsertParens;
if (results.length == 1
&& canAutoAccept_
&& results[0].type != RCompletionType.DIRECTORY)
{
onSelection(results[0]);
}
else
{
popup_.showCompletionValues(
results,
new PopupPositioner(rect, popup_),
false);
}
}
private void onSelection(QualifiedName qname)
{
suggestTimer_.cancel();
final String value = qname.name ;
if (invalidationToken_.isInvalid())
return;
requester_.flushCache() ;
helpStrategy_.clearCache();
if (value == null)
{
assert false : "Selected comp value is null" ;
return ;
}
applyValue(qname);
// For in-line edits, we don't want to auto-popup after replacement
if (suggestOnAccept_ ||
(qname.name.endsWith(":") &&
docDisplay_.getCharacterAtCursor() != ':'))
{
Scheduler.get().scheduleDeferred(new ScheduledCommand()
{
@Override
public void execute()
{
beginSuggest(true, true, false);
}
});
}
else
{
popup_.hide() ;
popup_.clearHelp(false);
popup_.setHelpVisible(false);
docDisplay_.setFocus(true);
}
}
// For input of the form 'something$foo' or 'something@bar', quote the
// element following '@' if it's a non-syntactic R symbol; otherwise
// return as is
private String quoteIfNotSyntacticNameCompletion(String string)
{
if (RegexUtil.isSyntacticRIdentifier(string))
return string;
else
return "`" + string + "`";
}
private void applyValueRmdOption(final String value)
{
suggestTimer_.cancel();
// If there is no token but spaces have been inserted, then compensate
// for that. This is necessary as we allow for spaces in the completion,
// and completions auto-popup after ',' so e.g. on
//
// ```{r, |}
// ^ -- automatically triggered completion
// ^ -- user inserted spaces
//
// if we accept a completion in that position, we should keep the
// spaces the user inserted. (After the user has inserted a character,
// it becomes part of the token and hence this is unnecessary.
if (token_ == "")
{
int startPos = selection_.getStart().getPosition();
String currentLine = docDisplay_.getCurrentLine();
while (startPos < currentLine.length() &&
currentLine.charAt(startPos) == ' ')
++startPos;
input_.setSelection(new InputEditorSelection(
selection_.getStart().movePosition(startPos, false),
input_.getSelection().getEnd()));
}
else
{
input_.setSelection(new InputEditorSelection(
selection_.getStart().movePosition(-token_.length(), true),
input_.getSelection().getEnd()));
}
input_.replaceSelection(value, true);
token_ = value;
selection_ = input_.getSelection();
}
private void applyValue(final QualifiedName qualifiedName)
{
String completionToken = getCurrentCompletionToken();
// Strip off the quotes for string completions.
if (completionToken.startsWith("'") || completionToken.startsWith("\""))
completionToken = completionToken.substring(1);
if (qualifiedName.source.equals("`chunk-option`"))
{
applyValueRmdOption(qualifiedName.name);
return;
}
if (qualifiedName.type == RCompletionType.SNIPPET)
{
snippets_.applySnippet(completionToken, qualifiedName.name);
return;
}
boolean insertParen =
uiPrefs_.insertParensAfterFunctionCompletion().getValue() &&
RCompletionType.isFunctionType(qualifiedName.type);
// Don't insert a paren if there is already a '(' following
// the cursor
AceEditor editor = (AceEditor) input_;
boolean textFollowingCursorIsOpenParen = false;
boolean textFollowingCursorIsClosingParen = false;
boolean textFollowingCursorIsColon = false;
if (editor != null)
{
TokenCursor cursor =
editor.getSession().getMode().getRCodeModel().getTokenCursor();
cursor.moveToPosition(editor.getCursorPosition());
if (cursor.moveToNextToken())
{
textFollowingCursorIsOpenParen =
cursor.currentValue() == "(";
textFollowingCursorIsClosingParen =
cursor.currentValue() == ")" && !cursor.bwdToMatchingToken();
textFollowingCursorIsColon =
cursor.currentValue() == ":" ||
cursor.currentValue() == "::" ||
cursor.currentValue() == ":::";
}
}
String value = qualifiedName.name;
String source = qualifiedName.source;
boolean shouldQuote = qualifiedName.shouldQuote;
// Don't insert the `::` following a package completion if there is
// already a `:` following the cursor
if (textFollowingCursorIsColon)
value = value.replaceAll(":", "");
if (qualifiedName.type == RCompletionType.DIRECTORY)
value = value + "/";
if (!RCompletionType.isFileType(qualifiedName.type))
{
if (value == ":=")
value = quoteIfNotSyntacticNameCompletion(value);
else if (!value.matches(".*[=:]\\s*$") &&
!value.matches("^\\s*([`'\"]).*\\1\\s*$") &&
source != "<file>" &&
source != "<directory>" &&
source != "`chunk-option`" &&
!value.startsWith("@") &&
!shouldQuote)
value = quoteIfNotSyntacticNameCompletion(value);
}
/* In some cases, applyValue can be called more than once
* as part of the same completion instance--specifically,
* if there's only one completion candidate and it is in
* a package. To make sure that the selection movement
* logic works the second time, we need to reset the
* selection.
*/
// There might be multiple cursors. Get the position of each cursor.
Range[] ranges = editor.getNativeSelection().getAllRanges();
// Determine the replacement value.
boolean shouldInsertParens = insertParen &&
!overrideInsertParens_ &&
!textFollowingCursorIsOpenParen;
boolean insertMatching = uiPrefs_.insertMatching().getValue();
boolean needToMoveCursorInsideParens = false;
if (shouldInsertParens)
{
// Munge the value -- determine whether we want to append '()'
// for e.g. function completions, and so on.
if (textFollowingCursorIsClosingParen || !insertMatching)
{
value = value + "(";
}
else
{
value = value + "()";
needToMoveCursorInsideParens = true;
}
}
else
{
if (shouldQuote)
value = "\"" + value + "\"";
// don't add spaces around equals if requested
final String kSpaceEquals = " = ";
if (!uiPrefs_.insertSpacesAroundEquals().getValue() &&
value.endsWith(kSpaceEquals))
{
value = value.substring(0, value.length() - kSpaceEquals.length()) + "=";
}
}
// Loop over all of the active cursors, and replace.
for (Range range : ranges)
{
// We should be typing, and so each range should just define a
// cursor position. Take those positions, construct ranges, replace
// text in those ranges, and proceed.
Position replaceEnd = range.getEnd();
Position replaceStart = Position.create(
replaceEnd.getRow(),
replaceEnd.getColumn() - completionToken.length());
editor.replaceRange(
Range.fromPoints(replaceStart, replaceEnd),
value);
}
// Set the active selection, and update the token.
token_ = value;
selection_ = input_.getSelection();
// Move the cursor(s) back inside parens if necessary.
if (needToMoveCursorInsideParens)
editor.moveCursorLeft();
if (RCompletionType.isFunctionType(qualifiedName.type))
sigTipManager_.displayToolTip(qualifiedName.name,
qualifiedName.source,
qualifiedName.helpHandler);
}
private final Invalidation.Token invalidationToken_ ;
private InputEditorSelection selection_ ;
private final boolean canAutoAccept_;
private boolean suggestOnAccept_;
private boolean overrideInsertParens_;
}
private String getSourceDocumentPath()
{
if (rContext_ == null)
return "";
else
return StringUtil.notNull(rContext_.getPath());
}
private String getSourceDocumentId()
{
if (rContext_ != null)
return StringUtil.notNull(rContext_.getId());
else
return "";
}
public void showHelpDeferred(final CompletionRequestContext context,
final QualifiedName item,
int milliseconds)
{
if (helpRequest_ != null && helpRequest_.isRunning())
helpRequest_.cancel();
helpRequest_ = new Timer() {
@Override
public void run()
{
if (item.equals(lastSelectedItem_) && popup_.isShowing())
context.showHelp(item);
}
};
helpRequest_.schedule(milliseconds);
}
String getCurrentCompletionToken()
{
AceEditor editor = (AceEditor) docDisplay_;
if (editor == null)
return "";
// TODO: Better handling of completions within markdown mode, e.g.
// `r foo`
if (DocumentMode.isCursorInMarkdownMode(docDisplay_))
return token_;
Position cursorPos = editor.getCursorPosition();
Token currentToken = editor.getSession().getTokenAt(cursorPos);
if (currentToken == null)
return "";
// If the user has inserted some spaces, the cursor might now lie
// on a 'text' token. In that case, find the previous token and
// use that for completion.
String suffix = "";
if (currentToken.getValue().trim().isEmpty())
{
suffix = currentToken.getValue();
TokenIterator it = editor.createTokenIterator();
it.moveToPosition(cursorPos);
Token token = it.stepBackward();
if (token != null)
currentToken = token;
}
// Exclude non-string and non-identifier tokens.
if (currentToken.hasType("operator", "comment", "numeric", "text", "punctuation"))
return "";
String tokenValue = currentToken.getValue();
String subsetted = tokenValue.substring(0, cursorPos.getColumn() - currentToken.getColumn());
return subsetted + suffix;
}
private boolean isDisabled()
{
// Disable the completion manager while a snippet tabstop
// manager is active
if (docDisplay_.isSnippetsTabStopManagerActive())
return true;
return false;
}
private GlobalDisplay globalDisplay_;
private FileTypeRegistry fileTypeRegistry_;
private EventBus eventBus_;
private HelpStrategy helpStrategy_;
private UIPrefs uiPrefs_;
private final CodeToolsServerOperations server_;
private final InputEditorDisplay input_ ;
private final NavigableSourceEditor navigableSourceEditor_;
private final CompletionPopupDisplay popup_ ;
private final CompletionRequester requester_ ;
private final InitCompletionFilter initFilter_ ;
// Prevents completion popup from being dismissed when you merely
// click on it to scroll.
private boolean ignoreNextInputBlur_ = false;
private String token_ ;
private final DocDisplay docDisplay_;
private final SnippetHelper snippets_;
private final boolean isConsole_;
private final Invalidation invalidation_ = new Invalidation();
private CompletionRequestContext context_ ;
private final RCompletionContext rContext_;
private final RnwCompletionContext rnwContext_;
private final SignatureToolTipManager sigTipManager_;
private NativeEvent nativeEvent_;
private QualifiedName lastSelectedItem_;
private Timer helpRequest_;
private final SuggestionTimer suggestTimer_;
private static class SuggestionTimer
{
SuggestionTimer(RCompletionManager manager, UIPrefs uiPrefs)
{
manager_ = manager;
uiPrefs_ = uiPrefs;
timer_ = new Timer()
{
@Override
public void run()
{
manager_.beginSuggest(
flushCache_,
implicit_,
canAutoInsert_);
}
};
}
public void schedule(boolean flushCache,
boolean implicit,
boolean canAutoInsert)
{
flushCache_ = flushCache;
implicit_ = implicit;
canAutoInsert_ = canAutoInsert;
timer_.schedule(uiPrefs_.alwaysCompleteDelayMs().getValue());
}
public void cancel()
{
timer_.cancel();
}
private final RCompletionManager manager_;
private final UIPrefs uiPrefs_;
private final Timer timer_;
private boolean flushCache_;
private boolean implicit_;
private boolean canAutoInsert_;
}
private final HandlerRegistrations handlers_;
}