blob: e223ff98b7bdca0587494917f88e84621aa9666f [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2008, 2020 Stephan Wahlbrink and others.
#
# This program and the accompanying materials are made available under the
# terms of the Eclipse Public License 2.0 which is available at
# https://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
# which is available at https://www.apache.org/licenses/LICENSE-2.0.
#
# SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
#
# Contributors:
# Stephan Wahlbrink <sw@wahlbrink.eu> - initial API and implementation
#=============================================================================*/
package org.eclipse.statet.internal.r.ui.editors;
import java.net.URI;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.jface.text.BadLocationException;
import org.eclipse.jface.text.IDocument;
import org.eclipse.jface.text.IInformationControlCreator;
import org.eclipse.jface.text.IRegion;
import org.eclipse.jface.text.contentassist.ICompletionProposalExtension5;
import org.eclipse.jface.text.link.LinkedModeModel;
import org.eclipse.jface.text.link.LinkedModeUI;
import org.eclipse.jface.text.link.LinkedPositionGroup;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.statet.jcommons.lang.NonNullByDefault;
import org.eclipse.statet.jcommons.lang.Nullable;
import org.eclipse.statet.jcommons.text.core.SearchPattern;
import org.eclipse.statet.ecommons.text.ui.BracketLevel.InBracketPosition;
import org.eclipse.statet.internal.r.ui.rhelp.RHelpInfoHoverCreator;
import org.eclipse.statet.internal.r.ui.rhelp.RHelpLtkUI;
import org.eclipse.statet.ltk.core.ElementName;
import org.eclipse.statet.ltk.ui.ElementLabelProvider;
import org.eclipse.statet.ltk.ui.sourceediting.assist.AssistInvocationContext;
import org.eclipse.statet.ltk.ui.sourceediting.assist.ElementNameCompletionProposal;
import org.eclipse.statet.ltk.ui.sourceediting.assist.InfoHover;
import org.eclipse.statet.nico.ui.console.InputSourceViewer;
import org.eclipse.statet.r.core.IRCoreAccess;
import org.eclipse.statet.r.core.RCodeStyleSettings;
import org.eclipse.statet.r.core.RCore;
import org.eclipse.statet.r.core.model.ArgsDefinition;
import org.eclipse.statet.r.core.model.IRElement;
import org.eclipse.statet.r.core.model.IRMethod;
import org.eclipse.statet.r.core.model.RElementName;
import org.eclipse.statet.r.core.source.RHeuristicTokenScanner;
import org.eclipse.statet.r.ui.RUI;
import org.eclipse.statet.r.ui.sourceediting.RAssistInvocationContext;
import org.eclipse.statet.r.ui.sourceediting.RBracketLevel;
import org.eclipse.statet.rhelp.core.REnvHelp;
import org.eclipse.statet.rhelp.core.RHelpManager;
import org.eclipse.statet.rhelp.core.RPkgHelp;
@NonNullByDefault
public class RElementCompletionProposal
extends ElementNameCompletionProposal<RAssistInvocationContext, IRElement>
implements ICompletionProposalExtension5 {
public static class RElementProposalParameters extends ProposalParameters<RAssistInvocationContext> {
public final ElementLabelProvider labelProvider;
public ElementName replacementName;
public IRElement element;
public RElementProposalParameters(final RAssistInvocationContext context, final int replacementOffset,
final SearchPattern namePattern, final int baseRelevance,
final ElementLabelProvider labelProvider) {
super(context, replacementOffset, namePattern, baseRelevance);
this.labelProvider= labelProvider;
}
public RElementProposalParameters(final RAssistInvocationContext context, final int replacementOffset,
final SearchPattern namePattern,
final ElementLabelProvider labelProvider) {
super(context, replacementOffset, namePattern);
this.labelProvider= labelProvider;
}
/** Only for context information */
public RElementProposalParameters(final RAssistInvocationContext context, final int replacementOffset,
final ElementLabelProvider labelProvider) {
super(context, replacementOffset, 0);
this.labelProvider= labelProvider;
}
}
protected static final int PACKAGE_NAME= 1;
protected static final int ARGUMENT_NAME= 2;
protected static final int FUNCTION= 3;
private static boolean rHelpInfoHoverInitialized;
private static @Nullable IInformationControlCreator rHelpInfoHoverCreator;
private static @Nullable IInformationControlCreator getRHelpInfoHoverCreator(final AssistInvocationContext context) {
if (!rHelpInfoHoverInitialized) {
final Shell shell= context.getSourceViewer().getTextWidget().getShell();
if (shell != null) {
if (RHelpInfoHoverCreator.isAvailable(shell)) {
rHelpInfoHoverCreator= new RHelpInfoHoverCreator(InfoHover.MODE_PROPOSAL_INFO);
}
rHelpInfoHoverInitialized= true;
}
}
return rHelpInfoHoverCreator;
}
public static class ArgumentProposal extends RElementCompletionProposal {
public ArgumentProposal(final RElementProposalParameters parameters) {
super(parameters);
}
@Override
protected int getMode() {
return ARGUMENT_NAME;
}
@Override
public String getDisplayString() {
return getReplacementName().getDisplayName();
}
@Override
public StyledString computeStyledText() {
return new StyledString(getReplacementName().getDisplayName());
}
@Override
public Image getImage() {
return RUI.getImage(RUI.IMG_OBJ_ARGUMENT_ASSIGN);
}
}
public static class ContextInformationProposal extends RElementCompletionProposal {
public ContextInformationProposal(final RElementProposalParameters parameters) {
super(parameters);
}
@Override
public boolean isAutoInsertable() {
return true;
}
@Override
protected void doApply(final char trigger, final int stateMask,
final int caretOffset, final int replacementOffset, final int replacementLength)
throws BadLocationException {
final ApplyData applyData= getApplyData();
applyData.clearSelection();
applyData.setContextInformation(new RArgumentListContextInformation(
getReplacementOffset(), // allow negative offsets
(IRMethod) getElement() ));
}
}
private static final boolean isFollowedByOpeningBracket(final int forwardOffset,
final RAssistInvocationContext context) {
final RHeuristicTokenScanner scanner= context.getRHeuristicTokenScanner();
scanner.configure(context.getDocument());
final int idx= scanner.findAnyNonBlankForward(forwardOffset, RHeuristicTokenScanner.UNBOUND, false);
return (idx >= 0
&& scanner.getChar() == '(' );
}
private static final boolean isClosedBracket(final int backwardOffset, final int forwardOffset,
final RAssistInvocationContext context) {
final int searchType= RHeuristicTokenScanner.ROUND_BRACKET_TYPE;
int[] balance= new int[3];
balance[searchType]++;
final RHeuristicTokenScanner scanner= context.getRHeuristicTokenScanner();
scanner.configureDefaultPartitions(context.getDocument());
balance= scanner.computeBracketBalance(backwardOffset, forwardOffset, balance, searchType);
return (balance[searchType] <= 0);
}
private static final boolean isFollowedByEqualAssign(final int forwardOffset,
final RAssistInvocationContext context) {
final RHeuristicTokenScanner scanner= context.getRHeuristicTokenScanner();
scanner.configure(context.getDocument());
final int idx= scanner.findAnyNonBlankForward(forwardOffset, RHeuristicTokenScanner.UNBOUND, false);
return (idx >= 0
&& scanner.getChar() == '=' );
}
private static final boolean isFollowedByAssign(final int forwardOffset,
final RAssistInvocationContext data) {
final RHeuristicTokenScanner scanner= data.getRHeuristicTokenScanner();
scanner.configure(data.getDocument());
final int idx= scanner.findAnyNonBlankForward(forwardOffset, RHeuristicTokenScanner.UNBOUND, false);
return (idx >= 0
&& (scanner.getChar() == '=' || scanner.getChar() == '<') );
}
public RElementCompletionProposal(final RElementProposalParameters parameters) {
super(parameters, parameters.replacementName, parameters.element, parameters.labelProvider);
}
protected IRCoreAccess getRCoreAccess() {
return getInvocationContext().getEditor().getRCoreAccess();
}
protected int getMode() {
return (getElement() != null
&& (getElement().getElementType() & IRElement.MASK_C1) == IRElement.C1_METHOD) ?
FUNCTION : 0;
}
@Override
protected int computeReplacementLength(final int replacementOffset, final Point selection, final int caretOffset, final boolean overwrite) {
// keep in synch with RSimpleCompletionProposal
final int end= Math.max(caretOffset, selection.x + selection.y);
if (overwrite) {
final RAssistInvocationContext context= getInvocationContext();
final RHeuristicTokenScanner scanner= context.getRHeuristicTokenScanner();
scanner.configure(context.getDocument());
final IRegion word= scanner.findRWord(end, false, true);
if (word != null) {
return (word.getOffset() + word.getLength() - replacementOffset);
}
}
return (end - replacementOffset);
}
@Override
protected @Nullable String getValidationPrefix(final int offset) throws BadLocationException {
// keep in synch with RSimpleCompletionProposal
final int startOffset= Math.max(getReplacementOffset(), 0);
if (offset >= startOffset) {
final RAssistInvocationContext context= getInvocationContext();
final IDocument document= context.getDocument();
int nameStartOffset= startOffset;
int nameEndOffset= offset;
if (nameEndOffset > nameStartOffset && document.getChar(nameStartOffset) == '`') {
nameStartOffset++;
}
if (nameEndOffset > nameStartOffset && document.getChar(nameEndOffset - 1) == '`') {
nameEndOffset--;
}
if (nameEndOffset >= nameStartOffset) {
return context.getIdentifierSegmentName(
document.get(startOffset, offset - startOffset) );
}
}
return null;
}
@Override
protected void doApply(final char trigger, final int stateMask, final int caretOffset,
final int replacementOffset, int replacementLength)
throws BadLocationException {
final RAssistInvocationContext context= getInvocationContext();
final IDocument document= context.getDocument();
final ApplyData applyData= getApplyData();
final ElementName replacementName= getReplacementName();
final int mode= getMode();
final boolean assignmentFunction= (mode == FUNCTION)
&& replacementName.getNextSegment() == null
&& replacementName.getSegmentName().endsWith("<-"); //$NON-NLS-1$
final ElementName elementName;
if (assignmentFunction) {
elementName= RElementName.create(RElementName.MAIN_DEFAULT,
replacementName.getSegmentName().substring(0, replacementName.getSegmentName().length() - 2) );
}
else {
elementName= replacementName;
}
final StringBuilder replacement= new StringBuilder((mode == PACKAGE_NAME) ?
elementName.getSegmentName() :
elementName.getDisplayName() );
int cursor= replacement.length();
if (replacementLength > 0 && document.getChar(replacementOffset) == '`' && replacement.charAt(0) != '`') {
if (replacement.length() == elementName.getSegmentName().length()
&& replacementOffset+replacementLength < document.getLength()
&& document.getChar(replacementOffset+replacementLength) == '`') {
replacementLength++;
}
replacement.insert(elementName.getSegmentName().length(), '`');
replacement.insert(0, '`');
cursor+= 2;
}
int subMode= 0;
int linkedMode= -1;
switch (mode) {
case FUNCTION:
subMode= 1;
final IRMethod rMethod= (IRMethod) getElement();
if (replacementOffset+replacementLength < document.getLength()-1
&& document.getChar(replacementOffset+replacementLength) == '(') {
cursor ++;
subMode= 10;
}
else if (!isFollowedByOpeningBracket(replacementOffset + replacementLength, context)) {
replacement.append('(');
cursor ++;
subMode= 11;
}
if (subMode >= 10) {
if (subMode == 11
&& !isClosedBracket(replacementOffset, replacementOffset + replacementLength, context)) {
replacement.append(')');
linkedMode= 2;
if (assignmentFunction && !isFollowedByAssign(replacementOffset + replacementLength, context)) {
replacement.append(" <- "); //$NON-NLS-1$
if (linkedMode >= 0) {
linkedMode+= 4;
}
}
}
final ArgsDefinition argsDef= rMethod.getArgsDefinition();
if (argsDef == null || argsDef.size() > 0 || (subMode == 11 && linkedMode < 0)) {
applyData.setContextInformation(new RArgumentListContextInformation(replacementOffset + cursor, rMethod));
}
else {
cursor ++;
linkedMode= -1;
}
}
break;
case ARGUMENT_NAME:
if (!isFollowedByEqualAssign(replacementOffset+replacementLength, context)) {
final RCodeStyleSettings codeStyle= getRCoreAccess().getRCodeStyle();
final String argAssign= codeStyle.getArgAssignString();
replacement.append(argAssign);
cursor+= argAssign.length();
}
break;
}
document.replace(replacementOffset, replacementLength, replacement.toString());
applyData.setSelection(replacementOffset + cursor);
if (linkedMode >= 0) {
createLinkedMode(replacementOffset + cursor - 1, linkedMode).enter();
}
}
private LinkedModeUI createLinkedMode(final int offset, final int mode)
throws BadLocationException {
final AssistInvocationContext context= getInvocationContext();
final IDocument document= context.getDocument();
final LinkedModeModel model= new LinkedModeModel();
int pos= 0;
final LinkedPositionGroup group= new LinkedPositionGroup();
final InBracketPosition position= RBracketLevel.createPosition('(', document,
offset + 1, 0, pos++);
group.addPosition(position);
model.addGroup(group);
model.forceInstall();
final RBracketLevel level= new RBracketLevel(model,
document, context.getEditor().getDocumentContentInfo(),
position, (context.getSourceViewer() instanceof InputSourceViewer), true);
/* create UI */
final LinkedModeUI ui= new LinkedModeUI(model, context.getSourceViewer());
ui.setCyclingMode(LinkedModeUI.CYCLE_NEVER);
ui.setExitPosition(context.getSourceViewer(), offset + (mode & 0xff), 0, pos);
ui.setSimpleMode(true);
ui.setExitPolicy(level);
return ui;
}
@Override
public IInformationControlCreator getInformationControlCreator() {
return getRHelpInfoHoverCreator(getInvocationContext());
}
@Override
public @Nullable Object getAdditionalProposalInfo(final IProgressMonitor monitor) {
final RHelpManager rHelpManager= RCore.getRHelpManager();
final int mode= getMode();
Object helpObject= null;
switch (mode) {
case PACKAGE_NAME: {
final ElementName elementName= getReplacementName();
if (elementName.getType() == RElementName.SCOPE_PACKAGE) {
final String pkgName= elementName.getSegmentName();
if (pkgName == null) {
return null;
}
final REnvHelp help= rHelpManager.getHelp(getRCoreAccess().getREnv());
if (help != null) {
try {
helpObject= help.getPkgHelp(pkgName);
}
finally {
help.unlock();
}
}
}
break;
}
default: {
final IRElement element= getElement();
if (element == null) {
return null;
}
final RElementName elementName= element.getElementName();
if (elementName.getType() == RElementName.MAIN_DEFAULT) {
RElementName scope= elementName.getScope();
if (scope == null && (element.getModelParent() instanceof IRElement)) {
scope= element.getModelParent().getElementName();
}
if (scope == null || !RElementName.isPackageFacetScopeType(scope.getType())) {
return null;
}
final String pkgName= scope.getSegmentName();
final String topic= elementName.getSegmentName();
if (pkgName == null || topic == null) {
return null;
}
final REnvHelp help= rHelpManager.getHelp(getRCoreAccess().getREnv());
if (help != null) {
try {
final RPkgHelp pkgHelp= help.getPkgHelp(pkgName);
if (pkgHelp != null) {
helpObject= pkgHelp.getPageForTopic(topic);
}
}
finally {
help.unlock();
}
}
}
break;
}
}
if (Thread.interrupted() || helpObject == null) {
return null;
}
{ final URI url= RCore.getRHelpHttpService().toHttpUrl(helpObject,
RHelpLtkUI.INFO_TARGET );
if (url != null) {
return new RHelpInfoHoverCreator.Data(getInvocationContext().getSourceViewer().getTextWidget(),
helpObject, url );
}
}
return null;
}
}