-
Couldn't load subscription status.
- Fork 1
Adding code completion #87
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
base: main
Are you sure you want to change the base?
Changes from all commits
466ef40
4307ed2
425f2ce
ad3bdec
068436f
d3a0f7b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
@@ -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 | ||
|
|
||
|
|
@@ -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)) | ||
| } | ||
|
|
@@ -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") | ||
tiagobstr marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| 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() | ||
| } | ||
|
|
||
|
|
@@ -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 } | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
|
@@ -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) { | ||
|
|
@@ -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) | ||
| } | ||
| } | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.