diff --git a/build.gradle.kts b/build.gradle.kts index cccd800..43ab342 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,10 +6,18 @@ allprojects { group = "com.strumenta.kolasu.languageserver" } +subprojects { + repositories { + mavenLocal() + mavenCentral() + gradlePluginPortal() + } +} + release { buildTasks.set(listOf(":kolasu-languageserver-library:publish", ":kolasu-languageserver-testing:publish", ":kolasu-languageserver-plugin:publishPlugins")) git { requireBranch.set("") pushToRemote.set("origin") } -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index dfab492..4d96796 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,6 +1,6 @@ -version=1.0.5-SNAPSHOT +version=1.0.5-lsp-experiment-46 kotlinVersion=1.8.22 kolasuVersion=1.5.45 lsp4jVersion=0.21.1 luceneVersion=9.8.0 -junitVersion=5.7.1 \ No newline at end of file +junitVersion=5.7.1 diff --git a/library/build.gradle.kts b/library/build.gradle.kts index e3a60a4..793eb19 100644 --- a/library/build.gradle.kts +++ b/library/build.gradle.kts @@ -6,15 +6,12 @@ plugins { signing } -repositories { - mavenCentral() -} - dependencies { implementation(libs.kotlin.reflect) implementation(libs.kolasu.core) + implementation(libs.kolasu.semantics) implementation(libs.lsp4j) - implementation(libs.lucene) + implementation(libs.antlr4.c3) } java { diff --git a/library/src/main/kotlin/com/strumenta/kolasu/languageserver/KolasuServer.kt b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/KolasuServer.kt index 0d7e4cc..c3aab24 100644 --- a/library/src/main/kotlin/com/strumenta/kolasu/languageserver/KolasuServer.kt +++ b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/KolasuServer.kt @@ -1,584 +1,156 @@ package com.strumenta.kolasu.languageserver import com.google.gson.JsonObject -import com.strumenta.kolasu.model.Node -import com.strumenta.kolasu.model.Point -import com.strumenta.kolasu.model.PossiblyNamed -import com.strumenta.kolasu.model.ReferenceByName -import com.strumenta.kolasu.model.children -import com.strumenta.kolasu.model.kReferenceByNameProperties -import com.strumenta.kolasu.parsing.ASTParser -import com.strumenta.kolasu.parsing.ParsingResult -import com.strumenta.kolasu.traversing.findByPosition -import com.strumenta.kolasu.traversing.walk -import org.apache.lucene.analysis.standard.StandardAnalyzer -import org.apache.lucene.document.Document -import org.apache.lucene.document.Field -import org.apache.lucene.document.IntField -import org.apache.lucene.document.IntPoint -import org.apache.lucene.document.StringField -import org.apache.lucene.index.DirectoryReader -import org.apache.lucene.index.IndexWriter -import org.apache.lucene.index.IndexWriterConfig -import org.apache.lucene.index.Term -import org.apache.lucene.search.BooleanClause -import org.apache.lucene.search.BooleanQuery -import org.apache.lucene.search.IndexSearcher -import org.apache.lucene.search.Sort -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.DefinitionParams -import org.eclipse.lsp4j.Diagnostic -import org.eclipse.lsp4j.DiagnosticSeverity -import org.eclipse.lsp4j.DidChangeConfigurationParams -import org.eclipse.lsp4j.DidChangeTextDocumentParams -import org.eclipse.lsp4j.DidChangeWatchedFilesParams -import org.eclipse.lsp4j.DidChangeWatchedFilesRegistrationOptions -import org.eclipse.lsp4j.DidChangeWorkspaceFoldersParams -import org.eclipse.lsp4j.DidCloseTextDocumentParams -import org.eclipse.lsp4j.DidOpenTextDocumentParams -import org.eclipse.lsp4j.DidSaveTextDocumentParams -import org.eclipse.lsp4j.DocumentSymbol -import org.eclipse.lsp4j.DocumentSymbolParams -import org.eclipse.lsp4j.FileChangeType -import org.eclipse.lsp4j.FileSystemWatcher +import com.strumenta.kolasu.ids.NodeIdProvider +import com.strumenta.kolasu.languageserver.manager.KolasuClientManager +import com.strumenta.kolasu.languageserver.manager.KolasuWorkspaceManager +import com.strumenta.kolasu.languageserver.service.KolasuTextDocumentService +import com.strumenta.kolasu.languageserver.service.KolasuWorkspaceService +import com.strumenta.kolasu.parsing.KolasuANTLRToken +import com.strumenta.kolasu.parsing.KolasuParser +import com.strumenta.kolasu.semantics.scope.provider.ScopeProvider +import org.antlr.v4.runtime.Parser +import org.eclipse.lsp4j.CompletionOptions import org.eclipse.lsp4j.InitializeParams import org.eclipse.lsp4j.InitializeResult import org.eclipse.lsp4j.InitializedParams -import org.eclipse.lsp4j.Location -import org.eclipse.lsp4j.LocationLink import org.eclipse.lsp4j.LogTraceParams import org.eclipse.lsp4j.MessageActionItem import org.eclipse.lsp4j.MessageParams import org.eclipse.lsp4j.MessageType -import org.eclipse.lsp4j.Position -import org.eclipse.lsp4j.ProgressParams -import org.eclipse.lsp4j.PublishDiagnosticsParams -import org.eclipse.lsp4j.Range -import org.eclipse.lsp4j.ReferenceParams -import org.eclipse.lsp4j.Registration -import org.eclipse.lsp4j.RegistrationParams import org.eclipse.lsp4j.ServerCapabilities import org.eclipse.lsp4j.SetTraceParams import org.eclipse.lsp4j.ShowMessageRequestParams -import org.eclipse.lsp4j.SymbolInformation -import org.eclipse.lsp4j.SymbolKind -import org.eclipse.lsp4j.TextDocumentPositionParams -import org.eclipse.lsp4j.TextDocumentSyncKind -import org.eclipse.lsp4j.TextDocumentSyncOptions -import org.eclipse.lsp4j.WorkDoneProgressBegin -import org.eclipse.lsp4j.WorkDoneProgressCreateParams -import org.eclipse.lsp4j.WorkDoneProgressEnd -import org.eclipse.lsp4j.WorkDoneProgressReport -import org.eclipse.lsp4j.WorkspaceFoldersOptions -import org.eclipse.lsp4j.WorkspaceServerCapabilities -import org.eclipse.lsp4j.jsonrpc.messages.Either +import org.eclipse.lsp4j.TextDocumentSyncKind.Full +import org.eclipse.lsp4j.TraceValue import org.eclipse.lsp4j.launch.LSPLauncher import org.eclipse.lsp4j.services.LanguageClient import org.eclipse.lsp4j.services.LanguageClientAware import org.eclipse.lsp4j.services.LanguageServer -import org.eclipse.lsp4j.services.TextDocumentService -import org.eclipse.lsp4j.services.WorkspaceService -import java.io.File import java.io.InputStream import java.io.OutputStream -import java.net.URI -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths -import java.util.UUID import java.util.concurrent.CompletableFuture import kotlin.system.exitProcess -open class KolasuServer( - protected open val parser: ASTParser?, - protected open val language: String = "", - protected open val extensions: List = listOf(), - protected open val enableDefinitionCapability: Boolean = false, - protected open val enableReferencesCapability: Boolean = false, - protected open val generator: CodeGenerator? = 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 = mutableListOf() - protected open val files: MutableMap> = mutableMapOf() - protected open val indexPath: Path = Paths.get("indexes", UUID.randomUUID().toString()) - protected open lateinit var indexWriter: IndexWriter - protected open lateinit var indexSearcher: IndexSearcher - protected open val uuid = mutableMapOf() - - override fun getTextDocumentService() = this - - override fun getWorkspaceService() = this - - open fun startCommunication( - inputStream: InputStream = System.`in`, - outputStream: OutputStream = System.out - ) { - val launcher = LSPLauncher.createServerLauncher(this, inputStream, outputStream) - connect(launcher.remoteProxy) - launcher.startListening() - } - - override fun connect(client: LanguageClient) { - this.client = client - } - - override fun initialize(params: InitializeParams?): CompletableFuture { - val workspaceFolders = params?.workspaceFolders - if (workspaceFolders != null) { - for (folder in workspaceFolders) { - folders.add(folder.uri) - } - } - - val capabilities = ServerCapabilities() - - capabilities.workspace = - WorkspaceServerCapabilities( - WorkspaceFoldersOptions().apply { - supported = true - changeNotifications = Either.forLeft("didChangeWorkspaceFoldersRegistration") - } - ) - - capabilities.setTextDocumentSync( - TextDocumentSyncOptions().apply { - openClose = true - change = TextDocumentSyncKind.Full - } - ) - capabilities.setDocumentSymbolProvider(true) - capabilities.setDefinitionProvider(this.enableDefinitionCapability) - capabilities.setReferencesProvider(this.enableReferencesCapability) - - return CompletableFuture.completedFuture(InitializeResult(capabilities)) - } - - override fun initialized(params: InitializedParams?) { - val watchers = mutableListOf() - for (folder in folders) { - watchers.add(FileSystemWatcher(Either.forLeft(URI(folder).path + """/**/*{${extensions.joinToString(","){".$it"}}}"""))) - } - client.registerCapability( - RegistrationParams( - listOf( - Registration( - "workspace/didChangeWatchedFiles", - "workspace/didChangeWatchedFiles", - DidChangeWatchedFilesRegistrationOptions(watchers) - ) - ) - ) - ) - - client.registerCapability( - RegistrationParams( - listOf( - Registration( - "workspace/didChangeConfiguration", - "workspace/didChangeConfiguration", - object { - val section = language - } - ) - ) - ) - ) - } - - override fun didChangeConfiguration(params: DidChangeConfigurationParams?) { - val settings = params?.settings as? JsonObject ?: return - configuration = settings[language].asJsonObject - - if (Files.exists(indexPath)) { - indexPath.toFile().deleteRecursively() - } - val indexDirectory = FSDirectory.open(indexPath) - val indexConfiguration = IndexWriterConfig(StandardAnalyzer()).apply { openMode = IndexWriterConfig.OpenMode.CREATE_OR_APPEND } - indexWriter = IndexWriter(indexDirectory, indexConfiguration) - commitIndex() - - client.createProgress(WorkDoneProgressCreateParams(Either.forLeft("indexing"))) - client.notifyProgress( - ProgressParams(Either.forLeft("indexing"), Either.forLeft(WorkDoneProgressBegin().apply { title = "indexing" })) - ) - for (folder in folders) { - val projectFiles = File(URI(folder)).walk().filter { extensions.contains(it.extension) }.toList() - val totalBytes = projectFiles.sumOf { it.length() } - var parsedBytes = 0L - for (file in projectFiles) { - parse(file.toURI().toString(), Files.readString(file.toPath())) - parsedBytes += file.length() - val percentage = (parsedBytes * 100 / totalBytes).toInt() - client.notifyProgress( - ProgressParams( - Either.forLeft("indexing"), - Either.forLeft( - WorkDoneProgressReport().apply { - this.percentage = percentage - } - ) - ) - ) - } - } - client.notifyProgress(ProgressParams(Either.forLeft("indexing"), Either.forLeft(WorkDoneProgressEnd()))) - } - - override fun setTrace(params: SetTraceParams?) { - val level = params?.value ?: return - traceLevel = level - } - - override fun didOpen(params: DidOpenTextDocumentParams?) { - val uri = params?.textDocument?.uri ?: return - val text = params.textDocument.text - - parse(uri, text) - } - - override fun didChange(params: DidChangeTextDocumentParams?) { - val uri = params?.textDocument?.uri ?: return - val text = params.contentChanges.first()?.text ?: return - - parse(uri, text) - } - - override fun didClose(params: DidCloseTextDocumentParams?) { - val uri = params?.textDocument?.uri ?: return - val text = Files.readString(Paths.get(URI(uri))) - - parse(uri, text) - } - - open fun parse( - uri: String, - text: String - ) { - if (!::indexWriter.isInitialized) return - - val parsingResult = parser?.parse(text) ?: return - files[uri] = parsingResult - - val tree = parsingResult.root ?: return - - indexWriter.deleteDocuments(TermQuery(Term("uri", uri))) - commitIndex() - var id = 0 - for (node in tree.walk()) { - uuid[node] = "$uri${id++}" - } - for (node in tree.walk()) { - val document = Document() - if (uuid[node] == null) continue - if (node == tree) { - document.add(IntField("root", 1, Field.Store.YES)) - } - document.add(StringField("uuid", uuid[node], Field.Store.YES)) - document.add(StringField("uri", uri, Field.Store.YES)) - node.position?.let { position -> - document.add(IntField("startLine", position.start.line, Field.Store.YES)) - document.add(IntField("startColumn", position.start.column, Field.Store.YES)) - document.add(IntField("endLine", position.end.line, Field.Store.YES)) - document.add(IntField("endColumn", position.end.column, Field.Store.YES)) - document.add(IntField("size", sizeOf(position), Field.Store.YES)) - } - - if (node is PossiblyNamed && node.name != null) { - document.add(StringField("name", node.name, Field.Store.YES)) - } - - // handle reference by name properties - node.kReferenceByNameProperties() - .mapNotNull { it.get(node) as? ReferenceByName<*> } - .mapNotNull { it.referred as? Node } - .mapNotNull { uuid[it] } - .map { StringField("reference", it, Field.Store.YES) } - .forEach(document::add) - - indexWriter.addDocument(document) - } - commitIndex() - - val showASTWarnings = configuration["showASTWarnings"]?.asBoolean ?: false - val showLeafPositions = configuration["showLeafPositions"]?.asBoolean ?: false - val showParsingErrors = configuration["showParsingErrors"]?.asBoolean ?: true - - val diagnostics = ArrayList() - - if (showParsingErrors) { - for (issue in parsingResult.issues) { - diagnostics.add(Diagnostic(toLSPRange(issue.position!!), issue.message)) - } - } - if (showASTWarnings || showLeafPositions) { - for (node in tree.walk()) { - if (node.children.isNotEmpty() || node.position == null) continue - - if (showASTWarnings && tree.findByPosition(node.position!!) != node) { - diagnostics.add( - Diagnostic( - toLSPRange(node.position!!), - "Leaf type: ${node.simpleNodeType} but findByPositionType: ${tree.findByPosition( - node.position!! - )?.simpleNodeType}" - ).apply { - severity = DiagnosticSeverity.Warning - } - ) - } - - if (showLeafPositions) { - diagnostics.add( - Diagnostic( - toLSPRange(node.position!!), - "Leaf position: ${node.position}, Source text: ${node.sourceText}" - ).apply { - severity = DiagnosticSeverity.Information - } - ) - } - } - } - client.publishDiagnostics(PublishDiagnosticsParams(uri, diagnostics)) - } - - // ? This is a bad workaround. In case nodes contain the sourceText field set properly, its length could be used instead - protected open fun sizeOf(position: com.strumenta.kolasu.model.Position): Int { - val lineDifference = position.end.line - position.start.line - val lineValue = lineDifference * 8000 - val columnDifference = position.end.column - position.start.column - - return lineValue + columnDifference - } - - override fun documentSymbol(params: DocumentSymbolParams?): CompletableFuture>> { - val uri = params?.textDocument?.uri ?: return CompletableFuture.completedFuture(null) - val root = files[uri]?.root ?: return CompletableFuture.completedFuture(null) - val range = toLSPRange(root.position!!) - - val namedTree = DocumentSymbol("Named tree", SymbolKind.Variable, range, range, "", mutableListOf()) - appendNamedChildren(root, namedTree) - - return CompletableFuture.completedFuture(mutableListOf(Either.forRight(namedTree))) - } - - protected open fun appendNamedChildren( - node: Node, - parent: DocumentSymbol - ) { - var nextParent = parent - if (node is PossiblyNamed && node.name != null) { - val range = toLSPRange(node.position!!) - val symbol = DocumentSymbol(node.name, symbolKindOf(node), range, range, "", mutableListOf()) - parent.children.add(symbol) - nextParent = symbol - } - node.children.forEach { child -> - appendNamedChildren(child, nextParent) - } - } - - protected open fun symbolKindOf(node: Node): SymbolKind { - return SymbolKind.Variable - } - - protected open fun symbolKindOf(document: Document): SymbolKind { - return SymbolKind.Variable - } - - override fun definition( - params: DefinitionParams? - ): CompletableFuture, MutableList>> { - val document = getDocument(params) ?: return CompletableFuture.completedFuture(null) - - val symbolID = document.fields.find { it.name() == "reference" }?.stringValue() ?: return CompletableFuture.completedFuture(null) - val result = - indexSearcher.search( - TermQuery(Term("uuid", symbolID)), - 1 - ).scoreDocs.firstOrNull() ?: return CompletableFuture.completedFuture(null) - val definition = indexSearcher.storedFields().document(result.doc) - - if (definition.fields.none { it.name() == "startLine" }) return CompletableFuture.completedFuture(null) - val location = toLSPLocation(definition) - - return CompletableFuture.completedFuture(Either.forLeft(mutableListOf(location))) - } - - override fun references(params: ReferenceParams?): CompletableFuture> { - val document = getDocument(params) ?: return CompletableFuture.completedFuture(null) - - val symbolID = document.fields.find { it.name() == "reference" }?.stringValue() ?: return CompletableFuture.completedFuture(null) - val results = indexSearcher.search(TermQuery(Term("reference", symbolID)), Int.MAX_VALUE).scoreDocs - - val list = mutableListOf() - for (result in results) { - val reference = indexSearcher.storedFields().document(result.doc) - list.add(toLSPLocation(reference)) - } - - if (params?.context?.isIncludeDeclaration == true) { - val result = - indexSearcher.search( - TermQuery(Term("uuid", symbolID)), - 1 - ).scoreDocs.firstOrNull() ?: return CompletableFuture.completedFuture(null) - val definition = indexSearcher.storedFields().document(result.doc) - - list.add(toLSPLocation(definition)) - } - - return CompletableFuture.completedFuture(list) - } - - // ? This is not completely correct, as it assumes that the node at this position is contained in a single line - protected open fun getDocument(params: TextDocumentPositionParams?): Document? { - val uri = params?.textDocument?.uri ?: return null - val position = params.position +class KolasuServer( + val language: String, + val fileExtensions: List, + val kolasuParser: KolasuParser<*, *, *, out KolasuANTLRToken>, + val nodeIdProvider: NodeIdProvider, + val scopeProvider: ScopeProvider, + val antlrParserFactory: (String) -> Parser, + val ignoredTokenIds: Set, + val referenceRuleIds: Set +) : LanguageServer, LanguageClientAware { + + private var configuration: JsonObject = JsonObject() + private var traceLevel: String = TraceValue.Off + + private val clientManager = KolasuClientManager( + this.language, + this.fileExtensions + ) - val query = - BooleanQuery.Builder() - .add(TermQuery(Term("uri", uri)), BooleanClause.Occur.MUST) - .add(IntPoint.newExactQuery("startLine", position.line + 1), BooleanClause.Occur.MUST) - .add(IntPoint.newExactQuery("endLine", position.line + 1), BooleanClause.Occur.MUST) - .add(IntPoint.newRangeQuery("startColumn", Int.MIN_VALUE, position.character), BooleanClause.Occur.MUST) - .add(IntPoint.newRangeQuery("endColumn", position.character, Int.MAX_VALUE), BooleanClause.Occur.MUST) - .build() + private val workspaceManager = KolasuWorkspaceManager( + this.kolasuParser, + this.nodeIdProvider, + this.clientManager + ) - val sortingField = SortedNumericSortField("size", SortField.Type.INT, true) - val results = indexSearcher.search(query, 100, Sort(sortingField)) - if (results.scoreDocs.isEmpty()) return null + private val kolasuTextDocumentService = KolasuTextDocumentService( + this.workspaceManager, + this.scopeProvider, + this.antlrParserFactory, + this.ignoredTokenIds, + this.referenceRuleIds + ) - val documentID = results.scoreDocs.last().doc - return indexSearcher.storedFields().document(documentID) - } + private val kolasuWorkspaceService = KolasuWorkspaceService( + this.workspaceManager + ) - protected open fun toLSPRange(kolasuRange: com.strumenta.kolasu.model.Position): Range { - val start = Position(kolasuRange.start.line - 1, kolasuRange.start.column) - val end = Position(kolasuRange.end.line - 1, kolasuRange.end.column) - return Range(start, end) + override fun initialize(parameters: InitializeParams?): CompletableFuture { + val response = InitializeResult(ServerCapabilities()) + // load workspace manager + parameters?.workspaceFolders?.let(this.workspaceManager::load) + // store client capabilities + this.clientManager.capabilities = parameters?.capabilities + // receive full text on synchronization + response.capabilities.setTextDocumentSync(Full) + // handle smart features (definitions, references, etc.) + // configure definition support + clientManager.takeUnless { it.supportsDynamicDefinitionCapabilityRegistration() } + .let { response.capabilities.setDefinitionProvider(true) } + // configure references support + clientManager.takeUnless { it.supportsDynamicReferencesCapabilityRegistration() } + .let { response.capabilities.setReferencesProvider(true) } + // configure completion support + clientManager.takeUnless { it.supportsDynamicCompletionCapabilityRegistration() } + .let { response.capabilities.completionProvider = CompletionOptions() } + return CompletableFuture.supplyAsync { response } + } + + override fun connect(languageClient: LanguageClient?) { + this.clientManager.languageClient = languageClient + } + + override fun initialized(parameters: InitializedParams?) { + // configure definition support + this.clientManager.registerDefinitionCapability() + // configure references support + this.clientManager.registerReferencesCapability() + // configure completion support + this.clientManager.registerCompletionCapability() + // watch file changes + this.clientManager.watchFileChanges() } - protected open fun toLSPLocation(document: Document): Location { - val uri = document.get("uri") - val range = - Range( - Position(document.get("startLine").toInt() - 1, document.get("startColumn").toInt()), - Position(document.get("endLine").toInt() - 1, document.get("endColumn").toInt()) - ) - return Location(uri, range) + override fun shutdown(): CompletableFuture { + return CompletableFuture.completedFuture(null) } - protected open fun toKolasuRange(position: Position): com.strumenta.kolasu.model.Position { - val start = Point(position.line + 1, position.character) - val end = Point(position.line + 1, position.character) - return com.strumenta.kolasu.model.Position(start, end) + override fun exit() { + exitProcess(0) } - override fun didSave(params: DidSaveTextDocumentParams?) { - val uri = params?.textDocument?.uri ?: return - val tree = files[uri]?.root ?: return - - generator?.generate(tree, uri) - } + // TODO logging as utility extensions - override fun didChangeWorkspaceFolders(params: DidChangeWorkspaceFoldersParams?) { - val event = params?.event ?: return - for (folder in event.added) { - folders.add(folder.uri) - } - for (folder in event.removed) { - folders.removeIf { it == folder.uri } - } + fun startCommunication(inputStream: InputStream = System.`in`, outputStream: OutputStream = System.out) { + val launcher = LSPLauncher.createServerLauncher(this, inputStream, outputStream) + connect(launcher.remoteProxy) + launcher.startListening() } - override fun didChangeWatchedFiles(params: DidChangeWatchedFilesParams?) { - val changes = params?.changes ?: return - for (change in changes) { - when (change.type) { - FileChangeType.Created -> { - val uri = change.uri - val text = Files.readString(Paths.get(URI(uri))) + override fun getTextDocumentService() = this.kolasuTextDocumentService - parse(uri, text) - } - FileChangeType.Changed -> { - val uri = change.uri - val text = Files.readString(Paths.get(URI(uri))) + override fun getWorkspaceService() = this.kolasuWorkspaceService - parse(uri, text) - } - FileChangeType.Deleted -> { - files.remove(change.uri) - } - null -> {} - } + override fun setTrace(parameters: SetTraceParams?) { + this.traceLevel = when (val traceLevel = parameters?.value) { + TraceValue.Verbose, TraceValue.Messages -> traceLevel + else -> TraceValue.Off } } - override fun shutdown(): CompletableFuture { - indexWriter.close() - return CompletableFuture.completedFuture(null) - } - - override fun exit() { - exitProcess(0) - } - protected open fun log( - text: String, - verboseExplanation: String? = null - ) { - client.logTrace(LogTraceParams(text, verboseExplanation)) - } + message: String, + verbose: String? = null + ) = this.clientManager.languageClient?.logTrace(LogTraceParams(message, verbose)) protected open fun showNotification( - text: String, - messageType: MessageType = MessageType.Info - ) { - client.showMessage(MessageParams(messageType, text)) - } + type: MessageType = MessageType.Info, + message: String, + ) = this.clientManager.languageClient?.showMessage(MessageParams(type, message)) protected open fun askClient( - messageText: String, - options: List = listOf("Yes", "No"), - messageType: MessageType = MessageType.Info - ): CompletableFuture { - val future = CompletableFuture() - - val request = - ShowMessageRequestParams(options.map { MessageActionItem(it) }).apply { - type = messageType - message = messageText - } - - client.showMessageRequest(request).thenApply { item -> future.complete(item.title) } - - return future + message: String, + type: MessageType = MessageType.Info, + actions: List = listOf("Yes", "No"), + ) = CompletableFuture().apply { + clientManager.languageClient?.showMessageRequest( + ShowMessageRequestParams(actions.map { MessageActionItem(it) }) + .apply { this.type = type; this.message = message } + )?.thenApply { this.complete(it.title) } } - protected open fun commitIndex() { - indexWriter.commit() - val reader = DirectoryReader.open(FSDirectory.open(indexPath)) - indexSearcher = IndexSearcher(reader) - } -} - -interface CodeGenerator { - fun generate( - tree: T, - uri: String - ) -} - -interface SymbolResolver { - fun resolveSymbols( - tree: Node?, - uri: String - ) } diff --git a/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuClientManager.kt b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuClientManager.kt new file mode 100644 index 0000000..132f04f --- /dev/null +++ b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuClientManager.kt @@ -0,0 +1,83 @@ +package com.strumenta.kolasu.languageserver.manager + +import com.strumenta.kolasu.languageserver.utils.kolasuPositionToLsp4jRange +import com.strumenta.kolasu.validation.Issue +import org.eclipse.lsp4j.ClientCapabilities +import org.eclipse.lsp4j.CompletionRegistrationOptions +import org.eclipse.lsp4j.DefinitionRegistrationOptions +import org.eclipse.lsp4j.Diagnostic +import org.eclipse.lsp4j.DidChangeWatchedFilesRegistrationOptions +import org.eclipse.lsp4j.DocumentFilter +import org.eclipse.lsp4j.FileSystemWatcher +import org.eclipse.lsp4j.PublishDiagnosticsParams +import org.eclipse.lsp4j.ReferenceRegistrationOptions +import org.eclipse.lsp4j.Registration +import org.eclipse.lsp4j.RegistrationParams +import org.eclipse.lsp4j.jsonrpc.messages.Either +import org.eclipse.lsp4j.services.LanguageClient +import java.util.* + +class KolasuClientManager( + private val language: String, + private val fileExtensions: List +) { + + private val documentSelector: List by lazy { + this.fileExtensions.map { DocumentFilter(this.language, "file", "*.${it}") } + } + + var languageClient: LanguageClient? = null + var capabilities: ClientCapabilities? = null + + fun updateFileIssues(fileUri: String, issues: List) { + this.languageClient?.publishDiagnostics(PublishDiagnosticsParams(fileUri, issues.mapNotNull { issue -> + issue.position?.let { kolasuPositionToLsp4jRange(it) }?.let { Diagnostic(it, issue.message) } + })) + } + + fun supportsDynamicDefinitionCapabilityRegistration(): Boolean { + return this.capabilities?.textDocument?.definition?.dynamicRegistration ?: false + } + + fun supportsDynamicReferencesCapabilityRegistration(): Boolean { + return this.capabilities?.textDocument?.references?.dynamicRegistration ?: false + } + + fun supportsDynamicCompletionCapabilityRegistration(): Boolean { + return this.capabilities?.textDocument?.completion?.dynamicRegistration ?: false + } + + fun watchFileChanges() { + this.registerSupport("workspace/didChangeWatchedFiles", DidChangeWatchedFilesRegistrationOptions( + this.fileExtensions.map { FileSystemWatcher(Either.forLeft("/**/*.${it}")) } + )) + } + + fun registerDefinitionCapability() { + if (this.supportsDynamicDefinitionCapabilityRegistration()) { + this.registerSupport("textDocument/definition", DefinitionRegistrationOptions() + .also { it.documentSelector = this.documentSelector }) + } + } + + fun registerReferencesCapability() { + if (this.supportsDynamicReferencesCapabilityRegistration()) { + this.registerSupport("textDocument/references", ReferenceRegistrationOptions() + .also { it.documentSelector = this.documentSelector }) + } + } + + fun registerCompletionCapability() { + if (this.supportsDynamicCompletionCapabilityRegistration()) { + this.registerSupport("textDocument/completion", CompletionRegistrationOptions() + .also { it.documentSelector = this.documentSelector}) + } + } + + private fun registerSupport(method: String, options: Any) { + val registration = Registration(UUID.randomUUID().toString(), method, options) + val parameters = RegistrationParams(listOf(registration)) + this.languageClient?.registerCapability(parameters) + } + +} diff --git a/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuCompletionManager.kt b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuCompletionManager.kt new file mode 100644 index 0000000..963c714 --- /dev/null +++ b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuCompletionManager.kt @@ -0,0 +1,115 @@ +package com.strumenta.kolasu.languageserver.manager + +import com.strumenta.kolasu.languageserver.utils.kolasuPositionToLsp4jRange +import com.strumenta.kolasu.model.Node +import com.strumenta.kolasu.model.pos +import com.strumenta.kolasu.parsing.position +import com.strumenta.kolasu.semantics.scope.provider.ScopeProvider +import com.strumenta.kolasu.traversing.findByPosition +import com.vmware.antlr4c3.CodeCompletionCore +import com.vmware.antlr4c3.CodeCompletionCore.CandidatesCollection +import org.antlr.v4.runtime.Parser +import org.antlr.v4.runtime.Token +import org.eclipse.lsp4j.CompletionItem +import org.eclipse.lsp4j.CompletionItemKind.Variable +import org.eclipse.lsp4j.TextEdit +import org.eclipse.lsp4j.jsonrpc.messages.Either +import com.strumenta.kolasu.model.Position as KolasuPosition +import org.eclipse.lsp4j.Position as Lsp4jPosition +import org.eclipse.lsp4j.Range as Lsp4jRange + +class KolasuCompletionManager( + private val workspaceManager: KolasuWorkspaceManager, + private val scopeProvider: ScopeProvider, + private val antlrParserFactory: (String) -> Parser, + private val ignoredTokenIds: Set, + private val referenceRuleIds: Set, +) { + + fun completionFor(uri: String, kolasuPosition: KolasuPosition): List { + val completions: MutableList = mutableListOf() + this.workspaceManager.get(uri).let { file -> + this.createAntlrParser(file)?.let { antlrParser -> + val engine = createCompletionEngine(antlrParser) + this.findCurrentToken(file, kolasuPosition) + ?.let { currentToken -> this.getCompletionsFrom(currentToken, engine, file, antlrParser) } + ?.forEach(completions::add) +// this.findPreviousToken(file, kolasuPosition) +// ?.let { previousToken -> this.getCompletionsFrom(previousToken, engine, file, antlrParser) } +// ?.forEach(completions::add) + } + } + return completions + } + + private fun findCurrentToken(file: KolasuFileManager, kolasuPosition: KolasuPosition): Token? { + return file.tokens().find { it.position.contains(kolasuPosition) } + } + + private fun findPreviousToken(file: KolasuFileManager, kolasuPosition: KolasuPosition): Token? { + return file.tokens().findLast { it.position < kolasuPosition } + } + + private fun createCompletionEngine(antlrParser: Parser): CodeCompletionCore { + return CodeCompletionCore(antlrParser, this.referenceRuleIds, this.ignoredTokenIds) + } + + private fun createAntlrParser(file: KolasuFileManager): Parser? { + return file.root()?.sourceText?.let { this.antlrParserFactory(it) } + } + + private fun getCompletionsFrom( + token: Token, + engine: CodeCompletionCore, + file: KolasuFileManager, + antlrParser: Parser, + ): List { + val completions: MutableList = mutableListOf() + engine.collectCandidates(token.tokenIndex, null)?.let { candidates -> + if (candidates.containsReferenceRules()) { + file.findNode(token) + ?.let { this.scopeProvider.from(it) }?.names() + ?.mapNotNull { this.buildCompletionItem(token, it) } + ?.forEach(completions::add) + } +// candidates.tokenLiterals(antlrParser) +// .mapNotNull { buildCompletionItem(token, it) } +// .forEach(completions::add) + } + return completions + } + + private fun CandidatesCollection.containsReferenceRules(): Boolean { + return this.rules.keys.any { referenceRuleIds.contains(it) } + } + + private fun KolasuFileManager.findNode(token: Token): Node? { + return this.root()?.findByPosition(token.position) + } + + private fun CandidatesCollection.tokenLiterals(antlrParser: Parser): List { + return this.tokens.keys.mapNotNull { antlrParser.vocabulary.getLiteralName(it) } + } + + private fun buildCompletionItem(token: Token, text: String): CompletionItem? { + return kolasuPositionToLsp4jRange(token.position).let { tokenRange -> + CompletionItem(text).apply { + kind = Variable + textEdit = Either.forLeft(textEditFrom(tokenRange, text)) + } + } + } + + private fun textEditFrom(tokenRange: Lsp4jRange, text: String): TextEdit { + return TextEdit(this.textEditRangeFrom(tokenRange, text), text) + } + + private fun textEditRangeFrom(tokenRange: Lsp4jRange, text: String): Lsp4jRange { + return Lsp4jRange(tokenRange.start, this.textEditRangeEndFrom(tokenRange, text)) + } + + private fun textEditRangeEndFrom(tokenRange: Lsp4jRange, text: String): Lsp4jPosition { + return Lsp4jPosition(tokenRange.end.line, tokenRange.start.character + text.length) + } + +} diff --git a/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuFileManager.kt b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuFileManager.kt new file mode 100644 index 0000000..5fb4bca --- /dev/null +++ b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuFileManager.kt @@ -0,0 +1,57 @@ +package com.strumenta.kolasu.languageserver.manager + +import com.strumenta.kolasu.ids.NodeIdProvider +import com.strumenta.kolasu.languageserver.repository.kolasuServerSymbolProvider +import com.strumenta.kolasu.model.Node +import com.strumenta.kolasu.parsing.KolasuANTLRToken +import com.strumenta.kolasu.parsing.KolasuParser +import com.strumenta.kolasu.parsing.LexingResult +import com.strumenta.kolasu.parsing.ParsingResult +import com.strumenta.kolasu.semantics.symbol.provider.SymbolProvider +import com.strumenta.kolasu.semantics.symbol.repository.SymbolRepository +import com.strumenta.kolasu.traversing.walk +import com.strumenta.kolasu.validation.Issue +import org.antlr.v4.runtime.Token + +class KolasuFileManager( + private val uri: String, + private val parser: KolasuParser<*, *, *, out KolasuANTLRToken>, + private val symbols: SymbolRepository, + private val identifiers: NodeIdProvider, + private val client: KolasuClientManager +) { + + private lateinit var parsingResult: ParsingResult<*> + + private val symbolProvider: SymbolProvider = kolasuServerSymbolProvider(uri, this.identifiers) + + fun tokens(): Sequence { + return this.parser.tokenFactory.extractTokens(this.parsingResult) + ?.tokens?.map { it.token }?.asSequence() ?: emptySequence() + } + + fun root(): Node? = this.parsingResult.root + + fun synchronizeWith(text: String) { + this.deleteSymbols() + this.updateParsingResult(text) + } + + fun deleteSymbols() { + this.symbols + .loadAll { it.fields["uri"]?.value == this.uri } + .toList() + .forEach { this.symbols.delete(it.identifier) } + } + + private fun updateParsingResult(text: String) { + this.parsingResult = this.parser.parse(text) + val issues: MutableList = this.parsingResult.issues.toMutableList() + this.parsingResult.root?.walk() + ?.mapNotNull { this.symbolProvider.from(it, issues) } + ?.forEach { this.symbols.store(it) } + this.client.updateFileIssues(this.uri, issues) + // if issues.isEmpty -> generateCode + } + +} diff --git a/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuWorkspaceManager.kt b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuWorkspaceManager.kt new file mode 100644 index 0000000..990ff10 --- /dev/null +++ b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/manager/KolasuWorkspaceManager.kt @@ -0,0 +1,60 @@ +package com.strumenta.kolasu.languageserver.manager + +import com.strumenta.kolasu.ids.NodeIdProvider +import com.strumenta.kolasu.languageserver.repository.KolasuServerSymbolRepository +import com.strumenta.kolasu.parsing.KolasuANTLRToken +import com.strumenta.kolasu.parsing.KolasuParser +import org.eclipse.lsp4j.WorkspaceFolder +import java.io.File +import java.net.URI + +class KolasuWorkspaceManager( + private val kolasuParser: KolasuParser<*, *, *, out KolasuANTLRToken>, + private val nodeIdProvider: NodeIdProvider, + private val clientManager: KolasuClientManager, +) { + + val symbolRepository = KolasuServerSymbolRepository() + private val fileManagers: MutableMap = mutableMapOf() + + fun load(folders: List) { + this.clear() + folders.map { File(URI(it.uri)) }.forEach { folder -> + folder.walk().filter { it.isFile }.forEach { file -> + this.set(file.toURI().toString(), file.readText()) + } + } + } + + private fun clear() { + this.symbolRepository.clear() + this.fileManagers.clear() + } + + fun get(uri: String): KolasuFileManager { + return this.fileManagers.getOrPut(uri) { + KolasuFileManager(uri, this.kolasuParser, this.symbolRepository, this.nodeIdProvider, this.clientManager) + } + } + + fun set(uri: String, text: String = File(URI(uri)).readText()) { + val fileManager = this.get(uri) + fileManager.synchronizeWith(text) + } + + fun delete(uri: String) { + val fileManager = this.get(uri) + fileManager.deleteSymbols() + this.fileManagers.remove(uri) + } + + fun close(uri: String) { + this.set(uri) + this.fileManagers.remove(uri) + } + + fun rename(oldUri: String, newUri: String) { + // TODO handle file renaming + } + +} diff --git a/library/src/main/kotlin/com/strumenta/kolasu/languageserver/repository/KolasuServerSymbolProvider.kt b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/repository/KolasuServerSymbolProvider.kt new file mode 100644 index 0000000..c7865a7 --- /dev/null +++ b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/repository/KolasuServerSymbolProvider.kt @@ -0,0 +1,19 @@ +package com.strumenta.kolasu.languageserver.repository + +import com.strumenta.kolasu.ids.NodeIdProvider +import com.strumenta.kolasu.languageserver.utils.kolasuPositionToLsp4jRange +import com.strumenta.kolasu.model.Node +import com.strumenta.kolasu.model.PossiblyNamed +import com.strumenta.kolasu.model.kReferenceByNameProperties +import com.strumenta.kolasu.semantics.symbol.provider.SymbolProvider +import com.strumenta.kolasu.semantics.symbol.provider.symbolProvider + +fun kolasuServerSymbolProvider(uri: String, nodeIdProvider: NodeIdProvider) = symbolProvider(nodeIdProvider) { + rule(Node::class) { (node) -> + include("name", (node as? PossiblyNamed)?.name ?: nodeIdProvider.id(node)) + include("uri", uri) + node.kReferenceByNameProperties().forEach { referenceByNameProperty -> + include(referenceByNameProperty.name, referenceByNameProperty.get(node)) + } + } +} diff --git a/library/src/main/kotlin/com/strumenta/kolasu/languageserver/repository/KolasuServerSymbolRepository.kt b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/repository/KolasuServerSymbolRepository.kt new file mode 100644 index 0000000..43769d3 --- /dev/null +++ b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/repository/KolasuServerSymbolRepository.kt @@ -0,0 +1,34 @@ +package com.strumenta.kolasu.languageserver.repository + +import com.strumenta.kolasu.languageserver.utils.sortedByNodePosition +import com.strumenta.kolasu.model.Node +import com.strumenta.kolasu.model.Position +import com.strumenta.kolasu.model.pos +import com.strumenta.kolasu.semantics.symbol.description.SymbolDescription +import com.strumenta.kolasu.semantics.symbol.repository.InMemorySymbolRepository +import com.strumenta.kolasu.semantics.symbol.repository.SymbolRepository +import kotlin.reflect.KClass + +class KolasuServerSymbolRepository: SymbolRepository by InMemorySymbolRepository() { + + fun delete(symbol: SymbolDescription): Boolean { + return this.delete(symbol.identifier) + } + + fun getByPosition(position: Position): SymbolDescription { + val symbolDescription = this.findByPosition(position) + check (symbolDescription != null) { + "Error while retrieving symbol by position - not found" + } + return symbolDescription + } + + fun findByPosition(position: Position): SymbolDescription? { + return this.searchByPosition(position).firstOrNull() + } + + fun searchByPosition(position: Position): Sequence { + return this.loadAll { it.contains(position) }.sortedByNodePosition() + } + +} diff --git a/library/src/main/kotlin/com/strumenta/kolasu/languageserver/service/KolasuTextDocumentService.kt b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/service/KolasuTextDocumentService.kt new file mode 100644 index 0000000..713179f --- /dev/null +++ b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/service/KolasuTextDocumentService.kt @@ -0,0 +1,106 @@ +package com.strumenta.kolasu.languageserver.service + +import com.strumenta.kolasu.languageserver.manager.KolasuCompletionManager +import com.strumenta.kolasu.languageserver.manager.KolasuWorkspaceManager +import com.strumenta.kolasu.languageserver.utils.findReferenceFieldAtPosition +import com.strumenta.kolasu.languageserver.utils.findReferencesFieldsWithTarget +import com.strumenta.kolasu.languageserver.utils.getUri +import com.strumenta.kolasu.languageserver.utils.kolasuPositionToLsp4jRange +import com.strumenta.kolasu.languageserver.utils.lsp4jPositionToKolasuPosition +import com.strumenta.kolasu.semantics.scope.provider.ScopeProvider +import org.antlr.v4.runtime.Parser +import org.eclipse.lsp4j.CompletionItem +import org.eclipse.lsp4j.CompletionList +import org.eclipse.lsp4j.CompletionParams +import org.eclipse.lsp4j.DefinitionParams +import org.eclipse.lsp4j.DidChangeTextDocumentParams +import org.eclipse.lsp4j.DidCloseTextDocumentParams +import org.eclipse.lsp4j.DidOpenTextDocumentParams +import org.eclipse.lsp4j.DidSaveTextDocumentParams +import org.eclipse.lsp4j.Location +import org.eclipse.lsp4j.LocationLink +import org.eclipse.lsp4j.ReferenceParams +import org.eclipse.lsp4j.jsonrpc.messages.Either +import org.eclipse.lsp4j.services.TextDocumentService +import java.util.concurrent.CompletableFuture + +class KolasuTextDocumentService( + private val workspaceManager: KolasuWorkspaceManager, + private val scopeProvider: ScopeProvider, + private val antlrParserFactory: (String) -> Parser, + private val ignoredTokenIds: Set, + private val referenceRuleIds: Set, +) : TextDocumentService { + + private val completionManager: KolasuCompletionManager = KolasuCompletionManager( + this.workspaceManager, + this.scopeProvider, + this.antlrParserFactory, + this.ignoredTokenIds, + this.referenceRuleIds + ) + + override fun didOpen(parameters: DidOpenTextDocumentParams?) { + val uri = parameters?.textDocument?.uri + val text = parameters?.textDocument?.text + if (uri != null && text != null) { + this.workspaceManager.set(uri, text) + } + } + + override fun didChange(parameters: DidChangeTextDocumentParams?) { + val uri = parameters?.textDocument?.uri + val text = parameters?.contentChanges?.firstOrNull { it.range == null }?.text + if (uri != null && text != null) { + this.workspaceManager.set(uri, text) + } + } + + override fun didSave(parameters: DidSaveTextDocumentParams?) { + val uri = parameters?.textDocument?.uri + val text = parameters?.text + if (uri != null && text != null) { + this.workspaceManager.set(uri, text) + } + } + + override fun didClose(parameters: DidCloseTextDocumentParams?) { + parameters?.textDocument?.uri?.let(this.workspaceManager::close) + } + + override fun definition(parameters: DefinitionParams?): CompletableFuture, MutableList>> { + val definitions: MutableList = mutableListOf() + parameters?.position?.let { lsp4jPositionToKolasuPosition(it) }?.let { position -> + this.workspaceManager.symbolRepository.findByPosition(position) + ?.findReferenceFieldAtPosition(position)?.value + ?.let { this.workspaceManager.symbolRepository.load(it) } + ?.let { Location(it.getUri(), kolasuPositionToLsp4jRange(it.position!!))} + ?.let(definitions::add) + } + return CompletableFuture.completedFuture(Either.forLeft(definitions)) + } + + override fun references(parameters: ReferenceParams?): CompletableFuture> { + val references: MutableList = mutableListOf() + parameters?.position?.let { lsp4jPositionToKolasuPosition(it) }?.let { position -> + this.workspaceManager.symbolRepository.findByPosition(position)?.let { target -> + this.workspaceManager.symbolRepository.loadAll().flatMap { source -> + source.findReferencesFieldsWithTarget(target.identifier) + .map { Location(source.getUri(), kolasuPositionToLsp4jRange(it.position!!)) } + }.forEach(references::add) + } + } + return CompletableFuture.completedFuture(references) + } + + override fun completion(parameters: CompletionParams?): CompletableFuture, CompletionList>> { + val completions: MutableList = mutableListOf() + val uri = parameters?.textDocument?.uri + val kolasuPosition = parameters?.position?.let { lsp4jPositionToKolasuPosition(it) } + if (uri != null && kolasuPosition != null) { + this.completionManager.completionFor(uri, kolasuPosition) + .forEach(completions::add) + } + return CompletableFuture.completedFuture(Either.forLeft(completions)) + } +} diff --git a/library/src/main/kotlin/com/strumenta/kolasu/languageserver/service/KolasuWorkspaceService.kt b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/service/KolasuWorkspaceService.kt new file mode 100644 index 0000000..5efd550 --- /dev/null +++ b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/service/KolasuWorkspaceService.kt @@ -0,0 +1,32 @@ +package com.strumenta.kolasu.languageserver.service + +import com.strumenta.kolasu.languageserver.manager.KolasuWorkspaceManager +import org.eclipse.lsp4j.DidChangeConfigurationParams +import org.eclipse.lsp4j.DidChangeWatchedFilesParams +import org.eclipse.lsp4j.FileChangeType.Changed +import org.eclipse.lsp4j.FileChangeType.Created +import org.eclipse.lsp4j.FileChangeType.Deleted +import org.eclipse.lsp4j.FileEvent +import org.eclipse.lsp4j.RenameFilesParams +import org.eclipse.lsp4j.services.WorkspaceService + +class KolasuWorkspaceService( + private val workspaceManager: KolasuWorkspaceManager +): WorkspaceService { + + override fun didChangeConfiguration(parameters: DidChangeConfigurationParams?) { + // TODO handle configuration changesgit a + } + + override fun didChangeWatchedFiles(parameters: DidChangeWatchedFilesParams?) { + parameters?.changes?.filter { it.type == Created || it.type == Changed } + ?.map(FileEvent::getUri)?.forEach(this.workspaceManager::set) + parameters?.changes?.filter { it.type == Deleted } + ?.map(FileEvent::getUri)?.forEach(this.workspaceManager::delete) + } + + override fun didRenameFiles(parameters: RenameFilesParams?) { + parameters?.files?.forEach { this.workspaceManager.rename(it.oldUri, it.newUri) } + } + +} diff --git a/library/src/main/kotlin/com/strumenta/kolasu/languageserver/utils/KolasuServerUtils.kt b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/utils/KolasuServerUtils.kt new file mode 100644 index 0000000..e5caae5 --- /dev/null +++ b/library/src/main/kotlin/com/strumenta/kolasu/languageserver/utils/KolasuServerUtils.kt @@ -0,0 +1,96 @@ +package com.strumenta.kolasu.languageserver.utils + +import com.strumenta.kolasu.semantics.symbol.description.ReferenceValueDescription +import com.strumenta.kolasu.semantics.symbol.description.SymbolDescription +import org.eclipse.lsp4j.Position +import org.eclipse.lsp4j.Range +import com.strumenta.kolasu.model.Point as KolasuPoint +import org.eclipse.lsp4j.Position as Lsp4jPosition +import com.strumenta.kolasu.model.Position as KolasuPosition +import org.eclipse.lsp4j.Range as Lsp4jRange + +fun kolasuPositionToLsp4jRange(kolasuPosition: KolasuPosition): Lsp4jRange { + return Lsp4jRange( + Lsp4jPosition(kolasuPosition.start.line - 1, kolasuPosition.start.column), + Lsp4jPosition(kolasuPosition.end.line - 1, kolasuPosition.end.column) + ) +} + +fun lsp4jPositionToKolasuPosition(lsp4jPosition: Lsp4jPosition): KolasuPosition { + return KolasuPosition( + KolasuPoint(lsp4jPosition.line + 1, lsp4jPosition.character), + KolasuPoint(lsp4jPosition.line + 1, lsp4jPosition.character) + ) +} + +fun kolasuPositionToNodeId(uri: String, kolasuPosition: KolasuPosition): String { + return lsp4jRangeToIdentifier(uri, kolasuPositionToLsp4jRange(kolasuPosition)) +} + +fun kolasuPositionToReferenceNodeId(uri: String, kolasuPosition: KolasuPosition): String { + return "${kolasuPositionToNodeId(uri, kolasuPosition)}:ref" +} + +fun lsp4jRangeToIdentifier(uri: String, lsp4jRange: Lsp4jRange) = + "${uri}:${lsp4jRange.start.line}:${lsp4jRange.start.character}:${lsp4jRange.end.line}:${lsp4jRange.end.character}" + +fun SymbolDescription.toLsp4jRange(): Lsp4jRange { + val startLine = this.fields["startLine"]?.value as? String + val startCharacter = this.fields["startCharacter"]?.value as? String + val startPosition = Position(startLine?.toInt() ?: 0, startCharacter?.toInt() ?: 1) + val endLine = this.fields["endLine"]?.value as? String + val endCharacter = this.fields["endCharacter"]?.value as? String + val endPosition = Position(endLine?.toInt() ?: 0, endCharacter?.toInt() ?: 1) + return Range(startPosition, endPosition) +} + +fun lsp4jRangeFromSymbolDescription(symbolDescription: SymbolDescription): Lsp4jRange { + val startLine = symbolDescription.fields["startLine"]?.value as? String + val startCharacter = symbolDescription.fields["startCharacter"]?.value as? String + val startPosition = Position(startLine?.toInt() ?: 0, startCharacter?.toInt() ?: 1) + val endLine = symbolDescription.fields["endLine"]?.value as? String + val endCharacter = symbolDescription.fields["endCharacter"]?.value as? String + val endPosition = Position(endLine?.toInt() ?: 0, endCharacter?.toInt() ?: 1) + return Range(startPosition, endPosition) +} + +fun SymbolDescription.isReference(): Boolean { + return this.fields["reference"]?.value as? Boolean == true +} + +fun SymbolDescription.getUri(): String? { + return this.fields["uri"]?.value as? String +} + +fun SymbolDescription.getReferenceTarget(): String? { + return this.fields["target"]?.value as? String +} + +fun SymbolDescription.findReferenceFieldAtPosition(kolasuPosition: KolasuPosition): ReferenceValueDescription? { + return this.fields.values.asSequence() + .filterIsInstance() + .filter { it.contains(kolasuPosition) } + .sortedByReferenceByNamePosition().firstOrNull() +} + +fun SymbolDescription.findReferencesFieldsWithTarget(targetNodeId: String): Sequence { + return this.fields.values.asSequence() + .filterIsInstance() + .filter { it.value == targetNodeId } +} + +fun Sequence.sortedByNodePosition(): Sequence { + return this.sortedWith { leftSymbolDescription, rightSymbolDescription -> when { + leftSymbolDescription.contains(rightSymbolDescription.position) -> 1 + rightSymbolDescription.contains(leftSymbolDescription.position) -> -1 + else -> 0 + }} +} + +fun Sequence.sortedByReferenceByNamePosition(): Sequence { + return this.sortedWith { leftReferenceValueDescription, rightReferenceValueDescription -> when { + leftReferenceValueDescription.contains(rightReferenceValueDescription.position) -> 1 + rightReferenceValueDescription.contains(leftReferenceValueDescription.position) -> -1 + else -> 0 + }} +} diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts index 151c0cd..dd11160 100644 --- a/plugin/build.gradle.kts +++ b/plugin/build.gradle.kts @@ -7,11 +7,6 @@ plugins { id("com.github.gmazzo.buildconfig") version "5.3.5" } -repositories { - mavenCentral() - gradlePluginPortal() -} - dependencies { implementation(libs.shadow) implementation(libs.kotlin.jvm) @@ -36,9 +31,9 @@ buildConfig { packageName = "com.strumenta.kolasu.languageserver.plugin" buildConfigField("KOLASU_LSP_VERSION", "${project.version}") buildConfigField("KOLASU_VERSION", libs.versions.kolasu) - buildConfigField("LUCENE_VERSION", libs.versions.lucene) buildConfigField("LSP4J_VERSION", libs.versions.lsp4j) buildConfigField("JUNIT_VERSION", libs.versions.junit5) + buildConfigField("ANTLR4C3_VERSION", libs.versions.antlr4.c3) } ktlint { diff --git a/plugin/src/main/kotlin/com/strumenta/kolasu/languageserver/plugin/LanguageServerPlugin.kt b/plugin/src/main/kotlin/com/strumenta/kolasu/languageserver/plugin/LanguageServerPlugin.kt index 8164d71..7fdcbb0 100644 --- a/plugin/src/main/kotlin/com/strumenta/kolasu/languageserver/plugin/LanguageServerPlugin.kt +++ b/plugin/src/main/kotlin/com/strumenta/kolasu/languageserver/plugin/LanguageServerPlugin.kt @@ -30,10 +30,7 @@ class LanguageServerPlugin : Plugin { project.dependencies.add("implementation", "com.strumenta.kolasu:language-server:${BuildConfig.KOLASU_LSP_VERSION}") project.dependencies.add("implementation", "com.strumenta.kolasu:kolasu-core:${BuildConfig.KOLASU_VERSION}") project.dependencies.add("implementation", "org.eclipse.lsp4j:org.eclipse.lsp4j:${BuildConfig.LSP4J_VERSION}") - project.dependencies.add("implementation", "org.apache.lucene:lucene-core:${BuildConfig.LUCENE_VERSION}") - project.dependencies.add("implementation", "org.apache.lucene:lucene-codecs:${BuildConfig.LUCENE_VERSION}") - project.dependencies.add("implementation", "org.apache.lucene:lucene-queryparser:${BuildConfig.LUCENE_VERSION}") - + project.dependencies.add("implementation", "com.vmware.antlr4-c3:antlr4-c3:${BuildConfig.ANTLR4C3_VERSION}") project.dependencies.add("testImplementation", "com.strumenta.kolasu:language-server-testing:${BuildConfig.KOLASU_LSP_VERSION}") project.dependencies.add("testImplementation", "org.junit.jupiter:junit-jupiter:${BuildConfig.JUNIT_VERSION}") @@ -74,10 +71,10 @@ class LanguageServerPlugin : Plugin { val shadowJar = project.tasks.getByName("shadowJar") as ShadowJar shadowJar.manifest.attributes["Main-Class"] = "com.strumenta.$language.languageserver.MainKt" shadowJar.manifest.attributes["Multi-Release"] = "true" - shadowJar.manifest.attributes["Class-Path"] = - "lucene-core-${BuildConfig.LUCENE_VERSION}.jar lucene-codecs-${BuildConfig.LUCENE_VERSION}.jar" +// shadowJar.manifest.attributes["Class-Path"] = +// "lucene-core-${BuildConfig.LUCENE_VERSION}.jar lucene-codecs-${BuildConfig.LUCENE_VERSION}.jar" shadowJar.archiveFileName.set("$language.jar") - shadowJar.excludes.add("org/apache/lucene/**/*") +// shadowJar.excludes.add("org/apache/lucene/**/*") val testTask = project.tasks.getByName("test") as org.gradle.api.tasks.testing.Test testTask.useJUnitPlatform() @@ -328,18 +325,18 @@ class LanguageServerPlugin : Plugin { } else { Files.writeString(Paths.get(configuration.outputPath.toString(), "LICENSE.md"), "Copyright Strumenta SRL") } - ProcessBuilder( - "curl", - "https://repo1.maven.org/maven2/org/apache/lucene/lucene-core/9.8.0/lucene-core-9.8.0.jar", - "-o", - Paths.get(configuration.outputPath.toString(), "lucene-core-9.8.0.jar").toString() - ).start().waitFor() - ProcessBuilder( - "curl", - "https://repo1.maven.org/maven2/org/apache/lucene/lucene-codecs/9.8.0/lucene-codecs-9.8.0.jar", - "-o", - Paths.get(configuration.outputPath.toString(), "lucene-codecs-9.8.0.jar").toString() - ).start().waitFor() +// ProcessBuilder( +// "curl", +// "https://repo1.maven.org/maven2/org/apache/lucene/lucene-core/9.8.0/lucene-core-9.8.0.jar", +// "-o", +// Paths.get(configuration.outputPath.toString(), "lucene-core-9.8.0.jar").toString() +// ).start().waitFor() +// ProcessBuilder( +// "curl", +// "https://repo1.maven.org/maven2/org/apache/lucene/lucene-codecs/9.8.0/lucene-codecs-9.8.0.jar", +// "-o", +// Paths.get(configuration.outputPath.toString(), "lucene-codecs-9.8.0.jar").toString() +// ).start().waitFor() ProcessBuilder(npx, "vsce@2.15", "package").directory(configuration.outputPath.toFile()).start().waitFor() } diff --git a/settings.gradle.kts b/settings.gradle.kts index f943103..046792d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -16,9 +16,10 @@ dependencyResolutionManagement { val ktlintPluginVersion = version("ktlint", "11.6.0") val ktlintLibraryVersion = version("ktlint-library", "0.47.1") val kotlinVersion = version("kotlin", "1.8.22") - val kolasuVersion = version("kolasu", "1.5.45") + val kolasuVersion = version("kolasu", "1.5.49-lsp-experiment-35") val lsp4jVersion = version("lsp4j", "0.21.1") val luceneVersion = version("lucene", "9.8.0") + val antlr4c3Version = version("antlr4-c3", "1.1") val junit5Version = version("junit5", "5.7.1") // plugins plugin("kotlin-jvm", "org.jetbrains.kotlin.jvm").versionRef(kotlinVersion) @@ -27,15 +28,15 @@ dependencyResolutionManagement { plugin("shadow", "com.github.johnrengelman.shadow").versionRef(shadowPluginVersion) // libraries library("kotlin-reflect", "org.jetbrains.kotlin", "kotlin-reflect").versionRef(kotlinVersion) -// library("kotlin-stdlib-jdk8", "org.jetbrains.kotlin", "kotlin-stdlib-jdk8").versionRef(kotlinVersion) library("kolasu-core", "com.strumenta.kolasu", "kolasu-core").versionRef(kolasuVersion) + library("kolasu-semantics", "com.strumenta.kolasu", "kolasu-semantics").versionRef(kolasuVersion) library("lsp4j", "org.eclipse.lsp4j", "org.eclipse.lsp4j").versionRef(lsp4jVersion) library("junit5", "org.junit.jupiter", "junit-jupiter-api").versionRef(junit5Version) - library("lucene", "org.apache.lucene", "lucene-core").versionRef(luceneVersion) + library("antlr4-c3", "com.vmware.antlr4-c3", "antlr4-c3").versionRef(antlr4c3Version) library("ktlint", "com.pinterest", "ktlint").versionRef(ktlintLibraryVersion) // plugin - libraries library("shadow", "com.github.johnrengelman.shadow", "com.github.johnrengelman.shadow.gradle.plugin").versionRef(shadowPluginVersion) library("kotlin-jvm", "org.jetbrains.kotlin.jvm", "org.jetbrains.kotlin.jvm.gradle.plugin").versionRef(kotlinVersion) } } -} \ No newline at end of file +} diff --git a/testing/build.gradle.kts b/testing/build.gradle.kts index 3b721f6..39e11e8 100644 --- a/testing/build.gradle.kts +++ b/testing/build.gradle.kts @@ -7,10 +7,6 @@ plugins { signing } -repositories { - mavenCentral() -} - dependencies { implementation(project(":kolasu-languageserver-library")) implementation(libs.kolasu.core) diff --git a/testing/src/main/kotlin/testing/TestKolasuServer.kt b/testing/src/main/kotlin/testing/TestKolasuServer.kt index 01a80a7..cb52678 100644 --- a/testing/src/main/kotlin/testing/TestKolasuServer.kt +++ b/testing/src/main/kotlin/testing/TestKolasuServer.kt @@ -1,10 +1,7 @@ package testing import com.google.gson.JsonObject -import com.strumenta.kolasu.languageserver.CodeGenerator import com.strumenta.kolasu.languageserver.KolasuServer -import com.strumenta.kolasu.model.Node -import com.strumenta.kolasu.parsing.ASTParser import org.eclipse.lsp4j.DefinitionParams import org.eclipse.lsp4j.DidChangeConfigurationParams import org.eclipse.lsp4j.DidChangeTextDocumentParams @@ -30,24 +27,19 @@ import java.nio.file.Paths import kotlin.io.path.extension import kotlin.system.measureNanoTime -open class TestKolasuServer( - protected open var parser: ASTParser? = null, - protected open var enableDefinitionCapability: Boolean = false, - protected open var enableReferencesCapability: Boolean = false, - protected open var codeGenerator: CodeGenerator? = null, - protected open var language: String = "languageserver", - protected open var fileExtensions: List = listOf(), - protected open var workspacePath: Path = Paths.get("src", "test", "resources") +open class TestKolasuServer( + val serverProvider: () -> KolasuServer, + val workspacePath: Path = Paths.get("src", "test", "resources") ) { - protected open lateinit var server: KolasuServer + + protected lateinit var server: KolasuServer @BeforeEach fun beforeEach() { - initializeServer() + this.server = serverProvider() } protected open fun initializeServer() { - server = KolasuServer(parser, language, fileExtensions, enableDefinitionCapability, enableReferencesCapability, codeGenerator) expectDiagnostics(0) val workspace = workspacePath.toUri().toString() @@ -55,8 +47,8 @@ open class TestKolasuServer( server.initialized(InitializedParams()) val configuration = JsonObject() - configuration.add(language, JsonObject()) - server.didChangeConfiguration(DidChangeConfigurationParams(configuration)) + configuration.add(this.server.language, JsonObject()) + server.getWorkspaceService().didChangeConfiguration(DidChangeConfigurationParams(configuration)) } protected open fun expectDiagnostics(amount: Int) { @@ -74,7 +66,7 @@ open class TestKolasuServer( val textDocument = TextDocumentItem(uri, "", 0, text) val parameters = DidOpenTextDocumentParams(textDocument) - server.didOpen(parameters) + server.textDocumentService.didOpen(parameters) } protected open fun change( @@ -85,14 +77,14 @@ open class TestKolasuServer( val changes = listOf(TextDocumentContentChangeEvent(text)) val parameters = DidChangeTextDocumentParams(document, changes) - return server.didChange(parameters) + return server.textDocumentService.didChange(parameters) } protected open fun outline(uri: String): DocumentSymbol? { val document = TextDocumentIdentifier(uri) val parameters = DocumentSymbolParams(document) - return server.documentSymbol(parameters).get()?.first()?.right + return server.textDocumentService.documentSymbol(parameters).get()?.first()?.right } protected open fun definition( @@ -102,7 +94,7 @@ open class TestKolasuServer( val document = TextDocumentIdentifier(uri) val parameters = DefinitionParams(document, position) - return server.definition(parameters).get()?.left?.first() + return server.textDocumentService.definition(parameters).get()?.left?.first() } protected open fun references( @@ -113,7 +105,7 @@ open class TestKolasuServer( val document = TextDocumentIdentifier(uri) val parameters = ReferenceParams(document, position, ReferenceContext(includeDeclaration)) - return server.references(parameters).get() + return server.textDocumentService.references(parameters).get() } protected fun requestAtEachPositionInResourceFiles( @@ -123,7 +115,7 @@ open class TestKolasuServer( val timings = mutableListOf() for (file in Files.list(workspacePath)) { - if (fileExtensions.contains(file.extension)) { + if (this.server.fileExtensions.contains(file.extension)) { val uri = file.toUri().toString() val lines = Files.readAllLines(file)