/*=============================================================================# # Copyright (c) 2008-2016 Stephan Wahlbrink (WalWare.de) and others. # All rights reserved. This program and the accompanying materials # are made available under the terms of the Eclipse Public License v1.0 # which accompanies this distribution, and is available at # http://www.eclipse.org/legal/epl-v10.html # # Contributors: # Stephan Wahlbrink - initial API and implementation #=============================================================================*/ package de.walware.statet.r.internal.ui.editors; 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 de.walware.ecommons.ltk.IElementName; import de.walware.ecommons.ltk.ui.IElementLabelProvider; import de.walware.ecommons.ltk.ui.sourceediting.assist.AssistInvocationContext; import de.walware.ecommons.ltk.ui.sourceediting.assist.ElementNameCompletionProposal; import de.walware.ecommons.ltk.ui.sourceediting.assist.IInfoHover; import de.walware.ecommons.text.ui.BracketLevel.InBracketPosition; import de.walware.statet.nico.ui.console.InputSourceViewer; import de.walware.statet.r.core.IRCoreAccess; import de.walware.statet.r.core.RCodeStyleSettings; import de.walware.statet.r.core.RCore; import de.walware.statet.r.core.RSymbolComparator; import de.walware.statet.r.core.model.ArgsDefinition; import de.walware.statet.r.core.model.IRElement; import de.walware.statet.r.core.model.IRMethod; import de.walware.statet.r.core.model.RElementName; import de.walware.statet.r.core.pkgmanager.IRPkgInfo; import de.walware.statet.r.core.renv.IREnv; import de.walware.statet.r.core.rhelp.IREnvHelp; import de.walware.statet.r.core.rhelp.IRHelpManager; import de.walware.statet.r.core.source.RHeuristicTokenScanner; import de.walware.statet.r.internal.ui.rhelp.RHelpInfoHoverCreator; import de.walware.statet.r.internal.ui.rhelp.RHelpUIServlet; import de.walware.statet.r.ui.RUI; import de.walware.statet.r.ui.sourceediting.RAssistInvocationContext; import de.walware.statet.r.ui.sourceediting.RBracketLevel; 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 IElementName 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 IRPkgInfo pkgInfo; public RPkgProposal(final RAssistInvocationContext context, final IElementName replacementName, final int replacementOffset, final IRPkgInfo 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 IElementName 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 IElementName 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.PLUGIN_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 IElementName replacementName= getReplacementName(); final int mode= getMode(); final boolean assignmentFunction= (mode == FUNCTION) && replacementName.getNextSegment() == null && replacementName.getSegmentName().endsWith("<-"); //$NON-NLS-1$ final IElementName 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(IInfoHover.MODE_PROPOSAL_INFO); } return this.informationControlCreator; } @Override public Object getAdditionalProposalInfo(final IProgressMonitor monitor) { final IRHelpManager rHelpManager= RCore.getRHelpManager(); final int mode= getMode(); Object helpObject= null; switch (mode) { case PACKAGE_NAME: { final IElementName elementName= getReplacementName(); if (elementName.getType() == RElementName.SCOPE_PACKAGE) { final String pkgName= elementName.getSegmentName(); if (pkgName == null) { return null; } final IREnvHelp 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 IREnv rEnv= getRCoreAccess().getREnv(); final String pkgName= scope.getSegmentName(); final String topic= elementName.getSegmentName(); if (rEnv == null || pkgName == null || topic == null) { return null; } final IREnvHelp help= rHelpManager.getHelp(rEnv); if (help != null) { try { helpObject= help.getPageForTopic(pkgName, topic); } finally { help.unlock(); } } } break; } } if (Thread.interrupted() || helpObject == null) { return null; } { final String httpUrl= rHelpManager.toHttpUrl(helpObject, RHelpUIServlet.INFO_TARGET); if (httpUrl != null) { return new RHelpInfoHoverCreator.Data(getInvocationContext().getSourceViewer().getTextWidget(), helpObject, httpUrl ); } } return null; } }