blob: 6136a7d25d769306f95fbf8291200e641d58a254 [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.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;
}
}