blob: db5e011beb432c4c248af2a730f17dce104f790f [file] [log] [blame]
/*=============================================================================#
# Copyright (c) 2008, 2019 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.DocumentEvent;
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.contentassist.IContextInformation;
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.text.source.SourceViewer;
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.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.IElementLabelProvider;
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.RSymbolComparator;
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;
import org.eclipse.statet.rj.renv.core.RPkgBuilt;
public class RElementCompletionProposal extends ElementNameCompletionProposal<IRElement>
implements ICompletionProposalExtension5 {
private static final int PACKAGE_NAME= 1;
private static final int ARGUMENT_NAME= 2;
private static final int FUNCTION= 3;
public static class ArgumentProposal extends RElementCompletionProposal {
public ArgumentProposal(final RAssistInvocationContext context,
final ElementName replacementName, final int replacementOffset,
final IRElement element, final int relevance, final IElementLabelProvider labelProvider) {
super(context, replacementName, replacementOffset, element, relevance, labelProvider);
}
@Override
public Image getImage() {
return RUI.getImage(RUI.IMG_OBJ_ARGUMENT_ASSIGN);
}
@Override
public String getDisplayString() {
return getReplacementName().getDisplayName();
}
@Override
public StyledString getStyledDisplayString() {
return new StyledString(getReplacementName().getDisplayName());
}
@Override
protected int getMode() {
return ARGUMENT_NAME;
}
}
public static class RPkgProposal extends RElementCompletionProposal {
private final RPkgBuilt pkgInfo;
public RPkgProposal(final RAssistInvocationContext context,
final ElementName replacementName, final int replacementOffset,
final RPkgBuilt pkgInfo, final int relevance) {
super(context, replacementName, replacementOffset, null, relevance, null);
this.pkgInfo= pkgInfo;
}
@Override
public Image getImage() {
return RUI.getImage(RUI.IMG_OBJ_R_PACKAGE);
}
@Override
public String getDisplayString() {
return getReplacementName().getSegmentName();
}
@Override
public StyledString getStyledDisplayString() {
final StyledString s= new StyledString(getReplacementName().getSegmentName());
if (this.pkgInfo != null) {
s.append(QUALIFIER_SEPARATOR, StyledString.QUALIFIER_STYLER);
s.append(this.pkgInfo.getTitle(), StyledString.QUALIFIER_STYLER);
}
return s;
}
@Override
protected int getMode() {
return PACKAGE_NAME;
}
}
public static class ContextInformationProposal extends RElementCompletionProposal {
public ContextInformationProposal(final RAssistInvocationContext context,
final ElementName elementName, final int replacementOffset,
final IRElement element, final int relevance,
final IElementLabelProvider labelProvider) {
super(context, elementName, replacementOffset, element, relevance, labelProvider);
}
@Override
public boolean validate(final IDocument document, final int offset, final DocumentEvent event) {
return (offset == getInvocationContext().getInvocationOffset());
}
@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 data= getApplyData();
setCursorPosition(-1);
data.setContextInformation(new RArgumentListContextInformation(replacementOffset,
(IRMethod) getElement() ));
}
}
static final class ApplyData {
private final RAssistInvocationContext context;
private final SourceViewer viewer;
private final IDocument document;
private IContextInformation contextInformation;
ApplyData(final RAssistInvocationContext context) {
this.context= context;
this.viewer= context.getSourceViewer();
this.document= this.viewer.getDocument();
}
public SourceViewer getViewer() {
return this.viewer;
}
public IDocument getDocument() {
return this.document;
}
public RHeuristicTokenScanner getScanner() {
return this.context.getRHeuristicTokenScanner();
}
public void setContextInformation(final IContextInformation info) {
this.contextInformation= info;
}
public IContextInformation getContextInformation() {
return this.contextInformation;
}
}
private static final boolean isFollowedByOpeningBracket(final ApplyData util, final int forwardOffset) {
final RHeuristicTokenScanner scanner= util.getScanner();
scanner.configure(util.getDocument());
final int idx= scanner.findAnyNonBlankForward(forwardOffset, RHeuristicTokenScanner.UNBOUND, false);
return (idx >= 0
&& scanner.getChar() == '(' );
}
private static final boolean isClosedBracket(final ApplyData data, final int backwardOffset, final int forwardOffset) {
final int searchType= RHeuristicTokenScanner.ROUND_BRACKET_TYPE;
int[] balance= new int[3];
balance[searchType]++;
final RHeuristicTokenScanner scanner= data.getScanner();
scanner.configureDefaultParitions(data.getDocument());
balance= scanner.computeBracketBalance(backwardOffset, forwardOffset, balance, searchType);
return (balance[searchType] <= 0);
}
private static final boolean isFollowedByEqualAssign(final ApplyData data, final int forwardOffset) {
final RHeuristicTokenScanner scanner= data.getScanner();
scanner.configure(data.getDocument());
final int idx= scanner.findAnyNonBlankForward(forwardOffset, RHeuristicTokenScanner.UNBOUND, false);
return (idx >= 0
&& scanner.getChar() == '=' );
}
private static final boolean isFollowedByAssign(final ApplyData util, final int forwardOffset) {
final RHeuristicTokenScanner scanner= util.getScanner();
scanner.configure(util.getDocument());
final int idx= scanner.findAnyNonBlankForward(forwardOffset, RHeuristicTokenScanner.UNBOUND, false);
return (idx >= 0
&& (scanner.getChar() == '=' || scanner.getChar() == '<') );
}
private ApplyData applyData;
private IInformationControlCreator informationControlCreator;
public RElementCompletionProposal(final RAssistInvocationContext context, final ElementName elementName,
final int replacementOffset, final IRElement element,
final int relevance, final IElementLabelProvider labelProvider) {
super(context, elementName, replacementOffset, element, relevance, labelProvider);
}
@Override
protected String getPluginId() {
return RUI.BUNDLE_ID;
}
@Override
public RAssistInvocationContext getInvocationContext() {
return (RAssistInvocationContext) super.getInvocationContext();
}
protected final ApplyData getApplyData() {
if (this.applyData == null) {
this.applyData= new ApplyData(getInvocationContext());
}
return this.applyData;
}
protected IRCoreAccess getRCoreAccess() {
return getInvocationContext().getEditor().getRCoreAccess();
}
@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 ApplyData data= getApplyData();
final RHeuristicTokenScanner scanner= data.getScanner();
scanner.configure(data.getDocument());
final IRegion word= scanner.findRWord(end, false, true);
if (word != null) {
return (word.getOffset() + word.getLength() - replacementOffset);
}
}
return (end - replacementOffset);
}
@Override
public boolean validate(final IDocument document, final int offset, final DocumentEvent event) {
// keep in synch with RSimpleCompletionProposal
try {
int start= getReplacementOffset();
int length= offset - start;
if (length > 0 && document.getChar(start) == '`') {
start++;
length--;
}
if (length > 0 && document.getChar(start+length-1) == '`') {
length--;
}
final String prefix= document.get(start, length);
final String replacement= getReplacementName().getSegmentName();
if (new RSymbolComparator.PrefixPattern(prefix).matches(replacement)) {
return true;
}
}
catch (final BadLocationException e) {
// ignore concurrently modified document
}
return false;
}
@Override
protected void doApply(final char trigger, final int stateMask, final int caretOffset,
final int replacementOffset, int replacementLength) throws BadLocationException {
final ApplyData data= getApplyData();
final IDocument document= data.getDocument();
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(data, replacementOffset+replacementLength)) {
replacement.append('(');
cursor ++;
subMode= 11;
}
if (subMode >= 10) {
if (subMode == 11
&& !isClosedBracket(data, replacementOffset, replacementOffset+replacementLength)) {
replacement.append(')');
linkedMode= 2;
if (assignmentFunction && !isFollowedByAssign(data, replacementOffset+replacementLength)) {
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)) {
data.setContextInformation(new RArgumentListContextInformation(replacementOffset + cursor, rMethod));
}
else {
cursor ++;
linkedMode= -1;
}
}
break;
case ARGUMENT_NAME:
if (!isFollowedByEqualAssign(data, replacementOffset+replacementLength)) {
final RCodeStyleSettings codeStyle= getRCoreAccess().getRCodeStyle();
final String argAssign= codeStyle.getArgAssignString();
replacement.append(argAssign);
cursor += argAssign.length();
}
break;
}
document.replace(replacementOffset, replacementLength, replacement.toString());
setCursorPosition(replacementOffset + cursor);
if (linkedMode >= 0) {
createLinkedMode(data, replacementOffset + cursor - 1, linkedMode).enter();
}
}
private LinkedModeUI createLinkedMode(final ApplyData util, final int offset, final int mode)
throws BadLocationException {
final AssistInvocationContext context= getInvocationContext();
final LinkedModeModel model= new LinkedModeModel();
int pos= 0;
final LinkedPositionGroup group= new LinkedPositionGroup();
final InBracketPosition position= RBracketLevel.createPosition('(', util.getDocument(),
offset + 1, 0, pos++);
group.addPosition(position);
model.addGroup(group);
model.forceInstall();
final RBracketLevel level= new RBracketLevel(model,
util.getDocument(), context.getEditor().getDocumentContentInfo(),
position, (util.getViewer() instanceof InputSourceViewer), true);
/* create UI */
final LinkedModeUI ui= new LinkedModeUI(model, util.getViewer());
ui.setCyclingMode(LinkedModeUI.CYCLE_NEVER);
ui.setExitPosition(util.getViewer(), offset + (mode & 0xff), 0, pos);
ui.setSimpleMode(true);
ui.setExitPolicy(level);
return ui;
}
protected int getMode() {
return (getElement() != null
&& (getElement().getElementType() & IRElement.MASK_C1) == IRElement.C1_METHOD) ?
FUNCTION : 0;
}
@Override
public IContextInformation getContextInformation() {
return getApplyData().getContextInformation();
}
@Override
public IInformationControlCreator getInformationControlCreator() {
final Shell shell= getInvocationContext().getSourceViewer().getTextWidget().getShell();
if (shell == null || !RHelpInfoHoverCreator.isAvailable(shell)) {
return null;
}
if (this.informationControlCreator == null) {
this.informationControlCreator= new RHelpInfoHoverCreator(InfoHover.MODE_PROPOSAL_INFO);
}
return this.informationControlCreator;
}
@Override
public 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;
}
}