feat: read snippets from JSON files stored in assets

This commit is contained in:
Akash Yadav
2023-03-31 22:17:37 +05:30
parent 8f0888799a
commit b8b17c2c39
10 changed files with 303 additions and 52 deletions
@@ -15,18 +15,18 @@
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
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<String>
) {
data class DefaultSnippet(
override val prefix: String,
override val description: String,
override val body: Array<String>
) : 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
}
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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<String>
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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/<lang>/snippets.<filename>.json'`
*/
val filename: String
}
@@ -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 <https://www.gnu.org/licenses/>.
*/
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 <S : ISnippetScope> parse(
lang: String,
scopes: Array<S>,
snippetFactory: (String, String, List<String>) -> ISnippet = { prefix, desc, body ->
DefaultSnippet(prefix, desc, body.toTypedArray())
}
): Map<S, List<ISnippet>> {
return ConcurrentHashMap<S, List<ISnippet>>().apply {
for (scope in scopes) {
this[scope] =
mutableListOf<ISnippet>().apply {
readSnippets(lang, scope.filename, snippetFactory, this)
}
}
}
}
private fun readSnippets(
lang: String,
type: String,
snippetFactory: (String, String, List<String>) -> ISnippet,
snippets: MutableList<ISnippet>
) {
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<String>) -> ISnippet,
snippets: MutableList<ISnippet>
) {
reader.beginObject()
var desc: String? = null
val body = mutableListOf<String>()
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()
}
}
@@ -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",
"}"
]
}
}
@@ -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() {
@@ -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}")
}
}
@@ -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<CompletionItem>()
val snippets =
mutableListOf<JavaSnippet>().apply {
mutableListOf<ISnippet>().apply {
JavaSnippetRepository.snippets[JavaSnippetScope.LOCAL]?.let { addAll(it) }
JavaSnippetRepository.snippets[JavaSnippetScope.GLOBAL]?.let { addAll(it) }
}
@@ -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<JavaSnippetScope, List<JavaSnippet>> by lazy { createSnippets() }
lateinit var snippets: Map<JavaSnippetScope, List<ISnippet>>
private set
private fun createSnippets(): Map<JavaSnippetScope, List<JavaSnippet>> {
return mutableMapOf<JavaSnippetScope, List<JavaSnippet>>().apply {
this[JavaSnippetScope.GLOBAL] = createGlobalSnippets()
this[JavaSnippetScope.TOP_LEVEL] = createTopLevelSnippets()
this[JavaSnippetScope.MEMBER] = createMemberSnippets()
this[JavaSnippetScope.LOCAL] = createLocalSnippets()
}
}
private fun createGlobalSnippets(): List<JavaSnippet> {
return listOf()
}
private fun createTopLevelSnippets(): List<JavaSnippet> {
return listOf()
}
private fun createMemberSnippets(): List<JavaSnippet> {
return listOf()
}
private fun createLocalSnippets(): List<JavaSnippet> {
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())
}
}
@@ -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")
}