/* * CppCompletionRequest.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.CommandWithArg; import org.rstudio.core.client.Invalidation; import org.rstudio.core.client.regex.Match; import org.rstudio.core.client.regex.Pattern; import org.rstudio.studio.client.RStudioGinjector; import org.rstudio.studio.client.server.ServerError; import org.rstudio.studio.client.server.ServerRequestCallback; import org.rstudio.studio.client.workbench.prefs.model.UIPrefs; import org.rstudio.studio.client.workbench.snippets.SnippetHelper; import org.rstudio.studio.client.workbench.views.console.shell.editor.InputEditorSelection; import org.rstudio.studio.client.workbench.views.output.lint.model.LintItem; 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.ace.Position; import org.rstudio.studio.client.workbench.views.source.editors.text.ace.Range; import org.rstudio.studio.client.workbench.views.source.model.CppCompletion; import org.rstudio.studio.client.workbench.views.source.model.CppCompletionResult; import org.rstudio.studio.client.workbench.views.source.model.CppDiagnostic; import org.rstudio.studio.client.workbench.views.source.model.CppServerOperations; import java.util.ArrayList; 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.event.logical.shared.CloseEvent; import com.google.gwt.event.logical.shared.CloseHandler; import com.google.gwt.user.client.Command; import com.google.gwt.user.client.ui.PopupPanel; import com.google.inject.Inject; public class CppCompletionRequest extends ServerRequestCallback<CppCompletionResult> { public CppCompletionRequest(String docPath, String docId, CompletionPosition completionPosition, DocDisplay docDisplay, Invalidation.Token token, boolean explicit, CppCompletionManager manager, Command onTerminated) { RStudioGinjector.INSTANCE.injectMembers(this); docDisplay_ = docDisplay; completionPosition_ = completionPosition; invalidationToken_ = token; explicit_ = explicit; manager_ = manager; onTerminated_ = onTerminated; snippets_ = new SnippetHelper((AceEditor) docDisplay, docPath); // Get the current line (up to the cursor position) Position cursorPos = docDisplay_.getCursorPosition(); String line = docDisplay_.getLine(cursorPos.getRow()).substring(0, cursorPos.getColumn()); Position completionPos = completionPosition_.getPosition(); server_.getCppCompletions(line, docPath, docId, completionPos.getRow() + 1, completionPos.getColumn() + 1, completionPosition_.getUserText(), this); } @Inject void initialize(CppServerOperations server, UIPrefs uiPrefs) { server_ = server; uiPrefs_ = uiPrefs; } public boolean isExplicit() { return explicit_; } public CompletionPosition getCompletionPosition() { return completionPosition_; } public CppCompletionPopupMenu getCompletionPopup() { return popup_; } public void updateUI(boolean autoAccept) { if (invalidationToken_.isInvalid()) return; // if we don't have the completion list back from the server yet // then just ignore this (this function will get called again when // the request completes) if (completions_ == null) return; // discover text already entered String userTypedText = getUserTypedText(); // build list of entries (filter on text already entered) JsArray<CppCompletion> filtered = JsArray.createArray().cast(); for (int i = 0; i < completions_.length(); i++) { CppCompletion completion = completions_.get(i); String typedText = completion.getTypedText(); if ((userTypedText.length() == 0) || typedText.startsWith(userTypedText)) { // be more picky for member scope completions because clang // returns a bunch of noise like constructors, destructors, // compiler generated assignments, etc. if (completionPosition_.getScope() == CompletionPosition.Scope.Member) { if (completion.getType() == CppCompletion.VARIABLE || (completion.getType() == CppCompletion.FUNCTION && !typedText.startsWith("operator="))) { filtered.push(completion); } } else { filtered.push(completion); } } } // add in snippets if they are enabled and this is a global scope if (uiPrefs_.enableSnippets().getValue() && (completionPosition_.getScope() == CompletionPosition.Scope.Global)) { ArrayList<String> snippets = snippets_.getAvailableSnippets(); for (String snippet : snippets) if (snippet.startsWith(userTypedText)) { String content = snippets_.getSnippet(snippet).getContent(); content = content.replace("\t", " "); filtered.unshift(CppCompletion.createSnippetCompletion(snippet, content)); } } // check for no hits on explicit request if ((filtered.length() == 0) && explicit_) { showCompletionPopup("(No matches)"); } // check for auto-accept else if ((filtered.length() == 1) && autoAccept && explicit_) { applyValue(filtered.get(0)); } // check for one completion that's already present else if (filtered.length() == 1 && filtered.get(0).getTypedText() == getUserTypedText() && filtered.get(0).getType() != CppCompletion.SNIPPET) { terminate(); } else { showCompletionPopup(filtered); } } public void terminate() { closeCompletionPopup(); terminated_ = true; if (onTerminated_ != null) onTerminated_.execute(); } public boolean isTerminated() { return terminated_; } @Override public void onResponseReceived(CppCompletionResult result) { if (invalidationToken_.isInvalid()) return; // null result means that completion is not supported for this file if (result == null) return; // get the completions completions_ = result.getCompletions(); // update the UI updateUI(true); } static Pattern RE_NO_MEMBER_NAMED = Pattern.create("^no member named '(.*)' in '(.*)'$"); static Pattern RE_USE_UNDECLARED_IDENTIFIER = Pattern.create("^use of undeclared identifier '(.*)'"); private static Range createRangeFromMatch(CppDiagnostic diagnostic, Match match) { return Range.create( diagnostic.getPosition().getLine() - 1, diagnostic.getPosition().getColumn() - 1, diagnostic.getPosition().getLine() - 1, diagnostic.getPosition().getColumn() - 1 + match.getGroup(1).length()); } private static Range getRangeForSpecializedDiagnostic(CppDiagnostic diagnostic) { String message = diagnostic.getMessage(); Match match; match = RE_NO_MEMBER_NAMED.match(message, 0); if (match != null) return createRangeFromMatch(diagnostic, match); match = RE_USE_UNDECLARED_IDENTIFIER.match(message, 0); if (match != null) return createRangeFromMatch(diagnostic, match); return null; } private static Range getRangeForDiagnostic(CppDiagnostic diagnostic) { // Try to get a range for specialized diagnostics Range range; range = getRangeForSpecializedDiagnostic(diagnostic); if (range != null) return range; // Default range -- override if we infer a better range return Range.create( diagnostic.getPosition().getLine() - 1, diagnostic.getPosition().getColumn() - 1, diagnostic.getPosition().getLine() - 1, diagnostic.getPosition().getColumn()); } public static JsArray<LintItem> asLintArray( JsArray<CppDiagnostic> diagnostics) { JsArray<LintItem> lint = JsArray.createArray(diagnostics.length()).cast(); for (int i = 0; i < diagnostics.length(); i++) { CppDiagnostic d = diagnostics.get(i); if (d.getPosition() != null) { Range range = getRangeForDiagnostic(d); lint.set(i, LintItem.create( range.getStart().getRow(), range.getStart().getColumn(), range.getEnd().getRow(), range.getEnd().getColumn(), d.getMessage(), cppDiagnosticSeverityToLintType(d.getSeverity()))); } } return lint; } private static String cppDiagnosticSeverityToLintType(int type) { switch (type) { case CppDiagnostic.IGNORED: return "ignored"; case CppDiagnostic.NOTE: return "note"; case CppDiagnostic.WARNING: return "warning"; case CppDiagnostic.ERROR: return "error"; case CppDiagnostic.FATAL: return "fatal"; default: return ""; } } private void showCompletionPopup(String message) { if (popup_ == null) popup_ = createCompletionPopup(); popup_.setText(message); docDisplay_.setPopupVisible(true); } private void showCompletionPopup(JsArray<CppCompletion> completions) { // clear any existing signature tips if (completions.length() > 0) CppCompletionSignatureTip.hideAll(); if (popup_ == null) popup_ = createCompletionPopup(); popup_.setCompletions(completions, new CommandWithArg<CppCompletion>() { @Override public void execute(CppCompletion completion) { applyValue(completion); } }); // notify document of popup status docDisplay_.setPopupVisible(completions.length() > 0); } private CppCompletionPopupMenu createCompletionPopup() { CppCompletionPopupMenu popup = new CppCompletionPopupMenu( docDisplay_, completionPosition_); popup.addCloseHandler(new CloseHandler<PopupPanel>() { @Override public void onClose(CloseEvent<PopupPanel> event) { closeCompletionPopup(); terminated_ = true; Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { docDisplay_.setPopupVisible(false); } }); } }); return popup; } private void closeCompletionPopup() { if (popup_ != null) { popup_.hide(); popup_ = null; } } @Override public void onError(ServerError error) { if (invalidationToken_.isInvalid()) return; if (explicit_) showCompletionPopup(error.getUserMessage()); } private void applyValue(CppCompletion completion) { if (invalidationToken_.isInvalid()) return; terminate(); if (completion.getType() == CppCompletion.SNIPPET) { snippets_.applySnippet(getUserTypedText(), completion.getTypedText()); return; } String insertText = completion.getTypedText(); if (completion.getType() == CppCompletion.FUNCTION && uiPrefs_.insertParensAfterFunctionCompletion().getValue()) { if (uiPrefs_.insertMatching().getValue()) insertText = insertText + "()"; else insertText = insertText + "("; } else if (completion.getType() == CppCompletion.DIRECTORY) { insertText += "/"; Scheduler.get().scheduleDeferred(new ScheduledCommand() { @Override public void execute() { manager_.codeCompletion(); } }); } docDisplay_.setFocus(true); docDisplay_.setSelection(getReplacementSelection()); docDisplay_.replaceSelection(insertText, true); if (completion.hasParameters() && uiPrefs_.insertParensAfterFunctionCompletion().getValue() && uiPrefs_.insertMatching().getValue()) { Position pos = docDisplay_.getCursorPosition(); pos = Position.create(pos.getRow(), pos.getColumn() - 1); docDisplay_.setSelectionRange(Range.fromPoints(pos, pos)); } else if (completion.getType() == CppCompletion.FILE) { char ch = docDisplay_.getCharacterAtCursor(); // if there is a '>' or '"' following the cursor, move over it if (ch == '>' || ch == '"') { docDisplay_.moveCursorForward(); } // otherwise, insert the corresponding closing character else { String line = docDisplay_.getCurrentLine(); if (line.contains("<")) docDisplay_.insertCode(">"); else if (line.contains("\"")) docDisplay_.insertCode("\""); } } if (completion.hasParameters() && uiPrefs_.showSignatureTooltips().getValue()) { new CppCompletionSignatureTip(completion, docDisplay_); } } private InputEditorSelection getReplacementSelection() { return docDisplay_.createSelection(completionPosition_.getPosition(), docDisplay_.getCursorPosition()); } private String getUserTypedText() { return docDisplay_.getCode( completionPosition_.getPosition(), docDisplay_.getCursorPosition()); } private CppServerOperations server_; private UIPrefs uiPrefs_; private final DocDisplay docDisplay_; private final boolean explicit_; private final CppCompletionManager manager_; private final Invalidation.Token invalidationToken_; private final SnippetHelper snippets_; private final CompletionPosition completionPosition_; private CppCompletionPopupMenu popup_; private JsArray<CppCompletion> completions_; private boolean terminated_ = false; private Command onTerminated_ = null; }