From b8b17c2c394d24f70a8ccbbd1fe49c6e8dfaa216 Mon Sep 17 00:00:00 2001 From: Akash Yadav Date: Fri, 31 Mar 2023 22:17:37 +0530 Subject: [PATCH] feat: read snippets from JSON files stored in assets --- .../lsp/snippets/DefaultSnippet.kt} | 18 +-- .../androidide/lsp/snippets/ISnippet.kt | 38 ++++++ .../androidide/lsp/snippets/ISnippetScope.kt | 32 +++++ .../androidide/lsp/snippets/SnippetParser.kt | 123 ++++++++++++++++++ .../data/editor/java/snippets.local.json | 76 +++++++++++ .../lsp/java/JavaLanguageServer.java | 3 + .../completion/IJavaCompletionProvider.kt | 10 +- .../completion/SnippetCompletionProvider.kt | 5 +- .../snippet/JavaSnippetRepository.kt | 38 +----- .../providers/snippet/JavaSnippetScope.kt | 12 +- 10 files changed, 303 insertions(+), 52 deletions(-) rename lsp/{java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippet.kt => api/src/main/java/com/itsaky/androidide/lsp/snippets/DefaultSnippet.kt} (78%) create mode 100644 lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/ISnippet.kt create mode 100644 lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/ISnippetScope.kt create mode 100644 lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetParser.kt create mode 100644 lsp/java/src/main/assets/data/editor/java/snippets.local.json diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippet.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/DefaultSnippet.kt similarity index 78% rename from lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippet.kt rename to lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/DefaultSnippet.kt index d2f62e90c..46cdb0cc9 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippet.kt +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/DefaultSnippet.kt @@ -15,18 +15,18 @@ * along with AndroidIDE. If not, see . */ -package com.itsaky.androidide.lsp.java.providers.snippet +package com.itsaky.androidide.lsp.snippets /** * Java snippet. * * @author Akash Yadav */ -data class JavaSnippet( - val prefix: String, - val description: String, - val content: Array -) { +data class DefaultSnippet( + override val prefix: String, + override val description: String, + override val body: Array +) : ISnippet { constructor( prefix: String, @@ -36,11 +36,11 @@ data class JavaSnippet( override fun equals(other: Any?): Boolean { if (this === other) return true - if (other !is JavaSnippet) return false + if (other !is DefaultSnippet) return false if (description != other.description) return false if (prefix != other.prefix) return false - if (!content.contentEquals(other.content)) return false + if (!body.contentEquals(other.body)) return false return true } @@ -48,7 +48,7 @@ data class JavaSnippet( override fun hashCode(): Int { var result = prefix.hashCode() result = 31 * result + description.hashCode() - result = 31 * result + content.contentHashCode() + result = 31 * result + body.contentHashCode() return result } } diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/ISnippet.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/ISnippet.kt new file mode 100644 index 000000000..2d9cfb208 --- /dev/null +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/ISnippet.kt @@ -0,0 +1,38 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.snippets + +/** + * A snippet item. + * + * @author Akash Yadav + */ +interface ISnippet { + + /** The prefix for the snippet. */ + val prefix: String + + /** A short description about the snippet. */ + val description: String + + /** + * The snippet body. Each element in this array represents a line of code. The lines are joined + * and indented before inserting the text. + */ + val body: Array +} diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/ISnippetScope.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/ISnippetScope.kt new file mode 100644 index 000000000..5bd00e67a --- /dev/null +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/ISnippetScope.kt @@ -0,0 +1,32 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.snippets + +/** + * Marker interface for snippet scopes. + * + * @author Akash Yadav + */ +interface ISnippetScope { + + /** + * The filename for the snippet. This is used to resolve the snippet file stored at location + * `'data/editor//snippets..json'` + */ + val filename: String +} diff --git a/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetParser.kt b/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetParser.kt new file mode 100644 index 000000000..ce69c3493 --- /dev/null +++ b/lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetParser.kt @@ -0,0 +1,123 @@ +/* + * This file is part of AndroidIDE. + * + * AndroidIDE is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * AndroidIDE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with AndroidIDE. If not, see . + */ + +package com.itsaky.androidide.lsp.snippets + +import com.google.gson.JsonParseException +import com.google.gson.stream.JsonReader +import com.itsaky.androidide.app.BaseApplication +import com.itsaky.androidide.tasks.executeAsyncProvideError +import com.itsaky.androidide.utils.ILogger +import java.io.IOException +import java.util.concurrent.ConcurrentHashMap + +/** + * Parser for parsing snippets from assets. + * + * @author Akash Yadav + */ +object SnippetParser { + + private val log = ILogger.newInstance("SnippetParser") + + fun parse( + lang: String, + scopes: Array, + snippetFactory: (String, String, List) -> ISnippet = { prefix, desc, body -> + DefaultSnippet(prefix, desc, body.toTypedArray()) + } + ): Map> { + return ConcurrentHashMap>().apply { + for (scope in scopes) { + this[scope] = + mutableListOf().apply { + readSnippets(lang, scope.filename, snippetFactory, this) + } + } + } + } + + private fun readSnippets( + lang: String, + type: String, + snippetFactory: (String, String, List) -> ISnippet, + snippets: MutableList + ) { + executeAsyncProvideError({ + val content = + try { + BaseApplication.getBaseInstance() + .assets + .open("data/editor/${lang}/snippets.${type}.json") + .reader() + } catch (e: IOException) { + // snippet file probably does not exist + return@executeAsyncProvideError + } + + JsonReader(content).use { + it.beginObject() + while (it.hasNext()) { + val prefix = it.nextName() + readSnippet(prefix, it, snippetFactory, snippets) + } + it.endObject() + } + }) { result, err -> + if (result == null || err != null) { + log.error("Failed to load '${type}' snippets", err) + } + } + } + + private fun readSnippet( + prefix: String, + reader: JsonReader, + snippetFactory: (String, String, List) -> ISnippet, + snippets: MutableList + ) { + reader.beginObject() + var desc: String? = null + val body = mutableListOf() + while (reader.hasNext()) { + val n = reader.nextName() + if (n != "desc" && n != "body") { + throw JsonParseException("'desc' or 'body' was expected, but found '${n}'") + } + + if (n == "desc") { + desc = reader.nextString() + continue + } + + if (n == "body") { + reader.beginArray() + while (reader.hasNext()) { + body.add(reader.nextString()) + } + reader.endArray() + } + } + + checkNotNull(desc) { "DefaultSnippet description not defined for '${prefix}'" } + check(body.isNotEmpty()) { "DefaultSnippet body not defined for '${prefix}'" } + + snippets.add(snippetFactory(prefix, desc, body)) + + reader.endObject() + } +} diff --git a/lsp/java/src/main/assets/data/editor/java/snippets.local.json b/lsp/java/src/main/assets/data/editor/java/snippets.local.json new file mode 100644 index 000000000..c1506bb06 --- /dev/null +++ b/lsp/java/src/main/assets/data/editor/java/snippets.local.json @@ -0,0 +1,76 @@ +{ + "for": { + "desc": "Indexed for loop", + "body": [ + "for(int ${1:i} = 0; $1 < ${2:count}; ++$1) {", + "\t$0", + "}" + ] + }, + "foreach": { + "desc": "For-each loop", + "body": [ + "for(${1:element} : ${2:iterable}) {", + "\t$0", + "}" + ] + }, + "forr": { + "desc": "Reverse-indexed for loop", + "body": [ + "for(int ${1:i} = ${2:count} - 1; $1 >= 0; --$1) {", + "\t$0", + "}" + ] + }, + "while": { + "desc": "While loop", + "body": [ + "while(${1:condition}) {", + "\t$0", + "}" + ] + }, + "dowhile": { + "desc": "Do-While loop", + "body": [ + "do {", + "\t$0", + "} while(${1:condition});" + ] + }, + "if": { + "desc": "If statement", + "body": [ + "if(${1:condition}) {", + "\t$0", + "}" + ] + }, + "ifelse": { + "desc": "If-else statement", + "body": [ + "if(${1:condition}) {", + "\t$0", + "} else {", + "\t", + "}" + ] + }, + "ifnull": { + "desc": "If-null", + "body": [ + "if(${1:object} == null) {", + "\t$0", + "}" + ] + }, + "ifnotnull": { + "desc": "If-not-null", + "body": [ + "if(${1:object} != null) {", + "\t$0", + "}" + ] + } +} \ No newline at end of file diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.java b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.java index 022a39708..2b1af8cf0 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.java +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/JavaLanguageServer.java @@ -42,6 +42,7 @@ import com.itsaky.androidide.lsp.java.providers.JavaDiagnosticProvider; import com.itsaky.androidide.lsp.java.providers.JavaSelectionProvider; import com.itsaky.androidide.lsp.java.providers.ReferenceProvider; import com.itsaky.androidide.lsp.java.providers.SignatureProvider; +import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetRepository; import com.itsaky.androidide.lsp.java.utils.AnalyzeTimer; import com.itsaky.androidide.lsp.java.utils.CancelChecker; import com.itsaky.androidide.lsp.models.CodeFormatResult; @@ -99,6 +100,8 @@ public class JavaLanguageServer implements ILanguageServer { if (!EventBus.getDefault().isRegistered(this)) { EventBus.getDefault().register(this); } + + JavaSnippetRepository.INSTANCE.init(); } public IServerSettings getSettings() { diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt index 312b6a772..26d20f8bb 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/IJavaCompletionProvider.kt @@ -24,7 +24,6 @@ import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService import com.itsaky.androidide.lsp.java.edits.ClassImportEditHandler import com.itsaky.androidide.lsp.java.models.JavaCompletionItem import com.itsaky.androidide.lsp.java.providers.BaseJavaServiceProvider -import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippet import com.itsaky.androidide.lsp.java.utils.EditHelper import com.itsaky.androidide.lsp.models.ClassCompletionData import com.itsaky.androidide.lsp.models.Command @@ -43,9 +42,11 @@ import com.itsaky.androidide.lsp.models.ICompletionData import com.itsaky.androidide.lsp.models.InsertTextFormat.SNIPPET import com.itsaky.androidide.lsp.models.MatchLevel import com.itsaky.androidide.lsp.models.MethodCompletionData +import com.itsaky.androidide.lsp.snippets.ISnippet import com.itsaky.androidide.preferences.utils.indentationString import com.itsaky.androidide.progress.ProgressManager.Companion.abortIfCancelled import com.itsaky.androidide.utils.ILogger +import java.nio.file.Path import jdkx.lang.model.element.Element import jdkx.lang.model.element.ElementKind.ANNOTATION_TYPE import jdkx.lang.model.element.ElementKind.CLASS @@ -69,7 +70,6 @@ import jdkx.lang.model.element.TypeElement import jdkx.lang.model.element.VariableElement import openjdk.source.tree.Tree import openjdk.source.util.TreePath -import java.nio.file.Path /** * Completion provider for Java source code. @@ -188,7 +188,7 @@ abstract class IJavaCompletionProvider( item.insertText = first.simpleName.toString() + "($0)" item.command = Command("Trigger Parameter Hints", Command.TRIGGER_PARAMETER_HINTS) } - item.insertTextFormat = SNIPPET // Snippet + item.insertTextFormat = SNIPPET // DefaultSnippet item.snippetDescription = describeSnippet(prefix = partial, allowCommandExecution = true) } return item @@ -294,7 +294,7 @@ abstract class IJavaCompletionProvider( } protected open fun snippetItem( - snippet: JavaSnippet, + snippet: ISnippet, matchLevel: MatchLevel, partial: String, indent: Int @@ -310,7 +310,7 @@ abstract class IJavaCompletionProvider( val indentation = indentationString(indent) this.insertTextFormat = SNIPPET this.insertText = - snippet.content.joinToString(separator = "\n").also { + snippet.body.joinToString(separator = "\n").also { it.replace("\t", indentationString).replace("\n", "\n${indentation}") } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt index 83e1a285c..bff92ebc0 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/completion/SnippetCompletionProvider.kt @@ -20,12 +20,13 @@ package com.itsaky.androidide.lsp.java.providers.completion import com.itsaky.androidide.lsp.api.IServerSettings import com.itsaky.androidide.lsp.java.compiler.CompileTask import com.itsaky.androidide.lsp.java.compiler.JavaCompilerService -import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippet +import com.itsaky.androidide.lsp.snippets.DefaultSnippet import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetRepository import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetScope import com.itsaky.androidide.lsp.models.CompletionItem import com.itsaky.androidide.lsp.models.CompletionResult import com.itsaky.androidide.lsp.models.MatchLevel +import com.itsaky.androidide.lsp.snippets.ISnippet import com.itsaky.androidide.preferences.internal.tabSize import io.github.rosemoe.sora.text.TextUtils import java.nio.file.Path @@ -99,7 +100,7 @@ class SnippetCompletionProvider( ): CompletionResult { val items = mutableListOf() val snippets = - mutableListOf().apply { + mutableListOf().apply { JavaSnippetRepository.snippets[JavaSnippetScope.LOCAL]?.let { addAll(it) } JavaSnippetRepository.snippets[JavaSnippetScope.GLOBAL]?.let { addAll(it) } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt index 022a51685..0562b72a8 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt @@ -17,6 +17,9 @@ package com.itsaky.androidide.lsp.java.providers.snippet +import com.itsaky.androidide.lsp.snippets.ISnippet +import com.itsaky.androidide.lsp.snippets.SnippetParser + /** * Repository to store various snippets for Java. * @@ -24,37 +27,10 @@ package com.itsaky.androidide.lsp.java.providers.snippet */ object JavaSnippetRepository { - val snippets: Map> by lazy { createSnippets() } + lateinit var snippets: Map> + private set - private fun createSnippets(): Map> { - return mutableMapOf>().apply { - this[JavaSnippetScope.GLOBAL] = createGlobalSnippets() - this[JavaSnippetScope.TOP_LEVEL] = createTopLevelSnippets() - this[JavaSnippetScope.MEMBER] = createMemberSnippets() - this[JavaSnippetScope.LOCAL] = createLocalSnippets() - } - } - - private fun createGlobalSnippets(): List { - return listOf() - } - - private fun createTopLevelSnippets(): List { - return listOf() - } - - private fun createMemberSnippets(): List { - return listOf() - } - - private fun createLocalSnippets(): List { - return listOf( - JavaSnippet("for", "Indexed for loop") { - arrayOf("for (int \${1:i} = 0; \$1 < \${2:count}; ++\$1) {", "\t\$0", "}") - }, - JavaSnippet("forr", "Reverse-indexed for loop") { - arrayOf("for (int \${1:i} = \${2:count} - 1; \$1 >= 0; --\$1) {", "\t\$0", "}") - } - ) + fun init() { + this.snippets = SnippetParser.parse("java", JavaSnippetScope.values()) } } diff --git a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetScope.kt b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetScope.kt index e9bf1850c..0ea574512 100644 --- a/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetScope.kt +++ b/lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetScope.kt @@ -17,25 +17,27 @@ package com.itsaky.androidide.lsp.java.providers.snippet +import com.itsaky.androidide.lsp.snippets.ISnippetScope + /** * Scope for [JavaSnippet]. * * @author Akash Yadav */ -enum class JavaSnippetScope { +enum class JavaSnippetScope(override val filename: String) : ISnippetScope { /** * Snippets that can be used at the top level. This includes snippes such as class, interface, * enum templates. */ - TOP_LEVEL, + TOP_LEVEL("top-level"), /** Snippets that can be used at the member level i.e. inside a class tree. */ - MEMBER, + MEMBER("member"), /** Snippets that can be used at a local level. E.g. inside a method or constructor. */ - LOCAL, + LOCAL("local"), /** Snippets that can be used anywhere in the code, irrespective of the current scope. */ - GLOBAL + GLOBAL("global") }