Erlang Plugin for NetBeans in Scala#10: Code Completion
Implementing Code-Completion is a bit complex, but you can got it work gradually. At the first step, you can implement Code-Completion for local vars/functions only, then, with the indexed supporting, you can add completion for remote functions.
You should define some kinds of completion proposal, which may show different behaviors when they are popped up and guard you followed steps. For example, a function proposal can auto-fill parameters, on the other side, a keyword proposal just complete itself.
The completion proposal classes are defined in ErlangComplectionProposal.scala, which implemented CSL's interface CompletionProposal. you may notice that the function proposal is the most complex one, which should handle parameters information.
Then, you should implement CSL's interface CodeCompletionHandler, for Erlang, it's ErlangCodeCompletion, where, the key method is:
override
def complete(context:CodeCompletionContext) :CodeCompletionResult = {
this.caseSensitive = context.isCaseSensitive
val pResult = context.getParserResult.asInstanceOf[ErlangParserResult]
val lexOffset = context.getCaretOffset
val prefix = context.getPrefix match {
case null => ""
case x => x
}
val kind = if (context.isPrefixMatch) QuerySupport.Kind.PREFIX else QuerySupport.Kind.EXACT
val queryType = context.getQueryType
val doc = LexUtil.document(pResult, true) match {
case None => return CodeCompletionResult.NONE
case Some(x) => x.asInstanceOf[BaseDocument]
}
val proposals = new ArrayList[CompletionProposal]
val completionResult = new DefaultCompletionResult(proposals, false)
// Read-lock due to Token hierarchy use
doc.readLock
try {
val astOffset = LexUtil.astOffset(pResult, lexOffset)
if (astOffset == -1) {
return CodeCompletionResult.NONE
}
val root = pResult.rootScope match {
case None => return CodeCompletionResult.NONE
case Some(x) => x
}
val th = LexUtil.tokenHierarchy(pResult).get
val fileObject = LexUtil.fileObject(pResult).get
val request = new CompletionRequest
request.completionResult = completionResult
request.result = pResult
request.lexOffset = lexOffset
request.astOffset = astOffset
request.index = ErlangIndex.get(pResult)
request.doc = doc
request.info = pResult
request.prefix = prefix
request.th = th
request.kind = kind
request.queryType = queryType
request.fileObject = fileObject
request.anchor = lexOffset - prefix.length
request.root = root
ErlangCodeCompletion.request = request
val token = LexUtil.token(doc, lexOffset - 1) match {
case None => return completionResult
case Some(x) => x
}
token.id match {
case ErlangTokenId.LineComment =>
// TODO - Complete symbols in comments?
return completionResult
case ErlangTokenId.StringLiteral =>
//completeStrings(proposals, request)
return completionResult
case _ =>
}
val ts = LexUtil.tokenSequence(th, lexOffset - 1) match {
case None => return completionResult
case Some(x) =>
x.move(lexOffset - 1)
if (!x.moveNext && !x.movePrevious) {
return completionResult
}
x
}
val closetToken = LexUtil.findPreviousNonWsNonComment(ts)
if (root != null) {
val sanitizedRange = pResult.sanitizedRange
val offset = if (sanitizedRange != OffsetRange.NONE && sanitizedRange.containsInclusive(astOffset)) {
sanitizedRange.getStart
} else astOffset
val call = Call(null, null, false)
findCall(root, ts, th, call, 0)
val prefixBak = request.prefix
call match {
case Call(null, _, _) =>
case Call(base, _, false) =>
// it's not a call, but may be candicate for module name, try to get modules and go-on
completeModules(base, proposals, request)
case Call(base, select, true) =>
if (select != null) {
request.prefix = call.select.text.toString
} else {
request.prefix = ""
}
completeModuleFunctions(call.base, proposals, request)
// Since is after a ":", we won't added other proposals, just return now whatever
return completionResult
}
request.prefix = prefixBak
completeLocals(proposals, request)
}
completeKeywords(proposals, request)
} finally {
doc.readUnlock
}
completionResult
}
For a Erlang function call, you should check the tokens surrounding the caret to get the call's base name and select first, which is done by a method findCall:
private def findCall(rootScope:AstRootScope, ts:TokenSequence[TokenId], th:TokenHierarchy[_], call:Call, times:Int) :Unit = {
assert(rootScope != null)
val closest = LexUtil.findPreviousNonWsNonComment(ts)
val idToken = closest.id match {
case ErlangTokenId.Colon =>
call.caretAfterColon = true
// skip RParen if it's the previous
if (ts.movePrevious) {
val prev = LexUtil.findPreviousNonWs(ts)
if (prev != null) {
prev.id match {
case ErlangTokenId.RParen => LexUtil.skipPair(ts, ErlangTokenId.LParen, ErlangTokenId.RParen, true)
case ErlangTokenId.RBrace => LexUtil.skipPair(ts, ErlangTokenId.LBrace, ErlangTokenId.RBrace, true)
case ErlangTokenId.RBracket => LexUtil.skipPair(ts, ErlangTokenId.LBracket, ErlangTokenId.RBracket, true)
case _ =>
}
}
}
LexUtil.findPrevIncluding(ts, LexUtil.CALL_IDs)
case id if LexUtil.CALL_IDs.contains(id) => closest
case _ => null
}
if (idToken != null) {
times match {
case 0 if call.caretAfterColon => call.base = idToken
case 0 if ts.movePrevious => LexUtil.findPreviousNonWsNonComment(ts) match {
case null => call.base = idToken
case prev if prev.id == ErlangTokenId.Colon =>
call.caretAfterColon = true
call.select = idToken
findCall(rootScope, ts, th, call, times + 1)
case _ => call.base = idToken
}
case _ => call.base = idToken
}
}
}
case class Call(var base:Token[TokenId], var select:Token[TokenId], var caretAfterColon:Boolean)
To complete a remote function call, you may need to visit outer modules, which needs an indexer, so as the first step, you can just ignore it, go straight to complete local vars/functions, or keywords:
private def completeLocals(proposals:List[CompletionProposal], request:CompletionRequest) :Unit = {
val prefix = request.prefix
val kind = request.kind
val pResult = request.result
val root = request.root
val closestScope = root.closestScope(request.th, request.astOffset) match {
case None => return
case Some(x) => x
}
val localVars = closestScope.visibleDfns(ElementKind.VARIABLE)
localVars ++= closestScope.visibleDfns(ElementKind.PARAMETER)
localVars.filter{v => filterKind(kind, prefix, v.name)}.foreach{v =>
proposals.add(new PlainProposal(v, request.anchor))
}
val localFuns = closestScope.visibleDfns(ElementKind.METHOD)
localFuns.filter{f => filterKind(kind, prefix, f.name)}.foreach{f =>
proposals.add(new FunctionProposal(f, request.anchor))
}
}
private def completeKeywords(proposals:List[CompletionProposal], request:CompletionRequest) :Unit = {
val prefix = request.prefix
val itr = LexerErlang.ERLANG_KEYWORDS.iterator
while (itr.hasNext) {
val keyword = itr.next
if (startsWith(keyword, prefix)) {
proposals.add(new KeywordProposal(keyword, null, request.anchor))
}
}
}
There is a function def visibleDfns(kind: ElementKind): ArrayBuffer[AstDfn] in AstRootScope.scala, if you've put definition items properly in scopes, it should handle the visibility automatically.
Now, register it in ErlangLanguage.scala:
override
def getCompletionHandler = new ErlangCodeCompletion
As usual, run it, you got:
The local functions and vars are proposed plus the keywords. BTW, I've fixed this feature for Scala plug-in.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/blog_logo.png)
rss

Comments
No comments.