Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance. Project price only 1 $
You can buy this project and download/modify it how often you want.
/*
Copyright (c) 2021 Kevin Jones & FinancialForce, All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. The name of the author may not be used to endorse or promote products
derived from this software without specific prior written permission.
*/
package com.nawforce.apexlink.org
import com.nawforce.apexlink.cst._
import com.nawforce.apexlink.finding.TypeResolver
import com.nawforce.apexlink.org.CompletionProvider._
import com.nawforce.apexlink.org.TextOps.TestOpsUtils
import com.nawforce.apexlink.rpc.CompletionItemLink
import com.nawforce.apexlink.types.apex.{ApexClassDeclaration, ApexFullDeclaration}
import com.nawforce.apexlink.types.core._
import com.nawforce.pkgforce.documents.{ApexClassDocument, ApexTriggerDocument, MetadataDocument}
import com.nawforce.pkgforce.modifiers.PUBLIC_MODIFIER
import com.nawforce.pkgforce.names.TypeName
import com.nawforce.pkgforce.path.PathLike
import com.vmware.antlr4c3.CodeCompletionCore
import io.github.apexdevtools.apexparser.{ApexLexer, ApexParser}
import org.antlr.v4.runtime.Token
import scala.collection.mutable
import scala.jdk.CollectionConverters._
import scala.util.matching.Regex
trait CompletionProvider {
this: OPM.PackageImpl =>
def getCompletionItems(
path: PathLike,
line: Int,
offset: Int,
content: String
): Array[CompletionItemLink] = {
// Get basic context of what we are looking at
val module = getPackageModule(path)
val terminatedContent = injectStatementTerminator(line, offset, content)
val classDetails = MetadataDocument(path)
.collect {
case _: ApexClassDocument => loadClass(path, terminatedContent._1)
case _: ApexTriggerDocument => loadTrigger(path, terminatedContent._1)
}
.getOrElse((None, None))
if (classDetails._1.isEmpty)
/* Bail if we did not at least parse the content */
return emptyCompletions
val parserAndCU = classDetails._1.get
val adjustedOffset = terminatedContent._2
// Attempt to find a searchTerm for dealing with dot expressions
lazy val searchTerm =
content.extractDotTermExclusive(() => new IdentifierAndMethodLimiter, line, offset)
// Setup to lazy validate the Class block to recover the validationResult for the cursor position
lazy val validationResult: Option[ValidationResult] = {
if (classDetails._2.nonEmpty && searchTerm.nonEmpty) {
val searchEnd = searchTerm.get.location.endPosition
val resultMap = classDetails._2.get.getValidationMap(line, searchEnd)
val exprLocations = resultMap.keys.filter(_.contains(line, searchEnd))
val targetExpression =
exprLocations.find(exprLocation => exprLocations.forall(_.contains(exprLocation)))
targetExpression.map(targetExpression => resultMap(targetExpression))
} else {
None
}
}
// Lazy extract local vars in scope at cursor position from the validation
lazy val localVars = validationResult.flatMap(_.vars).getOrElse(mutable.Map())
// Run C3 to get our keywords & rule matches
val tokenAndIndex =
findTokenAndIndex(parserAndCU._1, line, adjustedOffset, offset != adjustedOffset)
val core = new CodeCompletionCore(parserAndCU._1, preferredRules.asJava, ignoredTokens.asJava)
val candidates = core.collectCandidates(tokenAndIndex._2, parserAndCU._2, MAX_STATES)
// Generate a list of possible keyword matches
val keywords = candidates.tokens.asScala
.filter(_._1 >= 1)
.map(kv => parserAndCU._1.getVocabulary.getDisplayName(kv._1))
.map(keyword => stripQuotes(keyword))
.map(keyword => CompletionItemLink(keyword, "Keyword"))
.toArray
val creatorCompletions =
if (classDetails._2.nonEmpty && keywords.map(_.label).contains("new")) {
getEmptyCreatorCompletionItems(classDetails._2.get, terminatedContent._3)
} else {
emptyCompletions
}
// Find completions for a dot expression, we use the safe navigation operator here as a trigger as it is unique to
// dot expressions. We can't use rule matching for these as often the expression before the '.' is complete and
// we have to remove the '.' so the parser constructs a statement, see injectStatementTerminator().
val dotCompletions =
if (keywords.exists(_.label == "?.") && searchTerm.nonEmpty) {
validationResult
.filter(_.result.declaration.nonEmpty)
.map(validationResult =>
getAllCompletionItems(
validationResult.result.declaration.get,
validationResult.result.isStatic,
searchTerm.get.residualExpr
)
)
.getOrElse(emptyCompletions)
} else {
emptyCompletions
}
/* Now for rule matches. These are not distinct cases, they might combine to give the correct result. */
var haveTypes = false
val rules = candidates.rules.asScala
.collect(rule =>
rule._1.toInt match {
/* TypeRefs appear in lots of places, e.g. inside Primaries but we just handle once for simplicity. */
case ApexParser.RULE_typeRef =>
if (haveTypes) Array[CompletionItemLink]()
else {
haveTypes = true
module.map(_.matchTypeName(terminatedContent._3, offset)).getOrElse(Array())
}
/* Primary will appear at the start of an expression (recursively) but this just handles the first primary as
* dotCompletions covers over cases. At the point it is being handled it is indistinguishable from a MethodCall
* so we handle both cases. */
case ApexParser.RULE_primary =>
/* Also handles start of methodCall */
searchTerm
.map(searchTerm => {
// Local vars can only be on initial primary
if (searchTerm.prefixExpr.isEmpty) {
localVars.keys
.filter(
_.value.take(1).toLowerCase == searchTerm.residualExpr.take(1).toLowerCase
)
.map(name =>
CompletionItemLink(
name.value,
"Variable",
localVars(name).declaration.typeName.toString()
)
)
.toArray ++
classDetails._2
.map(td =>
getAllCompletionItems(
td,
None,
searchTerm.residualExpr,
hasPrivateAccess = true
)
)
.getOrElse(Array()) ++
(if (haveTypes) Array[CompletionItemLink]()
else {
haveTypes = true
module
.map(_.matchTypeName(terminatedContent._3, offset))
.getOrElse(Array())
})
} else {
emptyCompletions
}
})
.getOrElse(emptyCompletions)
case ApexParser.RULE_creator =>
module
.map(m => m.matchTdsForModule(terminatedContent._3, offset))
.map(_.flatMap(td => getAllCreatorCompletionItems(td, classDetails._2)))
.getOrElse(emptyCompletions)
}
)
.flatten
.toArray
(if (creatorCompletions.nonEmpty)
creatorCompletions
else
keywords.filterNot(_.label == "?.")) ++ dotCompletions ++ rules
}
private def findTokenAndIndex(
parser: ApexParser,
line: Int,
offset: Int,
dotRemoved: Boolean
): (Token, Int) = {
val tokenStream = parser.getInputStream
tokenStream.seek(0)
var i = 0
var token = tokenStream.get(i)
while (token.getType != -1 && !tokenContains(token, line, offset)) {
i += 1
token = tokenStream.get(i)
}
// Adjust cursor, see c3 README for details of cursor positioning
val idx =
if (dotRemoved || (i > 1 && tokenStream.get(i - 2).getText == ".")) {
i
} else {
Math.max(0, i - 1)
}
(tokenStream.get(idx), idx)
}
private def tokenContains(token: Token, line: Int, offset: Int): Boolean = {
token.getLine == line &&
token.getCharPositionInLine <= offset &&
token.getCharPositionInLine + token.getText.length > offset
}
private def stripQuotes(keyword: String): String = {
if (keyword.length > 2)
keyword.substring(1, keyword.length - 1)
else
keyword
}
private def injectStatementTerminator(
line: Int,
offset: Int,
content: String
): (String, Int, String) = {
val lines = content.splitLines
val result = new mutable.StringBuilder()
var adjustedOffset = offset
var activeLine = ""
for (i <- lines.indices) {
val currentLine = lines(i)
if (i == line - 1) {
activeLine = currentLine.substring(0, offset)
result.append(activeLine)
// Erase trailing dot so we have a legal expression that ANTLR will construct
if (result.last == '.') {
result.deleteCharAt(result.length() - 1)
adjustedOffset -= 1
}
appendTerminators(result)
result.append(currentLine.substring(offset))
result.append('\n')
} else {
result.append(currentLine)
result.append('\n')
}
}
(result.toString(), adjustedOffset, activeLine)
}
private def appendTerminators(buffer: mutable.StringBuilder): Unit = {
def appendStack(buffer: mutable.StringBuilder, stack: List[Char]): Unit = {
stack match {
case '(' :: tl =>
appendStack(buffer, tl)
buffer.append(')')
case '[' :: tl =>
appendStack(buffer, tl)
buffer.append(']')
case _ => ()
}
}
var at = buffer.length - 1
var stack: List[Char] = Nil
while (at > 0 && buffer.charAt(at) != '{' && buffer.charAt(at) != ';') {
val char = buffer(at)
if (char == ')' || char == ']')
stack = char :: stack
else if (char == '(') {
if (stack.headOption.contains(')'))
stack = stack.tail
else
stack = char :: stack
} else if (char == '[') {
if (stack.headOption.contains(']'))
stack = stack.tail
else
stack = char :: stack
}
at -= 1
}
appendStack(buffer, stack)
buffer.append(";")
}
private def getAllCompletionItems(
td: TypeDeclaration,
isStatic: Option[Boolean],
filterBy: String,
hasPrivateAccess: Boolean = false
): Array[CompletionItemLink] = {
var items = Array[CompletionItemLink]()
items = items ++ td.methods
.filter(isStatic.isEmpty || _.isStatic == isStatic.get)
.filter(hasPrivateAccess || _.modifiers.contains(PUBLIC_MODIFIER))
.map(method => CompletionItemLink(method))
items = items ++ td.fields
.filter(isStatic.isEmpty || _.isStatic == isStatic.get)
.filter(hasPrivateAccess || _.modifiers.contains(PUBLIC_MODIFIER))
.map(field => CompletionItemLink(field))
if (isStatic.isEmpty || isStatic.contains(true)) {
items = items ++ td.nestedTypes
.filter(hasPrivateAccess || _.modifiers.contains(PUBLIC_MODIFIER))
.flatMap(nested => CompletionItemLink(nested))
}
if (isStatic.isEmpty) {
val superCtors = td.superClassDeclaration
.map(superClass => {
superClass.constructors
.filter(ctor => ConstructorMap.isCtorAccessible(ctor, td, td.superClassDeclaration))
.map(ctor =>
(
"super(" + ctor.parameters.map(_.name.toString()).mkString(", ") + ")",
ctor.toString
)
)
.map(labelDetail => CompletionItemLink(labelDetail._1, "Constructor", labelDetail._2))
.toArray
})
.getOrElse(emptyCompletions)
val thisCtors = td.constructors
.filter(ctor => ConstructorMap.isCtorAccessible(ctor, td, td.superClassDeclaration))
.map(ctor =>
("this(" + ctor.parameters.map(_.name.toString()).mkString(", ") + ")", ctor.toString)
)
.map(labelDetail => CompletionItemLink(labelDetail._1, "Constructor", labelDetail._2))
items = items ++ thisCtors ++ superCtors
}
if (filterBy.nonEmpty)
items.filter(x => x.label.take(1).toLowerCase == filterBy.take(1).toLowerCase)
else
items
}
private def getAllCreatorCompletionItems(
itemsFor: ApexClassDeclaration,
callingFrom: Option[ApexFullDeclaration]
): Array[CompletionItemLink] = {
callingFrom.map(td => (td, td.superClassDeclaration)) match {
case Some((thisType, superType)) =>
itemsFor.constructors
.filter(ctor => ConstructorMap.isCtorAccessible(ctor, thisType, superType))
.map(ctor => CompletionItemLink(itemsFor.name, ctor))
.toArray
case None => emptyCompletions
}
}
private def getEmptyCreatorCompletionItems(
td: ApexFullDeclaration,
line: String
): Array[CompletionItemLink] = {
// Strip 'id = n' from '... typeName id = new '
val trimmed = idAssignPattern.replaceFirstIn(line, "")
if (trimmed == line)
return emptyCompletions
// Map valid typeName to creation completion
findTrailingTypeName(trimmed)
.flatMap(typeName => {
TypeResolver(typeName, td).toOption
.filter(td => td.isSObject || td.constructors.exists(ctor => ctor.parameters.isEmpty))
.map(_ => new CompletionItemLink(s"new $typeName();", "Constructor", ""))
})
.toArray
}
private def findTrailingTypeName(text: String): Option[TypeName] = {
if (text.isEmpty)
None
else
TypeName(text).toOption.orElse(findTrailingTypeName(text.substring(1)))
}
}
object CompletionProvider {
/* This limits how many states can be traversed during code completion, it provides a safeguard against run away
* analysis but needs to be large enough for long files. */
final val MAX_STATES: Int = 10000000
/* Match trailing 'id = n' as part of field/var creator pattern */
final val idAssignPattern: Regex = "\\s*[0-9a-zA-Z_]+\\s*=\\s*n\\s*$".r
final val emptyCompletions: Array[CompletionItemLink] = Array[CompletionItemLink]()
final val ignoredTokens: Set[Integer] = Set[Integer](
ApexLexer.LPAREN,
ApexLexer.RPAREN,
ApexLexer.LBRACE,
ApexLexer.LBRACE,
ApexLexer.RBRACE,
ApexLexer.LBRACK,
ApexLexer.RBRACK,
ApexLexer.SEMI,
ApexLexer.COMMA,
ApexLexer.DOT,
ApexLexer.ASSIGN,
ApexLexer.GT,
ApexLexer.LT,
ApexLexer.BANG,
ApexLexer.TILDE,
/* ApexLexer.QUESTIONDOT, - Needed for dotCompletion handling */
ApexLexer.QUESTION,
ApexLexer.COLON,
ApexLexer.EQUAL,
ApexLexer.TRIPLEEQUAL,
ApexLexer.NOTEQUAL,
ApexLexer.LESSANDGREATER,
ApexLexer.TRIPLENOTEQUAL,
ApexLexer.AND,
ApexLexer.OR,
ApexLexer.COAL,
ApexLexer.INC,
ApexLexer.DEC,
ApexLexer.ADD,
ApexLexer.SUB,
ApexLexer.MUL,
ApexLexer.DIV,
ApexLexer.BITAND,
ApexLexer.BITOR,
ApexLexer.CARET,
ApexLexer.MAPTO,
ApexLexer.ADD_ASSIGN,
ApexLexer.SUB_ASSIGN,
ApexLexer.MUL_ASSIGN,
ApexLexer.DIV_ASSIGN,
ApexLexer.AND_ASSIGN,
ApexLexer.OR_ASSIGN,
ApexLexer.XOR_ASSIGN,
ApexLexer.LSHIFT_ASSIGN,
ApexLexer.RSHIFT_ASSIGN,
ApexLexer.URSHIFT_ASSIGN,
ApexLexer.ATSIGN,
ApexLexer.INSTANCEOF
)
final val preferredRules: Set[Integer] =
Set[Integer](ApexParser.RULE_typeRef, ApexParser.RULE_primary, ApexParser.RULE_creator)
}