Erlang Plugin for NetBeans in Scala#10: Code Completion

Caoyuan Blog - - February 28, 2009

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:

nn

The local functions and vars are proposed plus the keywords. BTW, I’ve fixed this feature for Scala plug-in.



Categories: Blogs  Caoyuan Blog  

Comments

No comments so far, you could be the first.

Add comment

Name:

Email:

URL:

Smileys

Remember my personal information

Notify me of follow-up comments?