/* * CppCompletionManager.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.source.editors.text.cpp; import org.rstudio.core.client.CommandWith2Args; import org.rstudio.core.client.HandlerRegistrations; import org.rstudio.core.client.Invalidation; import org.rstudio.core.client.command.KeyboardHelper; import org.rstudio.core.client.command.KeyboardShortcut; import org.rstudio.studio.client.RStudioGinjector; import org.rstudio.studio.client.common.filetypes.DocumentMode; import org.rstudio.studio.client.common.filetypes.FileTypeRegistry; 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.shell.assist.CompletionManager; import org.rstudio.studio.client.workbench.views.console.shell.assist.CompletionUtils; import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorSelection; 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.events.PasteEvent; import org.rstudio.studio.client.workbench.views.source.model.CppServerOperations; import org.rstudio.studio.client.workbench.views.source.model.CppSourceLocation; 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.ClickEvent; import com.google.gwt.event.dom.client.ClickHandler; import com.google.gwt.event.dom.client.KeyCodes; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.Timer; import com.google.inject.Inject; public class CppCompletionManager implements CompletionManager { public void onPaste(PasteEvent event) { hideCompletionPopup(); if (rCompletionManager_ != null) rCompletionManager_.onPaste(event); } public CppCompletionManager(DocDisplay docDisplay, InitCompletionFilter initFilter, CppCompletionContext completionContext, CompletionManager rCompletionManager) { RStudioGinjector.INSTANCE.injectMembers(this); docDisplay_ = docDisplay; initFilter_ = initFilter; completionContext_ = completionContext; rCompletionManager_ = rCompletionManager; snippets_ = new SnippetHelper((AceEditor) docDisplay_, completionContext.getDocPath()); handlers_ = new HandlerRegistrations(); handlers_.add(docDisplay_.addClickHandler(new ClickHandler() { public void onClick(ClickEvent event) { terminateCompletionRequest(); } })); } @Inject void initialize(CppServerOperations server, FileTypeRegistry fileTypeRegistry, UIPrefs uiPrefs) { server_ = server; fileTypeRegistry_ = fileTypeRegistry; uiPrefs_ = uiPrefs; suggestionTimer_ = new SuggestionTimer(this, uiPrefs_); } // close the completion popup (if any) @Override public void close() { // delegate to R mode if necessary if (DocumentMode.isCursorInRMode(docDisplay_) || DocumentMode.isCursorInMarkdownMode(docDisplay_)) { rCompletionManager_.close(); } else { terminateCompletionRequest(); } } @Override public void detach() { handlers_.removeHandler(); snippets_.detach(); rCompletionManager_.detach(); } // perform completion at the current cursor location @Override public void codeCompletion() { // delegate to R mode if necessary if (DocumentMode.isCursorInRMode(docDisplay_) || DocumentMode.isCursorInMarkdownMode(docDisplay_)) { rCompletionManager_.codeCompletion(); } // check whether it's okay to do a completion else if (shouldComplete(null)) { suggestCompletions(true); } } // go to help at the current cursor location @Override public void goToHelp() { // delegate to R mode if necessary if (DocumentMode.isCursorInRMode(docDisplay_)) { rCompletionManager_.goToHelp(); } else { // no implementation here yet since we don't have access // to C/C++ help (we could implement this via using libclang // to parse doxygen though) } } // find the definition of the function at the current cursor location @Override public void goToFunctionDefinition() { // delegate to R mode if necessary if (DocumentMode.isCursorInRMode(docDisplay_)) { rCompletionManager_.goToFunctionDefinition(); } else { completionContext_.cppCompletionOperation(new CppCompletionOperation(){ @Override public void execute(String docPath, int line, int column) { server_.goToCppDefinition( docPath, line, column, new CppCompletionServerRequestCallback<CppSourceLocation>( "Finding definition...") { @Override public void onSuccess(CppSourceLocation loc) { if (loc != null) { fileTypeRegistry_.editFile(loc.getFile(), loc.getPosition()); } } }); } }); } } // return false to indicate key not handled @Override public boolean previewKeyDown(NativeEvent event) { suggestionTimer_.cancel(); // delegate to R mode if appropriate if (DocumentMode.isCursorInRMode(docDisplay_) || DocumentMode.isCursorInMarkdownMode(docDisplay_)) return rCompletionManager_.previewKeyDown(event); // if there is no completion request active then // check for a key-combo that triggers completion or // navigation / help int modifier = KeyboardShortcut.getModifierValue(event); if ((request_ == null) || request_.isTerminated()) { // check for user completion key combo if (CompletionUtils.isCompletionRequest(event, modifier) && shouldComplete(event)) { return suggestCompletions(true); } else if (event.getKeyCode() == KeyCodes.KEY_TAB && modifier == KeyboardShortcut.SHIFT) { return snippets_.attemptSnippetInsertion(true); } else if (event.getKeyCode() == 112 // F1 && modifier == KeyboardShortcut.NONE) { goToHelp(); return true; } else if (event.getKeyCode() == 113 // F2 && modifier == KeyboardShortcut.NONE) { goToFunctionDefinition(); return true; } else { return false; } } // otherwise handle keys within the completion popup else { // get the key code int keyCode = event.getKeyCode(); // bail on modifier keys if (KeyboardHelper.isModifierKey(keyCode)) return false; // if there is no popup then bail CppCompletionPopupMenu popup = getCompletionPopup(); if ((popup == null) || !popup.isVisible()) 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(); } } // backspace triggers completion if the popup is visible if (keyCode == KeyCodes.KEY_BACKSPACE) { deferredSuggestCompletions(false, false); return false; } // tab accepts the current selection (popup handles Enter) else if (keyCode == KeyCodes.KEY_TAB) { popup.acceptSelected(); return true; } // allow '.' when showing file completions else if (popup.getCompletionPosition().getScope() == CompletionPosition.Scope.File && KeyboardHelper.isPeriodKeycode(keyCode)) { return false; } // non c++ identifier keys (that aren't navigational) close the popup else if (!CppCompletionUtils.isCppIdentifierKey(event)) { terminateCompletionRequest(); return false; } // otherwise leave it alone else { return false; } } } // return false to indicate key not handled @Override public boolean previewKeyPress(char c) { suggestionTimer_.cancel(); // delegate to R mode if necessary if (DocumentMode.isCursorInRMode(docDisplay_) || DocumentMode.isCursorInMarkdownMode(docDisplay_)) { return rCompletionManager_.previewKeyPress(c); } else { // don't do implicit completions if the user has set completion to manual // (but always do them if the completion popup is visible) if (!uiPrefs_.codeComplete().getValue().equals(UIPrefsAccessor.COMPLETION_MANUAL) || isCompletionPopupVisible()) { deferredSuggestCompletions(false, true); } return false; } } private void deferredSuggestCompletions(final boolean explicit, final boolean canDelay) { Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { suggestCompletions(explicit, canDelay); } }); } private boolean suggestCompletions(final boolean explicit) { return suggestCompletions(explicit, false); } private boolean suggestCompletions(final boolean explicit, boolean canDelay) { suggestionTimer_.cancel(); // check for completions disabled if (!completionContext_.isCompletionEnabled()) return false; // check for no selection InputEditorSelection selection = docDisplay_.getSelection() ; if (selection == null) return false; // check for contiguous selection if (!docDisplay_.isSelectionCollapsed()) return false; // calculate explicit value for getting completion position (if a // previous request was explicit then count this as explicit) boolean positionExplicit = explicit || ((request_ != null) && request_.isExplicit()); // see if we even have a completion position boolean alwaysComplete = uiPrefs_.codeComplete().getValue().equals( UIPrefsAccessor.COMPLETION_ALWAYS); int autoChars = uiPrefs_.alwaysCompleteCharacters().getValue(); final CompletionPosition completionPosition = CppCompletionUtils.getCompletionPosition(docDisplay_, positionExplicit, alwaysComplete, autoChars); if (completionPosition == null) { terminateCompletionRequest(); return false; } else if ((request_ != null) && !request_.isTerminated() && request_.getCompletionPosition().isSupersetOf(completionPosition)) { request_.updateUI(false); } else if (canDelay && completionPosition.getScope() == CompletionPosition.Scope.Global) { suggestionTimer_.schedule(completionPosition); } else { performCompletionRequest(completionPosition, explicit); } return true; } private void performCompletionRequest( final CompletionPosition completionPosition, final boolean explicit) { terminateCompletionRequest(); final Invalidation.Token invalidationToken = completionRequestInvalidation_.getInvalidationToken(); completionContext_.withUpdatedDoc(new CommandWith2Args<String, String>() { @Override public void execute(String docPath, String docId) { if (invalidationToken.isInvalid()) return; request_ = new CppCompletionRequest( docPath, docId, completionPosition, docDisplay_, invalidationToken, explicit, CppCompletionManager.this, new Command() { @Override public void execute() { suggestionTimer_.cancel(); } }); } }); } private static class SuggestionTimer { SuggestionTimer(CppCompletionManager manager, UIPrefs uiPrefs) { manager_ = manager; uiPrefs_ = uiPrefs; timer_ = new Timer() { @Override public void run() { manager_.performCompletionRequest(completionPosition_, false); } }; } public void schedule(CompletionPosition completionPosition) { completionPosition_ = completionPosition; timer_.schedule(uiPrefs_.alwaysCompleteDelayMs().getValue()); } public void cancel() { timer_.cancel(); } private final CppCompletionManager manager_; private final UIPrefs uiPrefs_; private final Timer timer_; private CompletionPosition completionPosition_; } private CppCompletionPopupMenu getCompletionPopup() { CppCompletionPopupMenu popup = request_ != null ? request_.getCompletionPopup() : null; return popup; } private void hideCompletionPopup() { CppCompletionPopupMenu popup = getCompletionPopup(); if (popup != null) popup.hide(); } private boolean isCompletionPopupVisible() { CppCompletionPopupMenu popup = getCompletionPopup(); return (popup != null) && popup.isVisible(); } private void terminateCompletionRequest() { suggestionTimer_.cancel(); completionRequestInvalidation_.invalidate(); if (request_ != null) { request_.terminate(); request_ = null; } } private boolean shouldComplete(NativeEvent event) { return initFilter_ == null || initFilter_.shouldComplete(event); } private CppServerOperations server_; private UIPrefs uiPrefs_; private FileTypeRegistry fileTypeRegistry_; private final DocDisplay docDisplay_; private final CppCompletionContext completionContext_; private CppCompletionRequest request_; private SuggestionTimer suggestionTimer_; private final InitCompletionFilter initFilter_ ; private final CompletionManager rCompletionManager_; private final Invalidation completionRequestInvalidation_ = new Invalidation(); private final SnippetHelper snippets_; private final HandlerRegistrations handlers_; }