| /*=============================================================================# |
| # 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.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(replacementOffset, |
| (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.configureDefaultParitions(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= getReplacementOffset(); |
| 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; |
| } |
| |
| } |