| /*=============================================================================# |
| # 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.r.ui.sourceediting; |
| |
| import static org.eclipse.statet.ecommons.text.ITokenScanner.NOT_FOUND; |
| |
| import java.util.List; |
| |
| import org.eclipse.core.runtime.IProgressMonitor; |
| import org.eclipse.jface.text.AbstractDocument; |
| import org.eclipse.jface.text.BadLocationException; |
| import org.eclipse.jface.text.BadPartitioningException; |
| import org.eclipse.jface.text.IDocument; |
| import org.eclipse.jface.text.IRegion; |
| import org.eclipse.jface.text.ITypedRegion; |
| |
| import org.eclipse.statet.jcommons.collections.ImList; |
| import org.eclipse.statet.jcommons.lang.NonNullByDefault; |
| import org.eclipse.statet.jcommons.lang.Nullable; |
| import org.eclipse.statet.jcommons.text.core.TextRegion; |
| import org.eclipse.statet.jcommons.text.core.input.StringParserInput; |
| import org.eclipse.statet.jcommons.ts.core.Tool; |
| |
| import org.eclipse.statet.ecommons.text.core.FragmentDocument; |
| |
| import org.eclipse.statet.ltk.ast.core.AstInfo; |
| import org.eclipse.statet.ltk.ast.core.AstNode; |
| import org.eclipse.statet.ltk.ast.core.util.AstSelection; |
| import org.eclipse.statet.ltk.model.core.IModelManager; |
| import org.eclipse.statet.ltk.ui.sourceediting.ISourceEditor; |
| import org.eclipse.statet.ltk.ui.sourceediting.assist.AssistInvocationContext; |
| import org.eclipse.statet.nico.ui.NicoUITools; |
| import org.eclipse.statet.nico.ui.console.ConsolePageEditor; |
| import org.eclipse.statet.r.console.core.RProcess; |
| import org.eclipse.statet.r.console.core.util.LoadReferencesUtil; |
| import org.eclipse.statet.r.core.IRCoreAccess; |
| import org.eclipse.statet.r.core.data.CombinedRElement; |
| import org.eclipse.statet.r.core.model.IRSourceUnit; |
| import org.eclipse.statet.r.core.model.RElementAccess; |
| import org.eclipse.statet.r.core.model.RElementName; |
| import org.eclipse.statet.r.core.model.RModel; |
| import org.eclipse.statet.r.core.rlang.RTokens; |
| import org.eclipse.statet.r.core.rsource.RLexer; |
| import org.eclipse.statet.r.core.rsource.ast.FCall; |
| import org.eclipse.statet.r.core.rsource.ast.NodeType; |
| import org.eclipse.statet.r.core.rsource.ast.RAstNode; |
| import org.eclipse.statet.r.core.source.IRDocumentConstants; |
| import org.eclipse.statet.r.core.source.RHeuristicTokenScanner; |
| import org.eclipse.statet.r.ui.editors.IRSourceEditor; |
| |
| |
| /** |
| * AssistInvocationContext for R |
| */ |
| @NonNullByDefault |
| public class RAssistInvocationContext extends AssistInvocationContext { |
| |
| |
| public class FCallInfo { |
| |
| private final FCall node; |
| |
| private final RElementAccess access; |
| |
| private @Nullable RFrameSearchPath searchPath; |
| |
| |
| public FCallInfo(final FCall node, final RElementAccess access) { |
| this.node= node; |
| this.access= access; |
| } |
| |
| |
| public FCall getNode() { |
| return this.node; |
| } |
| |
| public RElementAccess getAccess() { |
| return this.access; |
| } |
| |
| private RFrameSearchPath createSearchPath(final int mode) { |
| final RFrameSearchPath searchPath= new RFrameSearchPath(); |
| final RAstNode parent= this.node.getRParent(); |
| searchPath.init(RAssistInvocationContext.this, (parent != null) ? parent : this.node, |
| mode, getAccess().getScope() ); |
| return searchPath; |
| } |
| |
| public RFrameSearchPath getSearchPath(final int mode) { |
| final int defaultMode= getDefaultRFrameSearchMode(); |
| if (mode == 0 || mode == defaultMode) { |
| @Nullable RFrameSearchPath searchPath= this.searchPath; |
| if (searchPath == null) { |
| searchPath= createSearchPath(defaultMode); |
| this.searchPath= searchPath; |
| } |
| return searchPath; |
| } |
| else { |
| return createSearchPath(mode); |
| } |
| } |
| |
| public int getArgIdx(final int offset) { |
| if (offset <= this.node.getArgsOpenOffset() |
| || (this.node.getArgsCloseOffset() != Integer.MIN_VALUE |
| && offset > this.node.getArgsCloseOffset() )) { |
| return -1; |
| } |
| final FCall.Args args= this.node.getArgsChild(); |
| final int last= args.getChildCount() - 1; |
| if (last < 0) { |
| return 0; |
| } |
| for (int argIdx= 0; argIdx < last; argIdx++) { |
| if (args.getSeparatorOffset(argIdx) >= offset) { |
| return argIdx; |
| } |
| } |
| return last; |
| } |
| |
| public int getArgBeginOffset(final int argIdx) { |
| if (argIdx < 0) { |
| return AstNode.NA_OFFSET; |
| } |
| final int sep= (argIdx == 0) ? |
| this.node.getArgsOpenOffset() : |
| this.node.getArgsChild().getSeparatorOffset(argIdx - 1); |
| return sep + 1; |
| } |
| |
| public FCall. @Nullable Arg getArg(final int argIdx) { |
| if (argIdx < 0) { |
| return null; |
| } |
| final FCall.Args args= this.node.getArgsChild(); |
| return (argIdx < args.getChildCount()) ? args.getChild(argIdx) : null; |
| } |
| |
| } |
| |
| |
| private static final byte PARSE_OPERATOR= 1 << 0; |
| private static final byte PARSE_SYMBOL= 1 << 1; |
| |
| private static final char[] F_BRACKETS= new char[] { '(', ')' }; |
| |
| |
| private @Nullable RHeuristicTokenScanner scanner; |
| private @Nullable RLexer lexer; |
| |
| private @Nullable RElementName prefixName; |
| |
| private int prefixLastSegmentOffset= -1; |
| |
| |
| private final @Nullable RProcess tool; |
| |
| private @Nullable LoadReferencesUtil toolReferencesUtil; |
| |
| |
| public RAssistInvocationContext(final IRSourceEditor editor, |
| final int offset, final String contentType, |
| final boolean isProposal, |
| final RHeuristicTokenScanner scanner, |
| final IProgressMonitor monitor) { |
| super(editor, offset, contentType, |
| (isProposal) ? IModelManager.MODEL_FILE : IModelManager.NONE, monitor ); |
| |
| this.scanner= scanner; |
| |
| this.tool= determineRProcess(); |
| } |
| |
| public RAssistInvocationContext(final IRSourceEditor editor, |
| final IRegion region, final String contentType, |
| final RHeuristicTokenScanner scanner, |
| final IProgressMonitor monitor) { |
| super(editor, region, contentType, IModelManager.MODEL_FILE, monitor); |
| |
| this.scanner= scanner; |
| |
| this.tool= determineRProcess(); |
| } |
| |
| |
| private @Nullable RProcess determineRProcess() { |
| final ISourceEditor editor= getEditor(); |
| final Tool tool; |
| if (editor instanceof ConsolePageEditor) { |
| tool= editor.getAdapter(Tool.class); |
| } |
| else { |
| tool= NicoUITools.getTool(editor.getWorkbenchPart()); |
| } |
| return (tool instanceof RProcess) ? (RProcess) tool : null; |
| } |
| |
| |
| @Override |
| protected boolean reuse(final ISourceEditor editor, final int offset) { |
| if (super.reuse(editor, offset)) { |
| LoadReferencesUtil toolReferencesUtil= this.toolReferencesUtil; |
| if (toolReferencesUtil != null) { |
| toolReferencesUtil.setWaitTimeout(getToolReferencesWaitTimeout()); |
| } |
| return true; |
| } |
| return false; |
| } |
| |
| |
| @Override |
| protected String getModelTypeId() { |
| return RModel.R_TYPE_ID; |
| } |
| |
| @Override |
| public IRSourceEditor getEditor() { |
| return (IRSourceEditor) super.getEditor(); |
| } |
| |
| @Override |
| public @Nullable IRSourceUnit getSourceUnit() { |
| return (IRSourceUnit) super.getSourceUnit(); |
| } |
| |
| public IRCoreAccess getRCoreAccess() { |
| return getEditor().getRCoreAccess(); |
| } |
| |
| |
| public final RHeuristicTokenScanner getRHeuristicTokenScanner() { |
| RHeuristicTokenScanner scanner= this.scanner; |
| if (scanner == null) { |
| scanner= RHeuristicTokenScanner.create(getEditor().getDocumentContentInfo()); |
| this.scanner= scanner; |
| } |
| return scanner; |
| } |
| |
| protected RLexer getLexer() { |
| RLexer lexer= this.lexer; |
| if (lexer == null) { |
| lexer= new RLexer((RLexer.DEFAULT | |
| RLexer.SKIP_WHITESPACE | RLexer.SKIP_LINEBREAK | RLexer.SKIP_COMMENT )); |
| lexer.reset(new StringParserInput()); |
| this.lexer= lexer; |
| } |
| return lexer; |
| } |
| |
| |
| @Override |
| protected String computeIdentifierPrefix(final int endOffset) |
| throws BadPartitioningException, BadLocationException { |
| final AbstractDocument document= (AbstractDocument) getDocument(); |
| |
| if (endOffset < 0 || endOffset > document.getLength()) { |
| throw new BadLocationException("offset= " + endOffset); //$NON-NLS-1$ |
| } |
| if (endOffset == 0) { |
| return ""; //$NON-NLS-1$ |
| } |
| |
| int offset= endOffset; |
| byte currentMode= (PARSE_SYMBOL | PARSE_OPERATOR); |
| byte validModes= (PARSE_SYMBOL | PARSE_OPERATOR); |
| final String partitioning= getEditor().getDocumentContentInfo().getPartitioning(); |
| ITypedRegion partition= document.getPartition(partitioning, offset, true); |
| if (partition.getType() == IRDocumentConstants.R_QUOTED_SYMBOL_CONTENT_TYPE |
| || partition.getType() == IRDocumentConstants.R_STRING_CONTENT_TYPE) { |
| offset= partition.getOffset(); |
| currentMode= PARSE_OPERATOR; |
| } |
| int startOffset= offset; |
| SEARCH_START: while (offset > 0) { |
| final char c= document.getChar(offset - 1); |
| if (RTokens.isRobustSeparator(c)) { |
| switch (c) { |
| case '$': |
| case '@': |
| if ((currentMode & PARSE_OPERATOR) != 0) { |
| offset--; |
| startOffset= offset; |
| currentMode= (byte) (validModes & PARSE_SYMBOL); |
| continue SEARCH_START; |
| } |
| break SEARCH_START; |
| case ':': |
| if ((currentMode & PARSE_OPERATOR) != 0 |
| && offset >= 2 && document.getChar(offset - 2) == ':') { |
| if (offset >= 3 && document.getChar(offset - 3) == ':') { |
| offset-= 3; |
| } |
| else { |
| offset-= 2; |
| } |
| validModes&= ~PARSE_OPERATOR; |
| currentMode= (byte) (validModes & PARSE_SYMBOL); |
| continue SEARCH_START; |
| } |
| break SEARCH_START; |
| // case ' ': |
| // case '\t': |
| // if (offset >= 2) { |
| // final char c2= document.getChar(offset - 2); |
| // if ((offset == getInvocationOffset()) ? |
| // !RTokens.isRobustSeparator(c2, false) : |
| // (c2 == '$' && c2 == '@')) { |
| // offset-= 2; |
| // continue SEARCH_START; |
| // } |
| // } |
| // break SEARCH_START; |
| case '`': |
| if ((currentMode & PARSE_SYMBOL) != 0) { |
| partition= document.getPartition(partitioning, offset - 1, false); |
| if (partition.getType() == IRDocumentConstants.R_QUOTED_SYMBOL_CONTENT_TYPE) { |
| offset= partition.getOffset(); |
| startOffset= offset; |
| currentMode= (byte) (validModes & PARSE_OPERATOR); |
| continue SEARCH_START; |
| } |
| } |
| break SEARCH_START; |
| |
| default: |
| break SEARCH_START; |
| } |
| } |
| else { |
| if ((currentMode & PARSE_SYMBOL) != 0) { |
| offset--; |
| startOffset= offset; |
| currentMode|= (byte) (validModes & PARSE_OPERATOR); |
| continue SEARCH_START; |
| } |
| break SEARCH_START; |
| } |
| } |
| |
| return document.get(startOffset, endOffset - startOffset); |
| } |
| |
| protected int computeIdentifierPrefixLastSegmentOffset(final int endOffset) |
| throws BadPartitioningException, BadLocationException { |
| final AbstractDocument document= (AbstractDocument) getDocument(); |
| |
| if (endOffset < 0 || endOffset > document.getLength()) { |
| throw new BadLocationException("endOffset= " + endOffset); //$NON-NLS-1$ |
| } |
| if (endOffset == 0) { |
| return 0; |
| } |
| |
| int offset= endOffset; |
| final String partitioning= getEditor().getDocumentContentInfo().getPartitioning(); |
| final ITypedRegion partition= document.getPartition(partitioning, offset, true); |
| if (partition.getType() == IRDocumentConstants.R_QUOTED_SYMBOL_CONTENT_TYPE |
| || partition.getType() == IRDocumentConstants.R_STRING_CONTENT_TYPE) { |
| return partition.getOffset(); |
| } |
| int startOffset= offset; |
| SEARCH_START: while (offset > 0) { |
| final char c= document.getChar(offset - 1); |
| if (RTokens.isRobustSeparator(c, false)) { |
| break SEARCH_START; |
| } |
| else { |
| offset--; |
| startOffset= offset; |
| } |
| } |
| |
| return startOffset; |
| } |
| |
| public @Nullable RElementName getIdentifierElementName() { |
| if (this.prefixName == null) { |
| this.prefixName= RElementName.parseDefault(getIdentifierPrefix()); |
| } |
| return this.prefixName; |
| } |
| |
| public int getIdentifierLastSegmentOffset() { |
| if (this.prefixLastSegmentOffset < 0) { |
| try { |
| this.prefixLastSegmentOffset= computeIdentifierPrefixLastSegmentOffset( |
| getInvocationOffset() ); |
| } |
| catch (final BadPartitioningException | BadLocationException e) { |
| this.prefixLastSegmentOffset= getInvocationOffset(); |
| throw new RuntimeException(e); |
| } |
| } |
| return this.prefixLastSegmentOffset; |
| } |
| |
| |
| public @Nullable String getIdentifierSegmentName(final String source) { |
| final RLexer lexer= getLexer(); |
| ((StringParserInput) lexer.getInput()).reset(source).init(); |
| lexer.reset(); |
| |
| switch (lexer.next()) { |
| case EOF: |
| return ""; |
| default: |
| return lexer.getText(); |
| } |
| } |
| |
| private static @Nullable RElementName getElementAccessOfRegion(final RElementAccess access, |
| final TextRegion region) { |
| RElementAccess current= access; |
| while (current != null) { |
| if (current.getSegmentName() == null) { |
| return null; |
| } |
| switch (current.getType()) { |
| case RElementName.SCOPE_NS: |
| case RElementName.SCOPE_NS_INT: |
| case RElementName.SCOPE_SEARCH_ENV: |
| case RElementName.SCOPE_PACKAGE: |
| |
| case RElementName.MAIN_DEFAULT: |
| case RElementName.MAIN_CLASS: |
| |
| case RElementName.SUB_NAMEDSLOT: |
| case RElementName.SUB_NAMEDPART: |
| break; |
| default: |
| return null; |
| } |
| |
| final RAstNode nameNode= current.getNameNode(); |
| if (nameNode != null |
| && nameNode.getStartOffset() <= region.getStartOffset() |
| && nameNode.getEndOffset() >= region.getEndOffset() ) { |
| return RElementName.create(access, current.getNextSegment(), true); |
| } |
| current= current.getNextSegment(); |
| } |
| |
| return null; |
| } |
| |
| public @Nullable RElementName getNameSelection() { |
| final AstNode selectedNode= getAstSelection().getCovering(); |
| if (selectedNode instanceof RAstNode) { |
| RAstNode node= (RAstNode) selectedNode; |
| RElementAccess access= null; |
| while (node != null && access == null) { |
| if (Thread.interrupted()) { |
| return null; |
| } |
| final List<Object> attachments= node.getAttachments(); |
| for (final Object attachment : attachments) { |
| if (attachment instanceof RElementAccess) { |
| node= null; |
| access= (RElementAccess) attachment; |
| final RElementName e= getElementAccessOfRegion(access, this); |
| if (e != null) { |
| return e; |
| } |
| if (Thread.interrupted()) { |
| return null; |
| } |
| } |
| } |
| if (node != null) { |
| node= node.getRParent(); |
| } |
| } |
| } |
| return null; |
| } |
| |
| |
| public @Nullable FCallInfo getFCallInfo() { |
| // This should be equals or more strict than the validation by RContextInformationValidator |
| // (RScanner.scanFCallArgs(..., expand= true)) |
| |
| // Unclosed function calls ends at last valid char, which is too strict for assists. |
| // Try to expand the argument list to also include whitespaces behind the last valid char / |
| // go back to last valid char. |
| final int endOffset= findRelevantOffsetBackward(getEndOffset()); |
| final int startOffset= Math.min(getStartOffset(), endOffset); |
| |
| final FCall fCallNode= searchFCallByArgsRegion(startOffset, endOffset); |
| if (fCallNode != null) { |
| final List<Object> attachments= fCallNode.getAttachments(); |
| for (final Object attachment : attachments) { |
| if (attachment instanceof RElementAccess) { |
| final RElementAccess fcallAccess= (RElementAccess)attachment; |
| if (fcallAccess.getNode() == fCallNode |
| && fcallAccess.isFunctionAccess() && !fcallAccess.isWriteAccess()) { |
| return new FCallInfo(fCallNode, fcallAccess); |
| } |
| } |
| } |
| } |
| return null; |
| } |
| |
| private int findRelevantOffsetBackward(final int offset) { |
| int docOffset= offset; |
| int docOffsetShift= 0; |
| IDocument document= getDocument(); |
| if (document instanceof FragmentDocument) { |
| final FragmentDocument fragmentDoc= (FragmentDocument)document; |
| document= fragmentDoc.getMasterDocument(); |
| docOffsetShift= fragmentDoc.getOffsetInMasterDocument(); |
| docOffset+= docOffsetShift; |
| } |
| if (docOffset > 0) { |
| try { |
| final RHeuristicTokenScanner scanner= getRHeuristicTokenScanner(); |
| final int bound= document.getLineOffset( |
| Math.max(document.getLineOfOffset(docOffset) - 2, 0) ); |
| |
| scanner.configure(document, IRDocumentConstants.R_NO_COMMENT_CONTENT_CONSTRAINT); |
| docOffset= scanner.findNonBlankBackward(docOffset, bound - 1, true); |
| if (docOffset != NOT_FOUND) { |
| return docOffset + 1 - docOffsetShift; // behind first relevant |
| } |
| } |
| catch (final BadLocationException e) {} |
| } |
| return offset; |
| } |
| |
| private @Nullable FCall searchFCallByArgsRegion(final int startOffset, final int endOffset) { |
| final AstInfo astInfo= getAstInfo(); |
| if (astInfo == null || astInfo.getRoot() == null) { |
| return null; |
| } |
| final AstSelection selection= AstSelection.search(astInfo.getRoot(), |
| startOffset, endOffset, AstSelection.MODE_COVERING_SAME_LAST ); |
| final AstNode node= selection.getCovering(); |
| |
| if (node instanceof RAstNode) { |
| RAstNode rNode= (RAstNode)node; |
| do { |
| if (rNode.getNodeType() == NodeType.F_CALL) { |
| final FCall fCallNode= (FCall)rNode; |
| if (fCallNode.getArgsOpenOffset() != AstNode.NA_OFFSET |
| && fCallNode.getArgsOpenOffset() < startOffset |
| && (fCallNode.getArgsCloseOffset() == AstNode.NA_OFFSET |
| || fCallNode.getArgsCloseOffset() >= endOffset )) { |
| return fCallNode; |
| } |
| } |
| rNode= rNode.getRParent(); |
| } while (rNode != null); |
| } |
| |
| return null; |
| } |
| |
| |
| public @Nullable RProcess getTool() { |
| return this.tool; |
| } |
| |
| public boolean isToolConsole() { |
| return (getEditor() instanceof ConsolePageEditor); |
| } |
| |
| public LoadReferencesUtil getToolReferencesUtil() { |
| assert (this.tool != null); |
| |
| LoadReferencesUtil util= this.toolReferencesUtil; |
| if (util == null) { |
| util= new LoadReferencesUtil(this.tool, getToolReferencesWaitTimeout()) { |
| @Override |
| protected void allFinished(final ImList<CombinedRElement> resolvedElements) { |
| if (!resolvedElements.isEmpty()) { |
| RAssistInvocationContext.this.toolReferencesResolved(resolvedElements); |
| } |
| } |
| }; |
| this.toolReferencesUtil= util; |
| } |
| return util; |
| } |
| |
| protected int getToolReferencesWaitTimeout() { |
| return LoadReferencesUtil.MAX_EXPLICITE_WAIT; |
| } |
| |
| protected void toolReferencesResolved(final ImList<CombinedRElement> resolvedElements) { |
| } |
| |
| |
| public int getDefaultRFrameSearchMode() { |
| return (isToolConsole()) ? RFrameSearchPath.CONSOLE_MODE : RFrameSearchPath.WORKSPACE_MODE; |
| } |
| |
| } |