Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement LSP autocomplete #384

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 40 additions & 2 deletions effekt/jvm/src/main/scala/effekt/Server.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@ import effekt.util.messages.EffektError
import kiama.util.{ Filenames, Position, Services, Source }
import kiama.output.PrettyPrinterTypes.Document

import org.eclipse.lsp4j.{ Diagnostic, DocumentSymbol, SymbolKind, ExecuteCommandParams }
import scala.collection.mutable.HashMap
import org.eclipse.lsp4j.{ Diagnostic, DocumentSymbol, CompletionItem, CompletionItemKind, InsertTextFormat, SymbolKind, ExecuteCommandParams }

/**
* effekt.Intelligence <--- gathers information -- LSPServer --- provides LSP interface ---> kiama.Server
Expand Down Expand Up @@ -132,7 +133,6 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
}

override def getSymbols(source: Source): Option[Vector[DocumentSymbol]] =

context.compiler.runFrontend(source)(using context)

val documentSymbols = for {
Expand All @@ -152,6 +152,25 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
allRefs = if (includeDecl) tree :: refs else refs
} yield allRefs.toVector

override def getCompletion(position: Position): Option[Vector[CompletionItem]] = for {
suggestions <- getCompletionsAt(position)(context)
items = suggestions.map {
case Left(sym) =>
val item = CompletionItem(sym.name.name)
item.setKind(getCompletionKind(sym))
getInfoOf(sym)(context).map { detail =>
item.setDetail(detail.header)
item.setDocumentation(detail.description.getOrElse(""))
}
item
case Right(name, detail) =>
val item = CompletionItem(name)
item.setKind(CompletionItemKind.Keyword)
item.setDetail(detail)
item
}
} yield items

// settings might be null
override def setSettings(settings: Object): Unit = {
import com.google.gson.JsonObject
Expand All @@ -177,6 +196,25 @@ trait LSPServer extends kiama.util.Server[Tree, EffektConfig, EffektError] with
None
}

// for custom icons and intelligence of autocomplete
def getCompletionKind(sym: Symbol): CompletionItemKind =
sym match {
case _: Module =>
CompletionItemKind.Module
case _: Interface | _: ExternInterface =>
CompletionItemKind.Interface
case _: DataType | _: ExternType | _: TypeAlias =>
CompletionItemKind.Enum
case _: Callable =>
CompletionItemKind.Method
case _: Param | _: ValBinder | _: VarBinder =>
CompletionItemKind.Variable
case _: TypeSymbol =>
CompletionItemKind.TypeParameter
case _ =>
CompletionItemKind.Text
}

override def getCodeActions(position: Position): Option[Vector[TreeAction]] =
Some(for {
trees <- getTreesAt(position)(context).toVector
Expand Down
35 changes: 34 additions & 1 deletion effekt/shared/src/main/scala/effekt/Intelligence.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package effekt

import effekt.context.{ Annotations, Context }
import effekt.source.{ FunDef, ModuleDecl, Tree }
import kiama.util.{ Position, Source }
import kiama.util.{ Position, Positions, Source }

trait Intelligence {

Expand Down Expand Up @@ -80,6 +80,39 @@ trait Intelligence {
case u => C.definitionTreeOption(u)
}

type CompletionSuggestion = Either[Symbol, (String, String)]

def getCompletableItems(position: Position, prefix: String)(implicit C: Context): Option[Vector[CompletionSuggestion]] = {
def isPrefix(s: String) = s.startsWith(prefix) // TODO: add more filters (e.g. Levenshtein distance?)
def filter(it: Iterable[String]) = it.filter(isPrefix)

val keywords = filter(EffektLexers(new Positions).keywordStrings).map { keyword => Right(keyword, "Keyword") }
val builtinTypes = filter(builtins.rootTypes.keys).map { key => Left(builtins.rootTypes.get(key).get) }
val builtinTerms = filter(builtins.rootTerms.keys).map { key => Left(builtins.rootTerms.get(key).get) }
val builtinCaptures = filter(builtins.rootCaptures.keys).map { key => Left(builtins.rootCaptures.get(key).get) }

// TODO: only return symbols within scope of current position and include library symbols
// "dumb" function for now to just get all symbols in AST recursively
def getAllSymbols(product: Product): Iterator[String] = product.productIterator.flatMap {
case p: Product => getAllSymbols(p)
case s: String => List(s)
case _ => List()
}.distinct

val allSymbols = C.compiler.getAST(position.source) match
case Some(ast) => getAllSymbols(ast).map { s => Right(s, "Symbol in scope") }
case None => List()

Some((keywords ++ builtinTypes ++ builtinTerms ++ builtinCaptures ++ allSymbols).toVector)
}

def getCompletionsAt(position: Position)(implicit C: Context): Option[Vector[CompletionSuggestion]] = for {
line <- position.source.optLineContents(position.line)
beforeCaret = line.take(position.column - 1)
word = beforeCaret.drop(beforeCaret.lastIndexOf(" ") + 1)
syms <- getCompletableItems(position, word)
} yield syms

// For now, only show the first call target
def resolveCallTarget(sym: Symbol): Symbol = sym match {
case t: CallTarget => t.symbols.flatten.headOption.getOrElse(sym)
Expand Down
Loading