编辑器插件

开发编辑器插件的步骤如下:
在
build.gradle.kts
文件中添加m8test sdk
依赖。为减小插件 APK 的体积,如果依赖项已包含在 M8Test Version Catalog 中,建议使用compileOnly
方式引入依赖。
import com.m8test.util.VersionUtils
plugins {
alias(m8test.plugins.android.application)
alias(m8test.plugins.kotlin.android)
alias(m8test.plugins.kotlin.compose)
}
android {
namespace = "com.m8test.editor.language"
compileSdk = m8test.versions.compileSdk.get().toInt()
defaultConfig {
minSdk = m8test.versions.minSdk.get().toInt()
targetSdk = m8test.versions.targetSdk.get().toInt()
versionName = libs.versions.versionName.get()
versionCode = VersionUtils.getCode(versionName!!)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.toVersion(m8test.versions.sourceCompatibility.get())
targetCompatibility = JavaVersion.toVersion(m8test.versions.targetCompatibility.get())
}
kotlinOptions {
jvmTarget = m8test.versions.jvmTarget.get()
}
buildFeatures {
compose = true
}
}
dependencies {
compileOnly(platform(m8test.androidx.compose.bom))
compileOnly(m8test.bundles.compose)
compileOnly(m8test.m8test.sdk)
compileOnly(m8test.gson)
implementation(libs.m8test.compose.widget)
implementation(libs.bundles.editor.kit)
}
编写插件类,继承 AbstractComposableEditorPlugin 并重写
Content
方法
package com.m8test.editor.language
import android.content.res.Resources
import androidx.activity.compose.LocalOnBackPressedDispatcherOwner
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.unit.dp
import com.blacksquircle.ui.language.css.CssLanguage
import com.blacksquircle.ui.language.groovy.GroovyLanguage
import com.blacksquircle.ui.language.html.HtmlLanguage
import com.blacksquircle.ui.language.ini.IniLanguage
import com.blacksquircle.ui.language.java.JavaLanguage
import com.blacksquircle.ui.language.javascript.JavaScriptLanguage
import com.blacksquircle.ui.language.json.JsonLanguage
import com.blacksquircle.ui.language.kotlin.KotlinLanguage
import com.blacksquircle.ui.language.lua.LuaLanguage
import com.blacksquircle.ui.language.markdown.MarkdownLanguage
import com.blacksquircle.ui.language.php.PhpLanguage
import com.blacksquircle.ui.language.python.PythonLanguage
import com.blacksquircle.ui.language.ruby.RubyLanguage
import com.blacksquircle.ui.language.toml.TomlLanguage
import com.blacksquircle.ui.language.typescript.TypeScriptLanguage
import com.blacksquircle.ui.language.xml.XmlLanguage
import com.blacksquircle.ui.language.yaml.YamlLanguage
import com.google.accompanist.imageloading.rememberDrawablePainter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.m8test.compose.widget.AutoResizeSurfaceDialog
import com.m8test.compose.widget.BackTopBarScaffold
import com.m8test.editor.plugin.AbstractComposableEditorPlugin
import com.m8test.editor.ui.Editor
import com.m8test.plugin.api.ApkPluginProvider
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
/**
* Description TODO
*
* @date 2024/12/18 15:43:44
* @author M8Test, [email protected], https://m8test.com
*/
class LanguageEditorPlugin(apkPluginProvider: ApkPluginProvider) :
AbstractComposableEditorPlugin(apkPluginProvider = apkPluginProvider) {
private val gson = Gson()
private val defaultLanguages = listOf(
LanguageWrapper(language = "groovy", extensions = mutableSetOf("groovy")),
LanguageWrapper(language = "java", extensions = mutableSetOf("java", "javas")),
LanguageWrapper(language = "javascript", extensions = mutableSetOf("js")),
LanguageWrapper(language = "kotlin", extensions = mutableSetOf("kt", "kts")),
LanguageWrapper(language = "lua", extensions = mutableSetOf("lua")),
LanguageWrapper(language = "php", extensions = mutableSetOf("php")),
LanguageWrapper(language = "python", extensions = mutableSetOf("py")),
LanguageWrapper(language = "ruby", extensions = mutableSetOf("rb")),
LanguageWrapper(language = "yaml", extensions = mutableSetOf("yaml", "yml")),
LanguageWrapper(language = "json", extensions = mutableSetOf("json")),
LanguageWrapper(language = "xml", extensions = mutableSetOf("xml")),
LanguageWrapper(language = "typescript", extensions = mutableSetOf("ts")),
LanguageWrapper(language = "markdown", extensions = mutableSetOf("md")),
LanguageWrapper(language = "css", extensions = mutableSetOf("css")),
LanguageWrapper(language = "html", extensions = mutableSetOf("html")),
LanguageWrapper(language = "ini", extensions = mutableSetOf("ini")),
LanguageWrapper(language = "toml", extensions = mutableSetOf("toml"))
)
private val languages = listOf(
GroovyLanguage(),
JavaLanguage(),
JavaScriptLanguage(),
KotlinLanguage(),
LuaLanguage(),
PhpLanguage(),
PythonLanguage(),
RubyLanguage(),
YamlLanguage(),
JsonLanguage(),
XmlLanguage(),
TypeScriptLanguage(),
MarkdownLanguage(),
CssLanguage(),
HtmlLanguage(),
IniLanguage(),
TomlLanguage(),
)
@Composable
private fun RestoreDialog(onConfirm: () -> Unit, onDismiss: () -> Unit) {
AutoResizeSurfaceDialog(
onDismissRequest = onDismiss,
shape = RoundedCornerShape(20.dp),
content = {
Column(
modifier = Modifier.padding(20.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
val r = [email protected]()
Text(
text = r.getString(R.string.text_warn),
style = MaterialTheme.typography.titleLarge
)
Text(text = r.getString(R.string.text_restore_language_warning))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
TextButton(onClick = onDismiss) {
Text(text = r.getString(R.string.text_cancel))
}
TextButton(onClick = {
onDismiss()
onConfirm()
}) {
Text(text = r.getString(R.string.text_confirm))
}
}
}
})
}
@Composable
override fun Content() {
val languages = remember {
mutableStateListOf<LanguageWrapper>().apply {
addAll(getLanguagesFromSetting())
}
}
val onBackPressedDispatcher =
LocalOnBackPressedDispatcherOwner.current?.onBackPressedDispatcher
var canSave by remember { mutableStateOf(false) }
var showRestoreDialog by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
if (showRestoreDialog) {
RestoreDialog(onConfirm = {
getSettings().remove("languages")
scope.launch(Dispatchers.IO) {
languages.clear()
delay(100)
languages.addAll(getLanguagesFromSetting())
}
}) { showRestoreDialog = false }
}
BackTopBarScaffold(
onBackPress = { onBackPressedDispatcher?.onBackPressed() },
titleText = [email protected]()
.getString(R.string.text_editor_language_setting),
actions = {
IconButton(enabled = canSave, onClick = {
getSettings().set("languages", gson.toJson(languages))
canSave = false
}) {
Icon(
painter = painterResource(
[email protected](),
R.drawable.ic_baseline_save_24
),
contentDescription = null
)
}
IconButton(onClick = { showRestoreDialog = true }) {
Icon(
painter = painterResource(
resources = [email protected](),
R.drawable.ic_baseline_restore_24
),
contentDescription = null
)
}
}) { paddingValues ->
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
) {
items(languages) { language ->
LanguageItem(
language = language,
onValueChange = { canSave = true },
modifier = Modifier.fillMaxWidth()
)
}
}
}
}
private fun getLanguagesFromSetting(): List<LanguageWrapper> {
return getSettings().get("languages")
?.let { gson.fromJson(it, object : TypeToken<List<LanguageWrapper>>() {}.type) }
?: defaultLanguages.map {
LanguageWrapper(
language = it.language,
extensions = mutableSetOf<String>().apply { addAll(it.extensions) }
)
}
}
@Composable
private fun LanguageItem(
language: LanguageWrapper,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
Row(modifier = modifier) {
var value by remember { mutableStateOf(language.extensions.joinToString()) }
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
label = { Text(text = language.language) },
placeholder = {
val r = [email protected]()
Text(text = r.getString(R.string.text_extension_placeholder))
},
value = value,
onValueChange = {
value = it
language.extensions.clear()
language.extensions.addAll(value.split(",").map { it.trim() }
.filter { it.isNotBlank() })
onValueChange(it)
})
}
}
override fun onAttached(editor: Editor) {
super.onAttached(editor)
val settingLanguages = getLanguagesFromSetting()
editor.language = editor.file.extension.let { extension ->
languages.firstOrNull { l ->
settingLanguages.firstOrNull { it.language == l.languageName }
?.extensions?.contains(extension) == true
}
} ?: editor.scriptLanguage?.getName()?.lowercase()
?.let { name -> languages.firstOrNull { it.languageName == name } }
}
}
@Composable
private fun painterResource(resources: Resources, id: Int): Painter {
return rememberDrawablePainter(drawable = resources.getDrawable(id, null))
}
在
AndroidManifest.xml
中配置插件信息
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application android:icon="@drawable/ic_launcher">
<meta-data
android:name="com.m8test.plugin.description"
android:value="本插件实现了为脚本编辑器添加编程语言语法高亮显示的功能." />
<meta-data
android:name="com.m8test.plugin.url"
android:value="https://github.com/m8test/Plugins" />
<meta-data
android:name="com.m8test.plugin.type"
android:value="editor" />
<meta-data
android:name="com.m8test.plugin.name"
android:value="language-editor" />
<meta-data
android:name="com.m8test.plugin.className"
android:value="com.m8test.editor.language.LanguageEditorPlugin" />
</application>
</manifest>
com.m8test.plugin.type
: 插件类型,此处为common
com.m8test.plugin.name
: 插件名称,可为任意字符串com.m8test.plugin.className
: 插件实现类的全限定类名
Last modified: 12 June 2025