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")
}