Skip to content
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.strumenta.kolasu.languageserver

import org.eclipse.lsp4j.*
import org.eclipse.lsp4j.jsonrpc.messages.Either
import java.util.concurrent.CompletableFuture

interface CompletionEngine {
fun complete(uri: String, text: String, pos: Position): CompletableFuture<Either<MutableList<CompletionItem>, CompletionList>>
fun resolve(item: CompletionItem): CompletionItem = item
val triggerCharacters: List<String> get() = listOf(".", ":", "@")
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,19 @@ import com.strumenta.kolasu.model.URLSource
import com.strumenta.kolasu.model.children
import com.strumenta.kolasu.model.kReferenceByNameProperties
import com.strumenta.kolasu.parsing.ASTParser
import com.strumenta.kolasu.parsing.KolasuLexer
import com.strumenta.kolasu.parsing.KolasuParser
import com.strumenta.kolasu.parsing.KolasuToken
import com.strumenta.kolasu.parsing.ParsingResult
import com.strumenta.kolasu.traversing.findByPosition
import com.strumenta.kolasu.traversing.walk
import com.strumenta.kolasu.validation.IssueSeverity
import org.antlr.runtime.CommonTokenStream
import org.antlr.runtime.Lexer
import org.antlr.runtime.Parser
import org.antlr.runtime.TokenSource
import org.antlr.v4.runtime.CharStreams
import org.antlr.v4.runtime.atn.LexerATNSimulator
import org.apache.lucene.analysis.standard.StandardAnalyzer
import org.apache.lucene.document.Document
import org.apache.lucene.document.Field
Expand All @@ -31,6 +40,10 @@ import org.apache.lucene.search.SortField
import org.apache.lucene.search.SortedNumericSortField
import org.apache.lucene.search.TermQuery
import org.apache.lucene.store.FSDirectory
import org.eclipse.lsp4j.CompletionItem
import org.eclipse.lsp4j.CompletionList
import org.eclipse.lsp4j.CompletionOptions
import org.eclipse.lsp4j.CompletionParams
import org.eclipse.lsp4j.DefinitionParams
import org.eclipse.lsp4j.Diagnostic
import org.eclipse.lsp4j.DiagnosticSeverity
Expand Down Expand Up @@ -100,17 +113,21 @@ open class KolasuServer<T : Node>(
protected open val extensions: List<String> = listOf(),
protected open val enableDefinitionCapability: Boolean = false,
protected open val enableReferencesCapability: Boolean = false,
protected open val generator: CodeGenerator<T>? = null
protected open val generator: CodeGenerator<T>? = null,
protected open val completionEngine: CompletionEngine? = null,
) : LanguageServer, TextDocumentService, WorkspaceService, LanguageClientAware {
protected open lateinit var client: LanguageClient
protected open var configuration: JsonObject = JsonObject()
protected open var traceLevel: String = "off"
protected open val folders: MutableList<String> = mutableListOf()
protected open val files: MutableMap<String, ParsingResult<T>> = mutableMapOf()
protected open val indexPath: Path = Paths.get("indexes", UUID.randomUUID().toString())
protected open var indexPath: Path? = null
protected open lateinit var indexWriter: IndexWriter
protected open lateinit var indexSearcher: IndexSearcher
protected open val uuid = mutableMapOf<Node, String>()
protected open val texts: MutableMap<String, String> = mutableMapOf()

val INDEX_FOLDER = ".starlasu"

override fun getTextDocumentService() = this

Expand Down Expand Up @@ -156,6 +173,12 @@ open class KolasuServer<T : Node>(
capabilities.setDocumentSymbolProvider(true)
capabilities.setDefinitionProvider(this.enableDefinitionCapability)
capabilities.setReferencesProvider(this.enableReferencesCapability)
if (completionEngine != null) {
capabilities.completionProvider = CompletionOptions().apply {
resolveProvider = false
triggerCharacters = completionEngine!!.triggerCharacters
}
}

return CompletableFuture.completedFuture(InitializeResult(capabilities))
}
Expand Down Expand Up @@ -228,14 +251,56 @@ open class KolasuServer<T : Node>(
client.notifyProgress(ProgressParams(Either.forLeft("indexing"), Either.forLeft(WorkDoneProgressEnd())))
}

private fun resolveIndexPath(): Path {
// Allow config from LSP settings JSON
val configured = configuration["indexDir"]?.asString
?: System.getProperty("kolasu.index.dir")
?: System.getenv("KOLASU_INDEX_DIR")

val base = when {
configured != null -> Paths.get(configured)
folders.isNotEmpty() -> { // first workspace folder
val ws = Paths.get(URI(folders.first()))
ws.resolve(INDEX_FOLDER)
}
else -> Paths.get(System.getProperty("user.home"), INDEX_FOLDER)
}

val wsId = if(folders.isNotEmpty()){
Paths.get(URI(folders.first())).toString().hashCode().toString()
}else{
val tmpBase = Paths.get(System.getProperty("java.io.tmpdir"), INDEX_FOLDER)
val uniqueId = UUID.randomUUID().toString()
log("No workspace folders found. Using temporary directory for index: $tmpBase/$uniqueId",
"This may cause the index to be lost after restart.")
"$tmpBase/$uniqueId"
}


return base.resolve("indexes").resolve(wsId)
}

private fun initIndex() {
if (Files.exists(indexPath)) {
indexPath.toFile().deleteRecursively()
if (::indexWriter.isInitialized && indexWriter.isOpen) {
indexWriter.close()
}
var path = resolveIndexPath()
try {
Files.createDirectories(path)
} catch (e: Exception) {
// there was an error where the base was readonly so we can fall back to tmp
val tmpBase = Paths.get(System.getProperty("java.io.tmpdir")).resolve("starlasu-indexes")
Files.createDirectories(tmpBase)
path = tmpBase.resolve(UUID.randomUUID().toString())
Files.createDirectories(path)
}
indexPath = path
val dir = FSDirectory.open(path)
val cfg = IndexWriterConfig(StandardAnalyzer()).apply {
// rebuild cleanly on each (re)index
openMode = IndexWriterConfig.OpenMode.CREATE
}
val indexDirectory = FSDirectory.open(indexPath)
val indexConfiguration =
IndexWriterConfig(StandardAnalyzer()).apply { openMode = IndexWriterConfig.OpenMode.CREATE_OR_APPEND }
indexWriter = IndexWriter(indexDirectory, indexConfiguration)
indexWriter = IndexWriter(dir, cfg)
commitIndex()
}

Expand All @@ -247,24 +312,52 @@ open class KolasuServer<T : Node>(
override fun didOpen(params: DidOpenTextDocumentParams?) {
val uri = params?.textDocument?.uri ?: return
val text = params.textDocument.text
texts[uri] = text

parse(uri, text)
}

override fun didChange(params: DidChangeTextDocumentParams?) {
val uri = params?.textDocument?.uri ?: return
val text = params.contentChanges.first()?.text ?: return
texts[uri] = text

parse(uri, text)
}

override fun didClose(params: DidCloseTextDocumentParams?) {
val uri = params?.textDocument?.uri ?: return
val text = Files.readString(Paths.get(URI(uri)))
texts[uri] = text

parse(uri, text)
}

override fun completion(params: CompletionParams): CompletableFuture<Either<MutableList<CompletionItem>, CompletionList>> {
val uri = params.textDocument.uri
val pos = params.position
val text = texts[uri] ?: ""
val items: CompletableFuture<Either<MutableList<CompletionItem>, CompletionList>> = try {
completionEngine?.complete(uri, text, pos) ?: CompletableFuture.completedFuture(Either.forLeft(mutableListOf()))
} catch (t: Throwable) {
// instead of crashing, log the error for better understanding
client.logTrace(LogTraceParams("completion error", t.stackTraceToString()))
CompletableFuture.completedFuture(Either.forLeft(mutableListOf()))
}
return items
}

override fun resolveCompletionItem(
item: CompletionItem
): CompletableFuture<CompletionItem> =
CompletableFuture.completedFuture(
try { completionEngine?.resolve(item) ?: item }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would have moved this CompletableFuture to the engine interface as well for consistency, even though we've rarely used that method. However we can keep it like that and open an issue if this is blocking further work on code completion.

catch (t: Throwable) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's rarely appropriate to catch Throwable as that includes system errors such as OutOfMemoryError so unless there are compelling reasons to keep it like that I'd just catch Exception

client.logTrace(LogTraceParams("resolve error", t.stackTraceToString()))
item
}
)

open fun parse(
uri: String,
text: String
Expand All @@ -273,10 +366,10 @@ open class KolasuServer<T : Node>(
val parsingResult = parser?.parse(text, source = URLSource(URI.create(uri).toURL())) ?: return
files[uri] = parsingResult

val tree = parsingResult.root ?: return

updateIndex(uri, tree)
reportDiagnostics(parsingResult, tree, uri)
if (parsingResult.root != null) {
updateIndex(uri, parsingResult.root!!)
}
reportDiagnostics(parsingResult, parsingResult.root!!, uri)
}

private fun updateIndex(uri: String, tree: Node) {
Expand Down Expand Up @@ -606,7 +699,7 @@ open class KolasuServer<T : Node>(

protected open fun commitIndex() {
indexWriter.commit()
val reader = DirectoryReader.open(FSDirectory.open(indexPath))
val reader = DirectoryReader.open(indexWriter)
indexSearcher = IndexSearcher(reader)
}
}
Expand Down