Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ import com.itsaky.androidide.fragments.output.ShareableOutputFragment
import com.itsaky.androidide.fragments.sidebar.FileTreeFragment
import com.itsaky.androidide.handlers.EditorActivityLifecyclerObserver
import com.itsaky.androidide.handlers.LspHandler.registerLanguageServers
import com.itsaky.androidide.handlers.SnippetHandler.loadPluginSnippets
import com.itsaky.androidide.handlers.SnippetHandler.loadUserSnippets
import com.itsaky.androidide.handlers.SnippetHandler.refreshPluginSnippets
import com.itsaky.androidide.idetooltips.TooltipManager
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.interfaces.DiagnosticClickListener
Expand Down Expand Up @@ -426,6 +429,8 @@ abstract class BaseEditorActivity :
protected open fun preDestroy() {
BuildOutputProvider.clearBottomSheet()

IDEApplication.getPluginManager()?.setSnippetRefreshListener(null)

Shizuku.removeBinderReceivedListener(shizukuBinderReceivedListener)
if (isAtLeastR()) wadbConnectionViewModel.stop(this)

Expand Down Expand Up @@ -614,6 +619,11 @@ abstract class BaseEditorActivity :

this.optionsMenuInvalidator = Runnable { super.invalidateOptionsMenu() }

loadUserSnippets()
loadPluginSnippets()
IDEApplication.getPluginManager()?.setSnippetRefreshListener { pluginId ->
refreshPluginSnippets(pluginId)
}
registerLanguageServers()

onBackPressedDispatcher.addCallback(this, onBackPressedCallback)
Expand Down
67 changes: 67 additions & 0 deletions app/src/main/java/com/itsaky/androidide/handlers/SnippetHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.itsaky.androidide.handlers

import com.itsaky.androidide.lsp.java.providers.snippet.JavaSnippetScope
import com.itsaky.androidide.lsp.snippets.DefaultSnippet
import com.itsaky.androidide.lsp.snippets.ISnippetScope
import com.itsaky.androidide.lsp.snippets.SnippetRegistry
import com.itsaky.androidide.plugins.extensions.SnippetContribution
import com.itsaky.androidide.lsp.snippets.UserSnippetLoader
import com.itsaky.androidide.lsp.xml.providers.snippet.XML_SNIPPET_SCOPES
import com.itsaky.androidide.plugins.manager.snippets.PluginSnippetManager
import org.slf4j.LoggerFactory

object SnippetHandler {

private val log = LoggerFactory.getLogger(SnippetHandler::class.java)

fun loadUserSnippets() {
loadUserSnippetsForLanguage("java", JavaSnippetScope.entries.toTypedArray())
loadUserSnippetsForLanguage("xml", XML_SNIPPET_SCOPES)
}


fun loadPluginSnippets() {
val allSnippets = PluginSnippetManager.getInstance().getAllSnippets()
allSnippets.forEach { (pluginId, contributions) ->
registerContributions(pluginId, contributions)
}
if (allSnippets.isNotEmpty()) {
log.info("Loaded plugin snippets from {} plugins", allSnippets.size)
}
}

fun refreshPluginSnippets(pluginId: String) {
SnippetRegistry.unregisterPluginSnippets(pluginId)
val contributions = PluginSnippetManager.getInstance().refreshPlugin(pluginId)
registerContributions(pluginId, contributions)
}

private fun registerContributions(pluginId: String, contributions: List<SnippetContribution>) {
contributions.groupBy { it.language to it.scope }.forEach { (key, group) ->
val (language, scope) = key
val snippets = group.map { DefaultSnippet(it.prefix, it.description, it.body.toTypedArray()) }
SnippetRegistry.registerPluginSnippets(pluginId, language, scope, snippets)
}
}

fun removePluginSnippets(pluginId: String) {
SnippetRegistry.unregisterPluginSnippets(pluginId)
}

private fun <S : ISnippetScope> loadUserSnippetsForLanguage(
language: String,
scopes: Array<S>,
) {
SnippetRegistry.clearUserSnippets(language)
val userSnippets = UserSnippetLoader.loadUserSnippets(language, scopes)
userSnippets.forEach { (scope, snippets) ->
if (snippets.isNotEmpty()) {
SnippetRegistry.registerUserSnippets(language, scope, snippets)
}
}
val total = userSnippets.values.sumOf { it.size }
if (total > 0) {
log.info("Loaded {} user snippets for {}", total, language)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ public final class Environment {
public static File NDK_DIR;

public static File TEMPLATES_DIR;
public static File SNIPPETS_DIR;

public static String getArchitecture() {
return IDEBuildConfigProvider.getInstance().getCpuAbiName();
Expand Down Expand Up @@ -191,6 +192,7 @@ public static void init(Context context) {
NDK_DIR = new File(ANDROID_HOME, "ndk");

TEMPLATES_DIR = mkdirIfNotExists(new File(ANDROIDIDE_HOME, "templates"));
SNIPPETS_DIR = mkdirIfNotExists(new File(ANDROIDIDE_HOME, "snippets"));

isInitialized.set(true);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import com.itsaky.androidide.tasks.executeAsyncProvideError
import com.itsaky.androidide.utils.VMUtils
import org.slf4j.LoggerFactory
import java.io.IOException
import java.io.Reader
import java.util.concurrent.ConcurrentHashMap

/**
Expand Down Expand Up @@ -56,6 +57,24 @@ object SnippetParser {
}
}

fun parseFromReader(
reader: Reader,
snippetFactory: (String, String, List<String>) -> ISnippet = { prefix, desc, body ->
DefaultSnippet(prefix, desc, body.toTypedArray())
},
): List<ISnippet> {
val snippets = mutableListOf<ISnippet>()
JsonReader(reader).use {
it.beginObject()
while (it.hasNext()) {
val prefix = it.nextName()
readSnippet(prefix, it, snippetFactory, snippets)
}
it.endObject()
}
return snippets
}

private fun readSnippets(
lang: String,
type: String,
Expand All @@ -70,18 +89,10 @@ object SnippetParser {
.open(assetsPath(lang, type))
.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()
}
snippets.addAll(parseFromReader(content, snippetFactory))
}) { result, err ->
if (result == null || err != null) {
log.error("Failed to load '{}' snippets", type, err)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.itsaky.androidide.lsp.snippets

import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.read
import kotlin.concurrent.write

object SnippetRegistry {

private val lock = ReentrantReadWriteLock()

private val builtIn = mutableMapOf<String, MutableMap<String, MutableList<ISnippet>>>()
private val user = mutableMapOf<String, MutableMap<String, MutableList<ISnippet>>>()
private val plugin = mutableMapOf<String, MutableMap<String, MutableMap<String, MutableList<ISnippet>>>>()

fun registerBuiltIn(language: String, scope: String, snippets: List<ISnippet>) {
lock.write {
builtIn.getOrPut(language) { mutableMapOf() }[scope] = snippets.toMutableList()
}
}

fun registerUserSnippets(language: String, scope: String, snippets: List<ISnippet>) {
lock.write {
user.getOrPut(language) { mutableMapOf() }[scope] = snippets.toMutableList()
}
}

fun registerPluginSnippets(
pluginId: String,
language: String,
scope: String,
snippets: List<ISnippet>,
) {
lock.write {
plugin.getOrPut(pluginId) { mutableMapOf() }
.getOrPut(language) { mutableMapOf() }[scope] = snippets.toMutableList()
}
}

fun unregisterPluginSnippets(pluginId: String) {
lock.write {
plugin.remove(pluginId)
}
}

fun clearUserSnippets(language: String) {
lock.write {
user.remove(language)
}
}

fun getSnippets(language: String, scope: String): List<ISnippet> = lock.read {
val builtInSnippets = builtIn[language]?.get(scope).orEmpty()
val userSnippets = user[language]?.get(scope).orEmpty()

val userPrefixes = userSnippets.map { it.prefix }.toSet()
val merged = builtInSnippets.filter { it.prefix !in userPrefixes }.toMutableList()
merged.addAll(userSnippets)

plugin.values.forEach { langMap ->
langMap[language]?.get(scope)?.let { merged.addAll(it) }
}

merged
}

fun getSnippets(language: String, scopes: List<String>): List<ISnippet> {
return scopes.flatMap { getSnippets(language, it) }
}

fun <S : ISnippetScope> initBuiltIn(language: String, scopes: Array<S>) {
val parsed = SnippetParser.parse(language, scopes)
lock.write {
parsed.forEach { (scope, snippets) ->
builtIn.getOrPut(language) { mutableMapOf() }[scope.filename] = snippets as MutableList<ISnippet>
}
}
}

fun clear() {
lock.write {
builtIn.clear()
user.clear()
plugin.clear()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.itsaky.androidide.lsp.snippets

import com.google.gson.JsonParseException
import com.itsaky.androidide.utils.Environment
import org.slf4j.LoggerFactory
import java.io.File
import java.io.IOException

object UserSnippetLoader {

private val log = LoggerFactory.getLogger(UserSnippetLoader::class.java)

fun <S : ISnippetScope> loadUserSnippets(
language: String,
scopes: Array<S>,
): Map<String, List<ISnippet>> {
val langDir = getUserSnippetsDir(language)
if (!langDir.isDirectory) return emptyMap()

return scopes.associate { scope ->
val file = File(langDir, "snippets.${scope.filename}.json")
val snippets = if (file.isFile) {
try {
SnippetParser.parseFromReader(file.reader())
} catch (e: IOException) {
log.error("Failed to read user snippets from {}", file.absolutePath, e)
emptyList()
} catch (e: JsonParseException) {
log.error("Failed to parse user snippets from {}", file.absolutePath, e)
emptyList()
}
} else {
emptyList()
}
scope.filename to snippets
}
}

fun getUserSnippetsDir(language: String): File {
return File(Environment.SNIPPETS_DIR, language)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,16 @@
package com.itsaky.androidide.lsp.java.providers.snippet

import com.itsaky.androidide.lsp.snippets.ISnippet
import com.itsaky.androidide.lsp.snippets.SnippetParser
import com.itsaky.androidide.lsp.snippets.SnippetRegistry

/**
* Repository to store various snippets for Java.
*
* @author Akash Yadav
*/
object JavaSnippetRepository {

lateinit var snippets: Map<JavaSnippetScope, List<ISnippet>>
private set
val snippets: Map<JavaSnippetScope, List<ISnippet>>
get() = JavaSnippetScope.entries.associateWith { scope ->
SnippetRegistry.getSnippets("java", scope.filename)
}

fun init() {
this.snippets = SnippetParser.parse("java", JavaSnippetScope.values())
SnippetRegistry.initBuiltIn("java", JavaSnippetScope.entries.toTypedArray())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,19 +18,16 @@
package com.itsaky.androidide.lsp.xml.providers.snippet

import com.itsaky.androidide.lsp.snippets.ISnippet
import com.itsaky.androidide.lsp.snippets.SnippetParser
import com.itsaky.androidide.lsp.snippets.SnippetRegistry

/**
* Repository for XML snippets.
*
* @author Akash Yadav
*/
object XmlSnippetRepository {

lateinit var snippets: Map<IXmlSnippetScope, List<ISnippet>>
private set
val snippets: Map<IXmlSnippetScope, List<ISnippet>>
get() = XML_SNIPPET_SCOPES.associateWith { scope ->
SnippetRegistry.getSnippets("xml", scope.filename)
}

fun init() {
this.snippets = SnippetParser.parse("xml", XML_SNIPPET_SCOPES)
SnippetRegistry.initBuiltIn("xml", XML_SNIPPET_SCOPES)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.itsaky.androidide.plugins.extensions

import com.itsaky.androidide.plugins.IPlugin

interface SnippetExtension : IPlugin {
fun getSnippetContributions(): List<SnippetContribution>
}

data class SnippetContribution(
val language: String,
val scope: String,
val prefix: String,
val description: String,
val body: List<String>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.itsaky.androidide.plugins.services

interface IdeSnippetService {
fun refreshSnippets(pluginId: String)
}
Loading
Loading