/******************************************************************************* * Copyright (c) 2014 Bruno Medeiros and other Contributors. * 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: * Bruno Medeiros - initial API and implementation *******************************************************************************/ package dtool.engine.operations; import static melnorme.utilbox.core.Assert.AssertNamespace.assertTrue; import static melnorme.utilbox.core.Assert.AssertNamespace.assertUnreachable; import static melnorme.utilbox.misc.NumberUtil.isInRange; import static melnorme.utilbox.misc.NumberUtil.isInsideRange; import java.nio.file.Path; import melnorme.lang.tooling.ast.CommonLanguageElement; import melnorme.lang.tooling.ast.util.ASTNodeFinderExtension; import melnorme.lang.tooling.ast_actual.ASTNode; import melnorme.lang.tooling.completion.CompletionLocationInfo; import melnorme.lang.tooling.context.ISemanticContext; import melnorme.lang.tooling.engine.completion.CompletionScopeLookup; import melnorme.utilbox.concurrency.OperationCancellation; import melnorme.utilbox.core.CommonException; import dtool.ast.definitions.Module; import dtool.ast.references.CommonQualifiedReference; import dtool.ast.references.NamedReference; import dtool.ast.references.RefModule; import dtool.engine.ResolvedModule; import dtool.engine.SemanticManager; import dtool.engine.operations.DeeSymbolCompletionResult.ECompletionResultStatus; import dtool.parser.DeeParser; import dtool.parser.DeeParserResult; import dtool.parser.DeeTokens; import dtool.parser.common.IToken; import dtool.parser.common.LexerResult.TokenAtOffsetResult; public class CodeCompletionOperation extends AbstractDToolOperation { public CodeCompletionOperation(SemanticManager semanticManager, Path filePath, int offset, Path compilerPath, String dubPath) throws CommonException { super(semanticManager, filePath, offset, compilerPath, dubPath); } public DeeSymbolCompletionResult doCodeCompletion() throws CommonException { ResolvedModule resolvedModule = getResolvedModule(fileLoc); return doCodeCompletion(resolvedModule, offset); } public static DeeSymbolCompletionResult doCodeCompletion(ResolvedModule resolvedModule, int offset) { return completionSearch(resolvedModule.getParsedModule(), offset, resolvedModule.getSemanticContext()); } public static boolean canCodeCompleteInsideToken(IToken token, int offset) { if(token.getType() == DeeTokens.WHITESPACE || token.getType().isAlphaNumeric()) { return true; } if(token.getType() == DeeTokens.STRING_TOKENS) { if(token.getSourceValue().length() < 3) { return false; // This should never happen, actually. } // This string token is often used for code, so we allow completion inside it: return isInRange(token.getStartPos()+2, offset, token.getEndPos()-1); } return false; } public static DeeSymbolCompletionResult completionSearch(DeeParserResult parseResult, int offset, ISemanticContext context) { assertTrue(isInRange(0, offset, parseResult.source.length())); TokenAtOffsetResult tokenAtOffsetResult = parseResult.findTokenAtOffset(offset); IToken tokenAtOffsetLeft = tokenAtOffsetResult.atLeft; IToken tokenAtOffsetRight = tokenAtOffsetResult.atRight; if(tokenAtOffsetResult.isSingleToken() && isInsideRange(tokenAtOffsetLeft.getStartPos(), offset, tokenAtOffsetLeft.getEndPos()) && !canCodeCompleteInsideToken(tokenAtOffsetLeft, offset) ) { return new DeeSymbolCompletionResult(ECompletionResultStatus.INVALID_TOKEN_LOCATION); } if(tokenAtOffsetLeft != null && tokenAtOffsetLeft.getType().getGroupingToken() == DeeTokens.GROUP_FLOAT && tokenAtOffsetLeft.getSourceValue().endsWith(".")) { return new DeeSymbolCompletionResult(ECompletionResultStatus.INVALID_TOKEN_LOCATION_FLOAT); } final IToken nameToken; if(tokenAtOffsetLeft != null && tokenAtOffsetLeft.getType().isAlphaNumeric()) { nameToken = tokenAtOffsetLeft; } else if(tokenAtOffsetRight != null && tokenAtOffsetRight.getType().isAlphaNumeric()) { nameToken = tokenAtOffsetRight; } else { nameToken = null; } Module module = parseResult.getModuleNode(); ASTNode pickedNode = new ASTNodeFinderExtension(module, offset, true).match; assertTrue(pickedNode.getSourceRange().inclusiveContains(offset)); CommonLanguageElement elementAtOffset = pickedNode; if(elementAtOffset instanceof CommonQualifiedReference) { CommonQualifiedReference namedRef = (CommonQualifiedReference) elementAtOffset; assertTrue(nameToken == null); if(offset <= namedRef.getDotOffset()) { elementAtOffset = namedRef.getLexicalParent(); } CompletionLocationInfo locationInfo = new CompletionLocationInfo(offset); return performCompletionSearch(locationInfo, context, elementAtOffset); } else if(elementAtOffset instanceof RefModule) { RefModule refModule = (RefModule) elementAtOffset; // RefModule has a specialized way to setup prefix len things String source = parseResult.source; CompletionLocationInfo locationInfo = codeCompletionRefModule(offset, tokenAtOffsetRight, source, refModule); return performCompletionSearch(locationInfo, context, elementAtOffset); } if(nameToken != null) { assertTrue(nameToken.getSourceRange().inclusiveContains(offset)); String searchPrefix = nameToken.getSourceValue().substring(0, offset - nameToken.getStartPos()); int rplLen = nameToken.getEndPos() - offset; CompletionLocationInfo locationInfo = new CompletionLocationInfo(offset, searchPrefix, rplLen); // Because of some parser limitations, in some cases nodeForNameLookup needs to be corrected, // such that it won't be the same as nodeForNameLookup ASTNode nodeForNameLookup = getStartingNodeForNameLookup(nameToken.getStartPos(), module); return performCompletionSearch(locationInfo, context, nodeForNameLookup); } else { CompletionLocationInfo locationInfo = new CompletionLocationInfo(offset); return performCompletionSearch(locationInfo, context, elementAtOffset); } } protected static ASTNode getStartingNodeForNameLookup(int offset, Module module) { ASTNodeFinderExtension nodeFinder = new ASTNodeFinderExtension(module, offset, true); ASTNode node = nodeFinder.match; if(nodeFinder.matchOnLeft instanceof NamedReference) { NamedReference reference = (NamedReference) nodeFinder.matchOnLeft; if(reference.isMissingCoreReference()) { node = nodeFinder.matchOnLeft; } } return node; } public static CompletionLocationInfo codeCompletionRefModule(final int offset, IToken tokenAtOffsetRight, String source, RefModule refModule) { int idEnd = refModule.getEndPos(); if(refModule.isMissingCoreReference()) { if(tokenAtOffsetRight.getType().isKeyword()) { idEnd = tokenAtOffsetRight.getEndPos(); // Fix for attached keyword ids } else { idEnd = refModule.moduleToken.getFullRangeStartPos(); } } int rplLen = offset > idEnd ? 0 : idEnd - offset; // We reparse the snipped source as it's the easiest way to determine search prefix String refModuleSnippedSource = source.substring(refModule.getStartPos(), offset); String moduleQualifiedNameCanonicalPrefix = parseModuleQualifiedNamePrefix(refModuleSnippedSource); return new CompletionLocationInfo(offset, moduleQualifiedNameCanonicalPrefix, rplLen); } protected static String parseModuleQualifiedNamePrefix(String refModuleSnippedSource) { DeeParser parser = new DeeParser(refModuleSnippedSource); String moduleQualifiedNameCanonicalPrefix; try { moduleQualifiedNameCanonicalPrefix = parser.parseRefModule().toStringAsCode(); } catch(OperationCancellation e) { throw assertUnreachable(); } DeeTokens lookAhead = parser.lookAhead(); if(lookAhead != DeeTokens.EOF) { assertTrue(lookAhead.isKeyword()); moduleQualifiedNameCanonicalPrefix += lookAhead.getSourceValue(); } return moduleQualifiedNameCanonicalPrefix; } public static DeeSymbolCompletionResult performCompletionSearch(CompletionLocationInfo locationInfo, ISemanticContext context, CommonLanguageElement element) { CompletionScopeLookup search = new CompletionScopeLookup(locationInfo.offset, context, locationInfo.searchPrefix); element.performNameLookup(search); return new DeeSymbolCompletionResult(locationInfo, search.getMatchedElements()); } }