修复BUG,优化UI

This commit is contained in:
muqing 2024-02-14 23:14:18 +08:00
parent ba74f5fb98
commit 79207f7c5b
323 changed files with 45787 additions and 536 deletions

View File

@ -76,44 +76,59 @@ android {
}
}
}
/*
configurations.all {
resolutionStrategy {
force 'org.jetbrains.kotlin:kotlin-stdlib:1.6.0'
}
}
*/
dependencies {
configurations.all {
configurations.configureEach {
exclude group: 'androidx.appcompat', module: 'appcompat'
}
// implementation('org.jetbrains.kotlin:kotlin-stdlib:1.8.0')
implementation('org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.8.0') {
exclude group: 'org.jetbrains.kotlin', module: 'kotlin-stdlib'
}
api 'androidx.appcompat:appcompat:1.6.1'
api 'com.google.android.material:material:1.9.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation platform('com.google.firebase:firebase-bom:31.1.1')
implementation 'com.google.firebase:firebase-analytics-ktx'
implementation 'com.google.firebase:firebase-crashlytics-ktx'
implementation 'com.google.firebase:firebase-perf-ktx'
// implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
api project(path: ':library')
def nav_version = "2.5.1"
implementation fileTree(dir: 'libs', include: ['*.aar', '*.jar'], exclude: [])
implementation project(path: ':library')
implementation project(':imageactivity')
// implementation project(':editor')
implementation 'io.github.Rosemoe.sora-editor:editor:0.16.5'
//
// Emoji
// implementation 'com.luolc:emoji-rain:0.1.1'
implementation 'me.zhanghai.android.fastscroll:library:1.1.8'
// https://github.com/getActivity/MultiLanguages
implementation 'com.github.getActivity:MultiLanguages:8.0'
//
implementation 'com.guolindev.permissionx:permissionx:1.6.1'
implementation 'com.guolindev.permissionx:permissionx:1.7.1'
implementation "androidx.room:room-runtime:2.4.0"
//Room
// Android Jetpack Room
// SQLite Android
annotationProcessor "androidx.room:room-compiler:2.4.0"
kapt "androidx.room:room-compiler:2.4.0"
//
// 使
implementation "dev.rikka.rikkax.preference:simplemenu-preference:1.0.3"
// Material Design 使
// Material Components
//
implementation "dev.rikka.rikkax.material:material-preference:2.0.0"
//
@ -121,27 +136,49 @@ dependencies {
implementation 'com.gyf.immersionbar:immersionbar-ktx:3.0.0'
// TinyPinyin核心包80KB
implementation 'com.github.promeg:tinypinyin:2.0.3'
implementation 'com.github.promeg:tinypinyin:2.0.3'//id ReleaseModActivity
// Banner广
implementation 'io.github.youth5201314:banner:2.2.2'
// AndroidX Kotlin Kotlin Android
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'com.google.code.gson:gson:2.9.1'
//
implementation 'com.github.yalantis:ucrop:2.2.8-native'
implementation 'io.github.Rosemoe.sora-editor:editor:0.16.5'
implementation project(path: ':assistantCoreLibrary')
implementation project(path: ':dialog')
implementation 'com.afollestad.material-dialogs:bottomsheets:3.3.0'
implementation 'com.afollestad.material-dialogs:input:3.3.0'
// Android
implementation 'com.github.bumptech.glide:glide:4.11.0'
// Glide Glide
implementation 'jp.wasabeef:glide-transformations:4.3.0'
// Glide 便
implementation 'com.github.florent37:glidepalette:2.1.2'
//
implementation 'cat.ereza:customactivityoncrash:2.3.0'
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
implementation 'dev.rikka.rikkax.appcompat:appcompat:1.6.1'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
// SwipeRefreshLayout UI 使
// implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
// AppCompat Google AppCompat
// UI 使 UI
implementation 'dev.rikka.rikkax.appcompat:appcompat:1.6.1'//
// R.xml.root_preferences
// implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
def nav_version = "2.5.1"
implementation "androidx.navigation:navigation-fragment-ktx:$nav_version"
implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
// Android AndroidX Preference Kotlin
implementation 'androidx.preference:preference-ktx:1.2.0'
/* Flexbox
CSS Flexbox */
implementation 'com.google.android.flexbox:flexbox:3.0.0'
}

File diff suppressed because it is too large Load Diff

View File

@ -11,9 +11,7 @@ import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuItem
import androidx.appcompat.app.ActionBarDrawerToggle
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.GravityCompat
import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider
import androidx.navigation.findNavController
import androidx.navigation.ui.AppBarConfiguration
@ -44,7 +42,7 @@ import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.gson.Gson
import com.gyf.immersionbar.ImmersionBar
//import com.gyf.immersionbar.ImmersionBar
import org.json.JSONObject
import java.io.File
import java.util.concurrent.Executors
@ -89,7 +87,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
AppSettings.getValue(AppSettings.Setting.UseTheCommunityAsTheLaunchPage, true)
this.setStartDestination(
if (use) {
viewBinding.mainButton.hide()
// viewBinding.mainButton.hide()
R.id.community_item
} else {
R.id.mod_item
@ -125,7 +123,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
AppSettings.forceSetValue(AppSettings.Setting.UpdateData, gson.toJson(data))
ifNeedShowUpdate(data)
} else {
Snackbar.make(viewBinding.mainButton, t.message, Snackbar.LENGTH_SHORT).show()
Snackbar.make(viewBinding.root, t.message, Snackbar.LENGTH_SHORT).show()
}
}
@ -282,31 +281,31 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
// help.isVisible = isActive
codeTable.isVisible = isActive
if (mod.isChecked) {
viewBinding.mainButton.isVisible = isActive
// viewBinding.mainButton.isVisible = isActive
}
if (isActive) {
//数据库
dataBase.setOnMenuItemClickListener {
/* dataBase.setOnMenuItemClickListener {
viewBinding.mainButton.postOnAnimationDelayed({
// viewBinding.tabLayout.isVisible = false
viewBinding.mainButton.hide()
}, hideViewDelay)
false
}
}*/
template.setOnMenuItemClickListener {
/* template.setOnMenuItemClickListener {
viewBinding.mainButton.postOnAnimationDelayed({
// viewBinding.tabLayout.isVisible = true
viewBinding.mainButton.show()
}, hideViewDelay)
false
}
}*/
codeTable.setOnMenuItemClickListener {
startActivity(Intent(this@MainActivity, CodeTableActivity::class.java))
false
}
/*重要部分
viewBinding.mainButton.setOnClickListener {
val item = viewBinding.navaiagtion.checkedItem.toString()
val warehouseItem = getString(R.string.warehouse)
@ -328,10 +327,10 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
else -> {
}
}
}
}*/
}
mod.setOnMenuItemClickListener {
/* mod.setOnMenuItemClickListener {
GlobalMethod.requestStoragePermissions(this) {
if (it) {
viewBinding.mainButton.postOnAnimationDelayed({
@ -343,12 +342,8 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
}
}
false
}
}*/
community.setOnMenuItemClickListener {
viewBinding.mainButton.postOnAnimationDelayed({
// viewBinding.tabLayout.isVisible = true
viewBinding.mainButton.hide()
}, hideViewDelay)
false
}
menu.findItem(R.id.startGame).setOnMenuItemClickListener {
@ -361,7 +356,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
} else {
viewBinding.drawerlayout.closeDrawer(GravityCompat.START)
Snackbar.make(
viewBinding.mainButton,
viewBinding.root,
R.string.no_game_installed,
Snackbar.LENGTH_SHORT
).show()
@ -431,20 +426,20 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
val to = File(modDirectory + from.name)
if (FileOperator.copyFile(from, to)) {
Snackbar.make(
viewBinding.mainButton,
viewBinding.root,
String.format(getString(R.string.import_complete), from.name),
Snackbar.LENGTH_SHORT
).show()
} else {
Snackbar.make(
viewBinding.mainButton,
viewBinding.root,
String.format(getString(R.string.import_failed), from.name),
Snackbar.LENGTH_SHORT
).show()
}
} else {
Snackbar.make(
viewBinding.mainButton,
viewBinding.root,
R.string.bad_file_type,
Snackbar.LENGTH_SHORT
).show()
@ -460,7 +455,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
importTemplate(from, outputFolder)
} else {
Snackbar.make(
viewBinding.mainButton,
viewBinding.root,
R.string.bad_file_type,
Snackbar.LENGTH_SHORT
).show()
@ -486,7 +481,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
if (newInfoData == null) {
handler.post {
Snackbar.make(
viewBinding.mainButton,
viewBinding.root,
getString(R.string.import_failed2),
Snackbar.LENGTH_LONG
).show()
@ -499,7 +494,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
if (oldInfo == null) {
handler.post {
Snackbar.make(
viewBinding.mainButton,
viewBinding.root,
R.string.import_failed2,
Snackbar.LENGTH_SHORT
).show()
@ -511,7 +506,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
if (newInfo.versionNum > thisAppVersion) {
handler.post {
Snackbar.make(
viewBinding.mainButton,
viewBinding.root,
String.format(
getString(R.string.app_version_error),
formFile.name
@ -544,7 +539,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
if (newInfo == null) {
handler.post {
Snackbar.make(
viewBinding.mainButton,
viewBinding.root,
getString(R.string.import_failed2),
Snackbar.LENGTH_LONG
).show()
@ -558,7 +553,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
if (appVersion > thisAppVersion) {
handler.post {
Snackbar.make(
viewBinding.mainButton,
viewBinding.root,
String.format(
getString(R.string.app_version_error),
formFile.name
@ -586,7 +581,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
handler.post {
handler.post {
Snackbar.make(
viewBinding.mainButton,
viewBinding.root,
String.format(
getString(R.string.import_complete),
formFile.name
@ -694,7 +689,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
startViewModel.dataSetMsgLiveData.observe(this) {
if (it.isNotBlank()) {
Snackbar.make(viewBinding.mainButton, it, Snackbar.LENGTH_SHORT).show()
Snackbar.make(viewBinding.root, it, Snackbar.LENGTH_SHORT).show()
}
}
@ -744,7 +739,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
initNav()
observeStartViewModel()
//偏移fab
if (ImmersionBar.hasNavigationBar(this)) {
/* if (ImmersionBar.hasNavigationBar(this)) {
val layoutParams =
viewBinding.mainButton.layoutParams as CoordinatorLayout.LayoutParams
layoutParams.setMargins(
@ -754,7 +749,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>() {
ImmersionBar.getNavigationBarHeight(this) + GlobalMethod.dp2px(16)
)
DebugHelper.printLog("导航适配", "已调整fab按钮的位置。")
}
}*/
checkAppUpdate()
} else {
startViewModel.initAllData()

View File

@ -128,14 +128,12 @@ class SearchActivity : BaseActivity<ActivitySearchBinding>() {
false
}
}
override fun getItemCount(): Int {
val empty = list.isEmpty()
viewBinding.textview1Text1.isVisible =empty
viewBinding.deleat.isVisible = !empty
return list.size
}
}
class VH(itemView: ItemStringBinding) : RecyclerView.ViewHolder(itemView.root) {

View File

@ -1,37 +1,34 @@
package com.coldmint.rust.pro
import com.coldmint.rust.pro.base.BaseActivity
import android.os.Bundle
import com.coldmint.rust.pro.tool.AppSettings
import android.content.Intent
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.widget.Toast
import androidx.appcompat.app.AppCompatDelegate
import androidx.preference.*
import androidx.preference.ListPreference
import androidx.preference.Preference
import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import com.bumptech.glide.Glide
import com.coldmint.rust.core.tool.AppOperator
import com.coldmint.rust.core.tool.FileOperator
import com.coldmint.rust.pro.base.BaseActivity
import com.coldmint.rust.pro.databinding.ActivitySettingsBinding
import com.coldmint.rust.pro.tool.AppSettings
import com.coldmint.rust.pro.tool.GlobalMethod
import com.google.android.material.color.DynamicColors
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import rikka.material.preference.MaterialSwitchPreference
import java.io.File
import java.util.*
import kotlin.collections.ArrayList
import kotlin.concurrent.thread
import java.util.Locale
class SettingsActivity : BaseActivity<ActivitySettingsBinding>() {
class SettingsFragment : PreferenceFragmentCompat() {
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey)
val manager = preferenceManager
@ -54,8 +51,10 @@ class SettingsActivity : BaseActivity<ActivitySettingsBinding>() {
true
}
/*
val english_editing_mode =
manager.findPreference<MaterialSwitchPreference>(requireContext().getString(R.string.setting_english_editing_mode))
*/
val customizeEdit = manager.findPreference<Preference>("customize_edit")
customizeEdit!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
@ -268,7 +267,7 @@ class SettingsActivity : BaseActivity<ActivitySettingsBinding>() {
this.getTheme().applyStyle(
rikka.material.preference.R.style.ThemeOverlay_Rikka_Material3_Preference,
true
);
)
title = getString(R.string.set_up)
setReturnButton()
val settingsFragment = SettingsFragment()

View File

@ -1,14 +1,9 @@
package com.coldmint.rust.pro
import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.widget.Toast
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.checkbox.BooleanCallback
import com.afollestad.materialdialogs.checkbox.checkBoxPrompt
@ -22,7 +17,6 @@ import com.coldmint.rust.pro.base.BaseActivity
import com.coldmint.rust.pro.databinding.ActivityUserListBinding
import com.coldmint.rust.pro.ui.StableLinearLayoutManager
import com.google.android.material.divider.MaterialDividerItemDecoration
import com.google.android.material.snackbar.Snackbar
class UserListActivity : BaseActivity<ActivityUserListBinding>() {

View File

@ -1,24 +1,21 @@
package com.coldmint.rust.pro.edit
import android.content.Context
import android.os.Bundle
import com.coldmint.rust.core.database.code.CodeDataBase
import com.coldmint.rust.core.database.file.FileDataBase
import com.coldmint.rust.core.interfaces.EnglishMode
import com.coldmint.rust.core.tool.DebugHelper
import com.coldmint.rust.pro.edit.autoComplete.CodeAutoCompleteJob
import io.github.rosemoe.sora.lang.Language
import io.github.rosemoe.sora.lang.analysis.AnalyzeManager
import io.github.rosemoe.sora.lang.completion.CompletionPublisher
import io.github.rosemoe.sora.lang.completion.IdentifierAutoComplete
import io.github.rosemoe.sora.lang.format.Formatter
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandleResult
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler
import io.github.rosemoe.sora.lang.styling.Styles
import io.github.rosemoe.sora.text.CharPosition
import io.github.rosemoe.sora.text.Content
import io.github.rosemoe.sora.text.ContentReference
import io.github.rosemoe.sora.text.TextRange
import io.github.rosemoe.sora.widget.CodeEditor
import io.github.rosemoe.sora.widget.SymbolPairMatch
import java.util.*
@ -32,9 +29,8 @@ class RustLanguage() : Language, EnglishMode {
private val codeAutoCompleteJob: CodeAutoCompleteJob by lazy {
CodeAutoCompleteJob()
}
private val newlineHandler: Array<NewlineHandler> by lazy {
arrayOf<NewlineHandler>(object : NewlineHandler {
arrayOf(object : NewlineHandler {
override fun matchesRequirement(beforeText: String?, afterText: String?): Boolean {
return true
}
@ -58,6 +54,29 @@ class RustLanguage() : Language, EnglishMode {
})
}
/*
private val newlineHandler: Array<NewlineHandler> by lazy {
arrayOf(object : NewlineHandler {
override fun matchesRequirement(text: Content, position: CharPosition, style: Styles?): Boolean {
// 判断是否需要进行换行操作
return true
}
override fun handleNewline(text: Content, position: CharPosition, style: Styles?, tabSize: Int): NewlineHandleResult {
var newText = "\n"
val beforeText = text.toString()
if (beforeText.startsWith("[")) {
if (beforeText.endsWith("_")) {
newText = "name]"
} else if (!beforeText.endsWith("]")) {
newText = "]"
}
}
return NewlineHandleResult(newText, 0)
}
})
}*/
private val autoCompleteProvider: RustAutoCompleteProvider by lazy {

View File

@ -1,17 +1,10 @@
package com.coldmint.rust.pro.fragments
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.view.LayoutInflater
import android.view.View
import androidx.core.content.FileProvider
import androidx.core.view.isVisible
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.LinearLayoutManager
import com.coldmint.dialog.CoreDialog
import com.coldmint.rust.core.MapClass
import com.coldmint.rust.core.tool.AppOperator

View File

@ -1,14 +1,13 @@
package com.coldmint.rust.pro.fragments
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import androidx.core.view.isVisible
import com.coldmint.rust.pro.MainActivity
import com.coldmint.rust.pro.CreationWizardActivity
import com.coldmint.rust.pro.R
import com.coldmint.rust.pro.adapters.TemplatePageAdapter
import com.coldmint.rust.pro.base.BaseFragment
import com.coldmint.rust.pro.databinding.FragmentTemplateBinding
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
class TemplateFragment : BaseFragment<FragmentTemplateBinding>() {
@ -22,22 +21,30 @@ class TemplateFragment : BaseFragment<FragmentTemplateBinding>() {
loadTab()
}
fun loadTab() {
val mainActivity = requireActivity() as MainActivity
TabLayoutMediator(viewBinding.tabLayout, viewBinding.viewPager2)
{ tab, position ->
when (position) {
0 -> {
tab.text = getText(R.string.local)
}
else -> {
tab.text = getText(R.string.network)
}
private fun loadTab() {
// val mainActivity = requireActivity() as MainActivity
TabLayoutMediator(viewBinding.tabLayout, viewBinding.viewPager2)
{ tab, position ->
when (position) {
0 -> {
tab.text = getText(R.string.local)
}
}.attach()
/* } else {
viewBinding.viewPager2.postDelayed({ loadTab() }, MainActivity.linkInterval)
}*/
else -> {
tab.text = getText(R.string.network)
}
}
}.attach()
viewBinding.mainButton.setOnClickListener {
val intent = Intent(context, CreationWizardActivity::class.java)
intent.putExtra("type", "template")
startActivity(intent)
}
/* } else {
viewBinding.viewPager2.postDelayed({ loadTab() }, MainActivity.linkInterval)
}*/
}
override fun getViewBindingObject(layoutInflater: LayoutInflater): FragmentTemplateBinding {

View File

@ -1,13 +1,20 @@
package com.coldmint.rust.pro.fragments
import android.Manifest
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import com.coldmint.rust.pro.CreationWizardActivity
import com.coldmint.rust.pro.MainActivity
import com.coldmint.rust.pro.R
import com.coldmint.rust.pro.adapters.WarehouseAdapter
import com.coldmint.rust.pro.base.BaseFragment
import com.coldmint.rust.pro.databinding.FragmentWarehouseBinding
import com.google.android.material.tabs.TabLayoutMediator
import com.permissionx.guolindev.PermissionX
import com.permissionx.guolindev.callback.RequestCallback
import javax.security.auth.callback.Callback
/**
* @author Cold Mint
@ -15,13 +22,43 @@ import com.google.android.material.tabs.TabLayoutMediator
*/
class WarehouseFragment : BaseFragment<FragmentWarehouseBinding>() {
private fun loadTab() {
// 在需要申请权限的地方调用如下方法
/*
PermissionX.init(this)
.permissions(Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
.request { allGranted, _, _ ->
// 在这里处理权限请求结果
if (allGranted) {
// 所有权限都已授予,可以进行文件操作
} else {
// 至少有一个权限被拒绝
}
}
*/
PermissionX.init(this)
.permissions(Manifest.permission.READ_EXTERNAL_STORAGE,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
.onExplainRequestReason { scope, deniedList ->
scope.showRequestReasonDialog(deniedList, "核心基础是基于这些权限", "授权", "取消")
}
.onForwardToSettings { scope, deniedList ->
scope.showForwardToSettingsDialog(deniedList, "您需要手动允许设置中的必要权限", "授权", "取消")
}
.request { allGranted, _, _ ->
if (allGranted) {
// Toast.makeText(this, "All permissions are granted", Toast.LENGTH_LONG).show()
} else {
// Toast.makeText(this, "These permissions are denied: $deniedList", Toast.LENGTH_LONG).show()
}
}
if (isAdded) {
val mainActivity = activity as MainActivity
/* if (tableLayout == null) {
viewBinding.pager.postDelayed({ loadTab() }, MainActivity.linkInterval)
} else {*/
/* if (tableLayout == null) {
viewBinding.pager.postDelayed({ loadTab() }, MainActivity.linkInterval)
} else {*/
TabLayoutMediator(viewBinding.tabLayout, viewBinding.pager) { tab, position ->
when (position) {
0 -> {
@ -33,6 +70,11 @@ class WarehouseFragment : BaseFragment<FragmentWarehouseBinding>() {
}
}
}.attach()
viewBinding.mainButton.setOnClickListener {
val intent = Intent(context, CreationWizardActivity::class.java)
intent.putExtra("type", "mod")
startActivity(intent)
}
}
}

View File

@ -1,8 +1,10 @@
package com.coldmint.rust.pro.fragments
import android.content.Intent
import android.graphics.drawable.BitmapDrawable
import android.os.Bundle
import android.view.LayoutInflater
import android.widget.ImageView
import androidx.core.view.isVisible
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModelProvider
@ -139,19 +141,19 @@ class WebModDetailsFragment(val modId: String, val modNameLiveData: MutableLiveD
viewBinding.loadLayout.isVisible = false
viewBinding.contentLayout.isVisible = true
val icon = t.data.icon
if (icon != null && icon.isNotBlank()) {
if (!icon.isNullOrBlank()) {
Glide.with(requireContext())
.load(ServerConfiguration.getRealLink(icon))
.apply(GlobalMethod.getRequestOptions())
.into(viewBinding.iconView)
}
val screenshotListData = t.data.screenshots
if (screenshotListData != null && screenshotListData.isNotBlank()) {
if (!screenshotListData.isNullOrBlank()) {
val list = ArrayList<String>()
val lineParser = LineParser()
lineParser.symbol = ","
lineParser.text = screenshotListData
lineParser.analyse { lineNum, lineData, isEnd ->
lineParser.analyse { _, lineData, _ ->
list.add(lineData)
true
}
@ -167,9 +169,18 @@ class WebModDetailsFragment(val modId: String, val modNameLiveData: MutableLiveD
.load(ServerConfiguration.getRealLink(data))
.apply(GlobalMethod.getRequestOptions())
.into(holder.imageView)
holder.imageView.setOnClickListener {v ->
val bitmap = (v as ImageView).
drawable?.let { (it as BitmapDrawable).bitmap }
com.imageactivity.Image.start(requireActivity(), v, bitmap)
}
}
}
}
// com.imageactivity.Image
viewBinding.banner.setAdapter(adapter)
viewBinding.banner.addBannerLifecycleObserver(requireActivity())
viewBinding.banner.indicator = CircleIndicator(requireActivity())
@ -182,7 +193,7 @@ class WebModDetailsFragment(val modId: String, val modNameLiveData: MutableLiveD
val lineParser = LineParser(tags)
val tagList = ArrayList<String>()
lineParser.symbol = ","
lineParser.analyse { lineNum, lineData, isEnd ->
lineParser.analyse { _, lineData, _ ->
val tag = lineData.subSequence(1, lineData.length - 1).toString()
tagList.add(tag)
true

View File

@ -20,7 +20,7 @@ import java.util.Collections;
/** @noinspection unused*/
public class gj {
public static String log_TAB = "铁锈助手";
public static String log_TAB = "打印";
public static void ts(Context a, Object b) {
Toast.makeText(a, b.toString(), Toast.LENGTH_SHORT).show();

View File

@ -8,5 +8,5 @@
android:topRightRadius="16dp" />
<stroke
android:width="1dp"
android:color="?attr/textFillColor" />
android:color="?attr/colorOnSecondaryContainer" />
</shape>

View File

@ -3,26 +3,23 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/editDrawerlayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:fitsSystemWindows="false">
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@android:color/transparent"
android:fitsSystemWindows="true">
android:background="@android:color/transparent">
<com.google.android.material.appbar.MaterialToolbar
android:id="@id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
<com.google.android.material.tabs.TabLayout
android:id="@id/tabLayout"
android:layout_width="match_parent"
@ -34,16 +31,12 @@
app:tabMode="scrollable"
app:tabTextAppearance="@style/TabLayoutTextStyle" />
</com.google.android.material.appbar.AppBarLayout>
<ProgressBar
android:id="@+id/myProgressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true" />
<io.github.rosemoe.sora.widget.CodeEditor
android:id="@+id/codeEditor"
android:layout_width="match_parent"
@ -51,8 +44,6 @@
android:layout_above="@id/searchLayout"
android:layout_below="@id/appBarLayout"
android:visibility="visible" />
<com.google.android.material.card.MaterialCardView
android:id="@+id/searchLayout"
style="@style/Widget.Material3.CardView.Elevated"
@ -62,8 +53,6 @@
android:layout_margin="8dp"
android:orientation="vertical"
android:visibility="gone">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
@ -89,8 +78,8 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:visibility="gone"
android:hint="@string/replace">
android:hint="@string/replace"
android:visibility="gone">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/replaceEditText"
@ -150,21 +139,15 @@
</LinearLayout>
</com.google.android.material.card.MaterialCardView>
<androidx.recyclerview.widget.RecyclerView
android:id="@id/recyclerview"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:visibility="invisible" />
</RelativeLayout>
<!-- <include layout="@layout/edit_end" />-->
<include layout="@layout/edit_start" />
<!-- <include layout="@layout/edit_end" />-->
<include
layout="@layout/edit_start" />
</androidx.drawerlayout.widget.DrawerLayout>

View File

@ -9,8 +9,7 @@
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
@ -21,10 +20,7 @@
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize" />
</com.google.android.material.appbar.AppBarLayout>
<fragment
android:id="@+id/baseFragment"
android:name="androidx.navigation.fragment.NavHostFragment"
@ -32,17 +28,6 @@
android:layout_height="match_parent"
app:defaultNavHost="true"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/mainButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:fitsSystemWindows="true"
android:src="@drawable/add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>
<com.google.android.material.navigation.NavigationView

View File

@ -1,10 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center">
android:layout_height="wrap_content">
<Button
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog"
style="@style/Widget.MaterialComponents.Button.TextButton.Dialog.Flush"
android:id="@+id/codeTextItemView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"

View File

@ -5,29 +5,23 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="start"
android:background="?android:windowBackground">
android:background="?android:colorBackground">
<androidx.cardview.widget.CardView
<LinearLayout
android:id="@+id/titleView"
android:layout_width="match_parent"
android:layout_height="?android:attr/actionBarSize"
android:layout_alignParentTop="true">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
app:cardElevation="2dp">
<TextView
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/file_manager" />
</LinearLayout>
</androidx.cardview.widget.CardView>
android:gravity="center"
android:fitsSystemWindows="true"
android:background="?android:windowBackground"
app:cardElevation="2dp">
<TextView
style="@style/TextAppearance.Material3.TitleMedium"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/file_manager" />
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
@ -37,8 +31,8 @@
android:orientation="vertical">
<TextView
style="@style/TextAppearance.Material3.TitleSmall"
android:id="@+id/unableOpenView"
style="@style/TextAppearance.Material3.TitleSmall"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/unable_to_open_this_directory" />

View File

@ -12,5 +12,4 @@
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@ -1,17 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent" />
android:layout_height="wrap_content">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/viewPager2"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</LinearLayout>
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/mainButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:fitsSystemWindows="true"
android:src="@drawable/add" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -1,18 +1,34 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent" />
android:layout_height="wrap_content">
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/transparent" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent" />
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/mainButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|end"
android:layout_margin="16dp"
android:layout_marginEnd="24dp"
android:layout_marginBottom="24dp"
android:fitsSystemWindows="true"
android:src="@drawable/add" />
</LinearLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -42,7 +42,8 @@
android:layout_marginHorizontal="16dp"
android:layout_marginBottom="60dp"
android:animateLayoutChanges="true"
android:orientation="vertical">
android:orientation="vertical"
android:visibility="visible">
<TextView
android:id="@+id/hideTextView"
@ -51,7 +52,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:text="@string/audit"
android:visibility="gone" />
android:visibility="visible" />
<com.google.android.material.card.MaterialCardView
@ -126,7 +127,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:orientation="vertical"
android:visibility="gone">
android:visibility="visible">
<RelativeLayout

View File

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -2,17 +2,15 @@ package com.coldmint.rust.core.database.code
import android.content.Context
import android.database.sqlite.SQLiteConstraintException
import android.util.Log
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Index
import androidx.room.RenameColumn
import androidx.room.RenameTable
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.AutoMigrationSpec
import com.coldmint.rust.core.DataSet
import com.coldmint.rust.core.dataBean.dataset.*
import com.coldmint.rust.core.dataBean.dataset.ChainInspectionDataBean
import com.coldmint.rust.core.dataBean.dataset.CodeDataBean
import com.coldmint.rust.core.dataBean.dataset.GameVersionDataBean
import com.coldmint.rust.core.dataBean.dataset.SectionDataBean
import com.coldmint.rust.core.dataBean.dataset.ValueTypeDataBean
import com.coldmint.rust.core.database.file.FileDataBase
import com.coldmint.rust.core.debug.LogCat
import com.coldmint.rust.core.tool.FileOperator

View File

@ -1,21 +0,0 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@ -1,6 +1,5 @@
package com.coldmint.dialog
import android.app.Dialog
import android.content.Context
import android.view.View
import androidx.appcompat.app.AlertDialog

1
editor/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
/build

37
editor/build.gradle Normal file
View File

@ -0,0 +1,37 @@
plugins {
id("com.android.library")
id("kotlin-android")
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 32
namespace = "io.github.rosemoe.sora"
defaultConfig {
minSdk 21
targetSdk 32
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
kotlinOptions {
jvmTarget = '1.8'
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation('androidx.collection:collection:1.4.0')
}

View File

@ -0,0 +1,27 @@
<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
~ sora-editor - the awesome code editor for Android
~ https://github.com/Rosemoe/sora-editor
~ Copyright (C) 2020-2024 Rosemoe
~
~ This library is free software; you can redistribute it and/or
~ modify it under the terms of the GNU Lesser General Public
~ License as published by the Free Software Foundation; either
~ version 2.1 of the License, or (at your option) any later version.
~
~ This library 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
~ Lesser General Public License for more details.
~
~ You should have received a copy of the GNU Lesser General Public
~ License along with this library; if not, write to the Free Software
~ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
~ USA
~
~ Please contact Rosemoe by email 2073412493@qq.com if you need
~ additional information or have any questions
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
</manifest>

View File

@ -0,0 +1,68 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora;
import android.content.Context;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
/**
* Map editor built-in string resources to your given string resource. Editor string resource has
* limited i18n function, as it only contains English and Chinese.
* <p>
* Note that you should configure this before creating editor instances
*
* @author Rosemoe
*/
public class I18nConfig {
private static final SparseIntArray mapping = new SparseIntArray();
/**
* Map the given editor resId to new one
*/
public static void mapTo(int originalResId, int newResId) {
mapping.put(originalResId, newResId);
}
/**
* Get mapped resource id or itself
*/
public static int getResourceId(int resId) {
int newResource = mapping.get(resId);
if (newResource == 0) {
return resId;
}
return newResource;
}
/**
* Get mapped resource string
*/
public static String getString(@NonNull Context context, int resId) {
return context.getString(getResourceId(resId));
}
}

View File

@ -0,0 +1,40 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* This annotation marks those fields, methods and constructors experimentally created.
* <p>
* Methods, fields and constructors with this annotation is very subject to keep or delete.
* For that reason, they are not stable for production use.
*/
@Target({ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Experimental {
}

View File

@ -0,0 +1,39 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.annotations;
import android.view.View;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks you must call {@link View#invalidate()} after changing this property
*/
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.FIELD})
public @interface InvalidateRequired {
}

View File

@ -0,0 +1,38 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Marks that this member is internally used and that it is not recommended using this
* member at your side.
*/
@Retention(RetentionPolicy.CLASS)
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.CONSTRUCTOR, ElementType.TYPE})
public @interface UnsupportedUserUsage {
}

View File

@ -0,0 +1,82 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.data;
import java.util.ArrayList;
import java.util.List;
import io.github.rosemoe.sora.lang.styling.CodeBlock;
/**
* An object provider for speed improvement
* Now meaningless because it is not as well as it expected
*
* @author Rose
*/
public class ObjectAllocator {
private static final int RECYCLE_LIMIT = 1024 * 8;
private static List<CodeBlock> codeBlocks;
private static List<CodeBlock> tempArray;
public static void recycleBlockLines(List<CodeBlock> src) {
if (src == null) {
return;
}
if (codeBlocks == null) {
codeBlocks = src;
return;
}
int size = codeBlocks.size();
int sizeAnother = src.size();
while (sizeAnother > 0 && size < RECYCLE_LIMIT) {
size++;
sizeAnother--;
var obj = src.get(sizeAnother);
obj.clear();
codeBlocks.add(obj);
}
src.clear();
synchronized (ObjectAllocator.class) {
tempArray = src;
}
}
public static List<CodeBlock> obtainList() {
List<CodeBlock> temp = null;
synchronized (ObjectAllocator.class) {
temp = tempArray;
tempArray = null;
}
if (temp == null) {
temp = new ArrayList<>(128);
}
return temp;
}
public static CodeBlock obtainBlockLine() {
return (codeBlocks == null || codeBlocks.isEmpty()) ? new CodeBlock() : codeBlocks.remove(codeBlocks.size() - 1);
}
}

View File

@ -0,0 +1,48 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.lang.styling.Span;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.TextRange;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* Report a single click
*
* @author Rosemoe
*/
public class ClickEvent extends EditorMotionEvent {
public ClickEvent(@NonNull CodeEditor editor, @NonNull CharPosition position, @NonNull MotionEvent event,
@Nullable Span span, @Nullable TextRange spanRange) {
super(editor, position, event, span, spanRange);
}
}

View File

@ -0,0 +1,43 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
package io.github.rosemoe.sora.event
import io.github.rosemoe.sora.widget.CodeEditor
/**
* Event trigger when any updates to the editor color scheme, including setting a new color and setting
* a new color scheme
*
* @author Rosemoe
*/
class ColorSchemeUpdateEvent(editor: CodeEditor) : Event(editor) {
/**
* Updated color scheme (the new one if new color scheme is set)
*/
val colorScheme
get() = editor.colorScheme
}

View File

@ -0,0 +1,121 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* This event happens when {@link CodeEditor#setText(CharSequence)} is called or
* user edited the displaying content.
* <p>
* Note that you should not update the content at this time. Otherwise, there might be some
* exceptions causing the editor framework to crash. If you do need to update the content, you should
* post your actions to the main thread so that the user's modification will be successful.
*
* @author Rosemoe
*/
public class ContentChangeEvent extends Event {
/**
* Notify that {@link CodeEditor#setText(CharSequence)} is called
*/
public final static int ACTION_SET_NEW_TEXT = 1;
/**
* Notify that user inserted some texts to the content
*/
public final static int ACTION_INSERT = 2;
/**
* Notify that user deleted some texts in the content
*/
public final static int ACTION_DELETE = 3;
private final int action;
private final CharPosition start;
private final CharPosition end;
private final CharSequence textChanged;
private final boolean causedByUndoManager;
public ContentChangeEvent(@NonNull CodeEditor editor, int action, @NonNull CharPosition changeStart, @NonNull CharPosition changeEnd, @NonNull CharSequence textChanged, boolean causeByUndoManager) {
super(editor);
this.action = action;
start = changeStart;
end = changeEnd;
this.textChanged = textChanged;
causedByUndoManager = causeByUndoManager;
}
/**
* Get action code of the event.
*
* @see #ACTION_SET_NEW_TEXT
* @see #ACTION_INSERT
* @see #ACTION_DELETE
*/
public int getAction() {
return action;
}
/**
* Return the CharPosition indicating the start of changed region.
* <p>
* Note that you can not modify the values in the returned instance.
*/
@NonNull
public CharPosition getChangeStart() {
return start;
}
/**
* Return the CharPosition indicating the end of changed region.
* <p>
* Note that you can not modify the values in the returned instance.
*/
@NonNull
public CharPosition getChangeEnd() {
return end;
}
/**
* Return the changed text in this modification.
* If action is {@link #ACTION_SET_NEW_TEXT}, Content instance is returned.
* If action is {@link #ACTION_INSERT}, inserted text is the result.
* If action is {@link #ACTION_DELETE}, deleted text is the result.
*/
@NonNull
public CharSequence getChangedText() {
return textChanged;
}
/**
* If the content change is caused by undo/redo
*/
public boolean isCausedByUndoManager() {
return causedByUndoManager;
}
}

View File

@ -0,0 +1,49 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.lang.styling.Span;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.TextRange;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* Report double click in editor.
* This event can be intercepted.
*
* @author Rosemoe
*/
public class DoubleClickEvent extends EditorMotionEvent {
public DoubleClickEvent(@NonNull CodeEditor editor, @NonNull CharPosition position, @NonNull MotionEvent event,
@Nullable Span span, @Nullable TextRange spanRange) {
super(editor, position, event, span, spanRange);
}
}

View File

@ -0,0 +1,151 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.text.method.KeyMetaStates;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* Receives key related events in editor.
* <p>
* You may set a boolean for editor to return to the Android KeyEvent framework.
*
* @author Rosemoe
* @see ResultedEvent#setResult(Object)
* <p>
* This class mirrors methods of {@link KeyEvent}, but some methods are changed:
* @see #isAltPressed()
* @see #isShiftPressed()
*/
public class EditorKeyEvent extends ResultedEvent<Boolean> {
private final KeyEvent src;
private final Type type;
private final boolean shiftPressed;
private final boolean altPressed;
public EditorKeyEvent(@NonNull CodeEditor editor, @NonNull KeyEvent src, @NonNull Type type) {
super(editor);
this.src = src;
this.type = type;
shiftPressed = getEditor().getKeyMetaStates().isShiftPressed();
altPressed = getEditor().getKeyMetaStates().isAltPressed();
}
@Override
public boolean canIntercept() {
return true;
}
public int getAction() {
return src.getAction();
}
public int getKeyCode() {
return src.getKeyCode();
}
public int getRepeatCount() {
return src.getRepeatCount();
}
public int getMetaState() {
return src.getMetaState();
}
public int getModifiers() {
return src.getModifiers();
}
public long getDownTime() {
return src.getDownTime();
}
/**
* Get the key event type.
*
* @return The key event type.
*/
@NonNull
public Type getEventType() {
return this.type;
}
@Override
public long getEventTime() {
return src.getEventTime();
}
/**
* editor change: track shift/alt by {@link KeyMetaStates}
*/
public boolean isShiftPressed() {
return shiftPressed;
}
/**
* editor change: track shift/alt by {@link KeyMetaStates}
*/
public boolean isAltPressed() {
return altPressed;
}
public boolean isCtrlPressed() {
return (src.getMetaState() & KeyEvent.META_CTRL_ON) != 0;
}
public final boolean result(boolean editorResult) {
var userResult = isResultSet() ? getResult() : false;
if (isIntercepted()) {
return userResult;
} else {
return userResult || editorResult;
}
}
/**
* The type of {@link EditorKeyEvent}.
*/
public enum Type {
/**
* Used for {@link CodeEditor#onKeyUp(int, KeyEvent)}.
*/
UP,
/**
* Used for {@link CodeEditor#onKeyDown(int, KeyEvent)}.
*/
DOWN,
/**
* Used for {@link CodeEditor#onKeyMultiple(int, int, KeyEvent)}.
*/
MULTIPLE
}
}

View File

@ -0,0 +1,118 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import android.view.InputDevice;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.lang.styling.Span;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.TextRange;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* Base class for click events
*
* @author Rosemoe
* @see ClickEvent
* @see DoubleClickEvent
* @see LongPressEvent
*/
public abstract class EditorMotionEvent extends Event {
private final CharPosition pos;
private final MotionEvent event;
private final Span span;
private final TextRange spanRange;
public EditorMotionEvent(@NonNull CodeEditor editor, @NonNull CharPosition position,
@NonNull MotionEvent event, @Nullable Span span, @Nullable TextRange spanRange) {
super(editor);
this.pos = position;
this.event = event;
this.span = span;
this.spanRange = spanRange;
}
@Override
public boolean canIntercept() {
return true;
}
public boolean isFromMouse() {
return event.isFromSource(InputDevice.SOURCE_MOUSE);
}
public int getLine() {
return pos.line;
}
public int getColumn() {
return pos.column;
}
public int getIndex() {
return pos.index;
}
public CharPosition getCharPosition() {
return pos.fromThis();
}
public float getX() {
return event.getX();
}
public float getY() {
return event.getY();
}
/**
* Get original event object from Android framework
*/
@NonNull
public MotionEvent getCausingEvent() {
return event;
}
/**
* Get span at event character position, maybe null.
*/
@Nullable
public Span getSpan() {
return span;
}
/**
* Get span range at event character position, maybe null
*/
@Nullable
public TextRange getSpanRange() {
return spanRange;
}
}

View File

@ -0,0 +1,131 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import androidx.annotation.NonNull;
import java.util.Objects;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* An Event object describes an event of editor.
* It includes several attributes such as time and the editor object.
* Subclasses of Event will define their own fields or methods.
*
* @author Rosemoe
*/
public abstract class Event {
private final long mEventTime;
private final CodeEditor mEditor;
private int mInterceptTargets;
public Event(@NonNull CodeEditor editor) {
this(editor, System.currentTimeMillis());
}
public Event(@NonNull CodeEditor editor, long eventTime) {
mEditor = Objects.requireNonNull(editor);
mEventTime = eventTime;
mInterceptTargets = 0;
}
/**
* Get event time
*/
public long getEventTime() {
return mEventTime;
}
/**
* Get the editor
*/
@NonNull
public CodeEditor getEditor() {
return mEditor;
}
/**
* Check whether this event can be intercepted (so that the event is not sent to other
* receivers after being intercepted)
* Intercept-able events:
*
* @see LongPressEvent
* @see ClickEvent
* @see DoubleClickEvent
* @see EditorKeyEvent
*/
public boolean canIntercept() {
return false;
}
/**
* Intercept the event for all targets.
* <p>
* Make sure {@link #canIntercept()} returns true. Otherwise, an {@link UnsupportedOperationException}
* will be thrown.
*
* @see InterceptTarget
*/
public void intercept() {
if (!canIntercept()) {
throw new UnsupportedOperationException("intercept() not supported");
}
mInterceptTargets = InterceptTarget.TARGET_EDITOR | InterceptTarget.TARGET_RECEIVERS;
}
/**
* Intercept the event for some targets
*
* @param targets Masks for target types
* @see InterceptTarget
*/
public void intercept(int targets) {
if (!canIntercept()) {
throw new UnsupportedOperationException("intercept() not supported");
}
mInterceptTargets = targets;
}
/**
* Get intercepted dispatch targets
*
* @see #intercept(int)
* @see InterceptTarget
*/
public int getInterceptTargets() {
return mInterceptTargets;
}
/**
* Check whether this event is intercepted for some types of targets
*
* @see #getInterceptTargets()
*/
public boolean isIntercepted() {
return mInterceptTargets != 0;
}
}

View File

@ -0,0 +1,328 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Vector;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.function.Consumer;
/**
* This class manages event dispatching in editor.
* Users can either register their event receivers here or dispatch event to
* the receivers in this manager.
* <p>
* There may be several EventManagers in one editor instance. For example, each plugin
* will have it own EventManager and the editor also has a root event manager for external
* listeners.
* <p>
* Note that the event type must be exact. That's to say, you need to use a terminal class instead
* of using its parent classes. For instance, if you register a receiver with the event type {@link Event},
* no event will be sent to your receiver.
*
* @author Rosemoe
*/
public final class EventManager {
@SuppressWarnings("rawtypes")
private final Map<Class<?>, Receivers> receivers;
private final ReadWriteLock lock;
private final EventManager parent;
private final List<EventManager> children;
private final EventReceiver<?>[][] caches = new EventReceiver[5][];
private boolean enabled = true;
private boolean detached = false;
/**
* Create an EventManager with no parent
*/
public EventManager() {
this(null);
}
/**
* Create an EventManager with the given parent.
* Null for no parent.
*/
public EventManager(@Nullable EventManager parent) {
receivers = new HashMap<>();
this.parent = parent;
lock = new ReentrantReadWriteLock();
children = new Vector<>();
if (parent != null) {
parent.children.add(this);
}
}
/**
* Check is the manager enabled
*/
public boolean isEnabled() {
return enabled;
}
/**
* Set enabled.
* Disabled EventManager will not deliver event to its subscribers or children.
* Root EventManager can not be disabled.
*/
public void setEnabled(boolean enabled) {
if (parent == null && !enabled) {
throw new IllegalStateException("The event manager is set to be root, and can not be disabled");
}
this.enabled = enabled;
}
/**
* Get root node
*/
@NonNull
public EventManager getRootManager() {
checkDetached();
return parent == null ? this : parent.getRootManager();
}
/**
* Get root manager and dispatch the given event
*
* @see #dispatchEvent(Event)
*/
public <T extends Event> int dispatchEventFromRoot(@NonNull T event) {
return getRootManager().dispatchEvent(event);
}
/**
* Detached from parent.
* This manager will not receive future events from parent
*/
public void detach() {
if (parent == null) {
throw new IllegalStateException("root manager can not be detached");
}
checkDetached();
detached = true;
parent.children.remove(this);
}
private void checkDetached() {
if (detached) {
throw new IllegalStateException("already detached");
}
}
/**
* Get receivers container of a given event type safely
*/
@NonNull
@SuppressWarnings("unchecked")
<T extends Event> Receivers<T> getReceivers(@NonNull Class<T> type) {
lock.readLock().lock();
Receivers<T> result;
try {
result = receivers.get(type);
} finally {
lock.readLock().unlock();
}
if (result == null) {
lock.writeLock().lock();
try {
result = receivers.get(type);
if (result == null) {
result = new Receivers<>();
receivers.put(type, result);
}
} finally {
lock.writeLock().unlock();
}
}
return result;
}
/**
* @see #subscribeEvent(Class, EventReceiver)
*/
@NonNull
public <T extends Event> SubscriptionReceipt<T> subscribe(@NonNull Class<T> eventType, @NonNull EventReceiver<T> receiver) {
return subscribeEvent(eventType, receiver);
}
/**
* Subscribe a event and never unsubscribe from event receiver
*/
@NonNull
public <T extends Event> SubscriptionReceipt<T> subscribeAlways(@NonNull Class<T> eventType, @NonNull NoUnsubscribeReceiver<T> receiver) {
return subscribeEvent(eventType, ((event, unsubscribe) -> receiver.onReceive(event)));
}
/**
* Register a receiver of the given event.
*
* @param eventType Event type to be received
* @param receiver Receiver of event
* @param <T> Event type
*/
@NonNull
public <T extends Event> SubscriptionReceipt<T> subscribeEvent(@NonNull Class<T> eventType, @NonNull EventReceiver<T> receiver) {
var receivers = getReceivers(eventType);
receivers.lock.writeLock().lock();
try {
var list = receivers.receivers;
if (list.contains(receiver)) {
// Simply detect if the event receiver has been added and return the SubscriptionReceipt directly.
// Even if add multiple subscription, actually send an event, the event receiver will only receive an event once.
// See also how LiveData does it:
// https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lifecycle/lifecycle-livedata-core/src/main/java/androidx/lifecycle/LiveData.java;l=190;drc=b69fe340ccf37160705e6d7dc512b814fd6bb100
return new SubscriptionReceipt<>(this, eventType, receiver);
}
list.add(receiver);
} finally {
receivers.lock.writeLock().unlock();
}
return new SubscriptionReceipt<>(this, eventType, receiver);
}
/**
* Dispatch the given event to its receivers registered in this manager.
*
* @param event Event to dispatch
* @param <T> Event type
* @return <> </>he event's intercept targets
*/
@SuppressWarnings("unchecked")
public <T extends Event> int dispatchEvent(@NonNull T event) {
if (!enabled) {
return event.getInterceptTargets();
}
// Safe cast
var receivers = getReceivers((Class<T>) event.getClass());
receivers.lock.readLock().lock();
EventReceiver<T>[] receiverArr;
int count;
try {
count = receivers.receivers.size();
receiverArr = obtainBuffer(count);
receivers.receivers.toArray(receiverArr);
} finally {
receivers.lock.readLock().unlock();
}
List<EventReceiver<T>> unsubscribedReceivers = null;
try {
Unsubscribe unsubscribe = new Unsubscribe();
for (int i = 0; i < count && (event.getInterceptTargets() & InterceptTarget.TARGET_RECEIVERS) == 0; i++) {
var receiver = receiverArr[i];
receiver.onReceive(event, unsubscribe);
if (unsubscribe.isUnsubscribed()) {
if (unsubscribedReceivers == null) {
unsubscribedReceivers = new LinkedList<>();
}
unsubscribedReceivers.add(receiver);
}
unsubscribe.reset();
}
} finally {
if (unsubscribedReceivers != null) {
receivers.lock.writeLock().lock();
try {
receivers.receivers.removeAll(unsubscribedReceivers);
} finally {
receivers.lock.writeLock().unlock();
}
}
recycleBuffer(receiverArr);
}
for (int i = 0; i < children.size() && (event.getInterceptTargets() & InterceptTarget.TARGET_RECEIVERS) == 0; i++) {
EventManager sub = null;
try {
sub = children.get(i);
} catch (IndexOutOfBoundsException e) {
// concurrent mod ignored
}
if (sub != null) {
sub.dispatchEvent(event);
}
}
return event.getInterceptTargets();
}
@NonNull
@SuppressWarnings("unchecked")
private <V extends Event> EventReceiver<V>[] obtainBuffer(int size) {
EventReceiver<V>[] res = null;
synchronized (this) {
for (int i = 0; i < caches.length; i++) {
if (caches[i] != null && caches[i].length >= size) {
res = (EventReceiver<V>[]) caches[i];
caches[i] = null;
break;
}
}
}
if (res == null) {
res = new EventReceiver[size];
}
return res;
}
private synchronized void recycleBuffer(@Nullable EventReceiver<?>[] array) {
if (array == null) {
return;
}
for (int i = 0; i < caches.length; i++) {
if (caches[i] == null) {
Arrays.fill(array, null);
caches[i] = array;
break;
}
}
}
/**
* Internal class for saving receivers of each type
*
* @param <T> Event type
*/
static class Receivers<T extends Event> {
ReadWriteLock lock = new ReentrantReadWriteLock();
List<EventReceiver<T>> receivers = new ArrayList<>();
}
public interface NoUnsubscribeReceiver<T extends Event> {
void onReceive(T event);
}
}

View File

@ -0,0 +1,32 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import androidx.annotation.NonNull;
public interface EventReceiver<T extends Event> {
void onReceive(@NonNull T event, @NonNull Unsubscribe unsubscribe);
}

View File

@ -0,0 +1,66 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* Notifies a selection handle's touch state has changed
*
* @author Rosemoe
*/
public class HandleStateChangeEvent extends Event {
public final static int HANDLE_TYPE_INSERT = 0;
public final static int HANDLE_TYPE_LEFT = 1;
public final static int HANDLE_TYPE_RIGHT = 2;
private final int which;
private final boolean isHeld;
public HandleStateChangeEvent(@NonNull CodeEditor editor, int which, boolean heldState) {
super(editor);
this.which = which;
isHeld = heldState;
}
/**
* Get handle type of this event
* @see #HANDLE_TYPE_LEFT
* @see #HANDLE_TYPE_RIGHT
* @see #HANDLE_TYPE_INSERT
*/
public int getHandleType() {
return which;
}
/**
* Is the handle held now
*/
public boolean isHeld() {
return isHeld;
}
}

View File

@ -0,0 +1,43 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
/**
* Define available intercept targets. You may intercept one or more targets of the given event.
*
* @author Rosemoe
*/
public interface InterceptTarget {
/**
* Registered receivers in the event dispatching graph
*/
int TARGET_RECEIVERS = 1;
/**
* Editor built-in behavior
*/
int TARGET_EDITOR = 1 << 1;
}

View File

@ -0,0 +1,76 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import android.view.KeyEvent;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* Keybinding event in editor.
*
* <p> This is different from {@link EditorKeyEvent}.
* An {@code EditorKeyEvent} is dispatched by the editor whenever there is a key event.
* However, a {@code KeyBindingEvent} is dispatched only for keybindings i.e.
* when multiple keys are pressed at once.
* For example, <b>Ctrl + X, Ctrl + D, Ctrl + Alt + O, etc.</b>
* </p>
*
* <p>
* This event is dispatched <strong>after</strong> the {@link EditorKeyEvent}.
* So, if any {@code EditorKeyEvent} consumes the event (sets the {@link InterceptTarget#TARGET_EDITOR} flag),
* {@code KeyBindingEvent} will not be called.
* </p>
*
* @author Akash Yadav
*/
public class KeyBindingEvent extends EditorKeyEvent {
private final boolean editorAbleToHandle;
/**
* Creates a new {@code KeyBindingEvent} instance.
*
* @param editor The editor.
* @param src The source {@link KeyEvent}.
* @param type The key event type.
* @param editorAbleToHandle <code>true</code> if the editor can handle this event, <code>false</code> otherwise.
*/
public KeyBindingEvent(@NonNull CodeEditor editor, @NonNull KeyEvent src, Type type, boolean editorAbleToHandle) {
super(editor, src, type);
this.editorAbleToHandle = editorAbleToHandle;
}
/**
* Is the editor capable of handling this key binding event?
*
* @return <code>true</code> if the editor can handle this event. <code>false</code> otherwise.
*/
public boolean canEditorHandle() {
return this.editorAbleToHandle;
}
}

View File

@ -0,0 +1,52 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import android.view.MotionEvent;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.lang.styling.Span;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.TextRange;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* Long press event.
* <p>
* This event can be intercepted so that the editor framework will do nothing (such as selecting a word). You can take over the
* procedure. Note that after intercepting an event, it will not be sent to other listeners, either.
*
* @author Rosemoe
*/
public class LongPressEvent extends EditorMotionEvent {
public LongPressEvent(@NonNull CodeEditor editor, @NonNull CharPosition position, @NonNull MotionEvent event,
@Nullable Span span, @Nullable TextRange spanRange) {
super(editor, position, event, span, spanRange);
}
}

View File

@ -0,0 +1,39 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
package io.github.rosemoe.sora.event
import io.github.rosemoe.sora.widget.CodeEditor
/**
* Event when search result is available in main thread.
* Note that this is also triggered when query is changed to null.
*
* @author Rosemoe
*/
class PublishSearchResultEvent(editor: CodeEditor) : Event(editor) {
fun getSearcher() = editor.searcher
}

View File

@ -0,0 +1,57 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* Event with a result
*
* @param <T> Result type
*/
public abstract class ResultedEvent<T> extends Event {
private T result;
public ResultedEvent(@NonNull CodeEditor editor) {
super(editor);
}
@Nullable
public T getResult() {
return result;
}
public void setResult(@Nullable T result) {
this.result = result;
}
public boolean isResultSet() {
return result != null;
}
}

View File

@ -0,0 +1,125 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* Reports a scroll in editor.
* The scrolling action can either have run or be running when this event is generated and sent.
* <p>
* The returned x,y positions are usually positive when over-scrolling is disabled. They represent
* the left-top position's pixel in editor.
*/
public class ScrollEvent extends Event {
/**
* Caused by thumb's exact movements
*/
public final static int CAUSE_USER_DRAG = 1;
/**
* Caused by fling after user's movements
*/
public final static int CAUSE_USER_FLING = 2;
/**
* Caused by calling {@link CodeEditor#ensurePositionVisible(int, int)}.
* This can happen when this method is manually called or either the user edits the text
*/
public final static int CAUSE_MAKE_POSITION_VISIBLE = 3;
/**
* Caused by the user's thumb reaching the edge of editor viewport, which causes the editor to
* scroll to move the selection to text currently outside the viewport.
*/
public final static int CAUSE_TEXT_SELECTING = 4;
public final static int CAUSE_SCALE_TEXT = 5;
private final int startX;
private final int startY;
private final int endX;
private final int endY;
private final int cause;
private float flingVelocityX;
private float flingVelocityY;
public ScrollEvent(@NonNull CodeEditor editor, int startX, int startY, int endX, int endY, int cause) {
this(editor, startX, startY, endX, endY, cause, 0f, 0f);
}
public ScrollEvent(@NonNull CodeEditor editor, int startX, int startY, int endX, int endY, int cause, float vx, float vy) {
super(editor);
this.startX = startX;
this.startY = startY;
this.endX = endX;
this.endY = endY;
this.cause = cause;
this.flingVelocityX = vx;
this.flingVelocityY = vy;
}
/**
* Get the start x
*/
public int getStartX() {
return startX;
}
/**
* Get the start y
*/
public int getStartY() {
return startY;
}
/**
* Get end x
*/
public int getEndX() {
return endX;
}
/**
* Get end y
*/
public int getEndY() {
return endY;
}
/**
* Get the cause of the scroll
*/
public int getCause() {
return cause;
}
public float getFlingVelocityX() {
return flingVelocityX;
}
public float getFlingVelocityY() {
return flingVelocityY;
}
}

View File

@ -0,0 +1,127 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.widget.CodeEditor;
import io.github.rosemoe.sora.widget.EditorSearcher;
/**
* This event happens when text is edited by the user, or the user click the view to change the
* position of selection. Even when the actual values of CharPosition are not changed, you may receive the event.
* <p>
* Note that you should not change returned CharPosition objects because they are shared in an event
* dispatch.
*/
public class SelectionChangeEvent extends Event {
/**
* Unknown cause
*/
public final static int CAUSE_UNKNOWN = 0;
/**
* Selection change caused by text modifications
*/
public final static int CAUSE_TEXT_MODIFICATION = 1;
/**
* Set selection by handle
*/
public final static int CAUSE_SELECTION_HANDLE = 2;
/**
* Set selection by single tap
*/
public final static int CAUSE_TAP = 3;
/**
* Set selection because of {@link android.view.inputmethod.InputConnection#setSelection(int, int)}
*/
public final static int CAUSE_IME = 4;
/**
* Long press
*/
public final static int CAUSE_LONG_PRESS = 5;
/**
* Search text by {@link EditorSearcher}
*/
public final static int CAUSE_SEARCH = 6;
/**
* From keyboard or direct method invocation to change selection
*/
public final static int CAUSE_KEYBOARD_OR_CODE = 7;
/**
* From mouse
*/
public final static int CAUSE_MOUSE_INPUT = 8;
private final CharPosition left;
private final CharPosition right;
private final int cause;
public SelectionChangeEvent(@NonNull CodeEditor editor, int cause) {
super(editor);
var cursor = editor.getText().getCursor();
left = cursor.left();
right = cursor.right();
this.cause = cause;
}
/**
* Get cause of the change
*
* @see #CAUSE_UNKNOWN
* @see #CAUSE_TEXT_MODIFICATION
* @see #CAUSE_SELECTION_HANDLE
* @see #CAUSE_TAP
* @see #CAUSE_IME
* @see #CAUSE_LONG_PRESS
* @see #CAUSE_SEARCH
*/
public int getCause() {
return cause;
}
/**
* Get the left selection's position
*/
@NonNull
public CharPosition getLeft() {
return left;
}
/**
* Get the right selection's position
*/
@NonNull
public CharPosition getRight() {
return right;
}
/**
* Checks whether text is selected
*/
public boolean isSelected() {
return left.index != right.index;
}
}

View File

@ -0,0 +1,35 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
package io.github.rosemoe.sora.event
import io.github.rosemoe.sora.lang.styling.line.LineSideIcon
import io.github.rosemoe.sora.widget.CodeEditor
/**
* Called when side icon is clicked.
* If you would like to avoid [ClickEvent] to be triggered, you are expected to intercept editor by
* calling [SideIconClickEvent.intercept]
*/
class SideIconClickEvent(editor: CodeEditor, val clickedIcon: LineSideIcon) : Event(editor)

View File

@ -0,0 +1,91 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* Notify that snippet controller state is changed.
* <br/>
* If action is {@link #ACTION_START} and any event receiver intercepts editor, the snippet edit will
* stop before moving to any tab stop. And consequently, a {@link SnippetEvent} with action {@link #ACTION_STOP}
* will be broadcast immediately.
* <br/>
* There is at least one tab stop in the list when action is {@link #ACTION_START} or {@link #ACTION_SHIFT}.
* But no tab stop is left there when action is {@link #ACTION_STOP}. The last tab stop is where the selection
* will be placed when the snippet is finished normally.
*
* @author Rosemoe
*/
public class SnippetEvent extends Event {
/**
* Called before controller shifts to any tab stop
*/
public final static int ACTION_START = 1;
/**
* Called when controller shifted to a tab stop
*/
public final static int ACTION_SHIFT = 2;
/**
* Called when controller <strong>has exited</strong> a snippet
*/
public final static int ACTION_STOP = 3;
private final int action;
private final int currentTabStop;
private final int totalTabStop;
public SnippetEvent(@NonNull CodeEditor editor, int action, int currentTabStop, int totalTabStop) {
super(editor);
this.action = action;
this.currentTabStop = currentTabStop;
this.totalTabStop = totalTabStop;
}
/**
* @see #ACTION_START
* @see #ACTION_SHIFT
* @see #ACTION_STOP
*/
public int getAction() {
return action;
}
/**
* Get the current index of tab stops
*/
public int getCurrentTabStop() {
return currentTabStop;
}
/**
* Get the count of tab stops
*/
public int getTotalTabStop() {
return totalTabStop;
}
}

View File

@ -0,0 +1,66 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
import androidx.annotation.NonNull;
import java.lang.ref.WeakReference;
/**
* Receipt of {@link EventManager#subscribeEvent(Class, EventReceiver)}. You can unsubscribe the event outside
* the dispatch process from any thread by calling {@link SubscriptionReceipt#unsubscribe()}
*
* @author Rosemoe
*/
public class SubscriptionReceipt<R extends Event> {
private final Class<R> clazz;
private final WeakReference<EventReceiver<R>> receiver;
private final EventManager manager;
SubscriptionReceipt(@NonNull EventManager mgr, @NonNull Class<R> clazz, @NonNull EventReceiver<R> receiver) {
this.clazz = clazz;
this.receiver = new WeakReference<>(receiver);
this.manager = mgr;
}
/**
* Unsubscribe the event receiver.
* <p>
* Does nothing if the listener is already recycled or unsubscribed.
*/
public void unsubscribe() {
var receivers = manager.getReceivers(clazz);
receivers.lock.writeLock().lock();
try {
var target = receiver.get();
if (target != null) {
receivers.receivers.remove(target);
}
} finally {
receivers.lock.writeLock().unlock();
}
}
}

View File

@ -0,0 +1,59 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.event;
/**
* Instance for unsubscribing for a receiver.
* <p>
* Note that this instance can be reused during an event dispatch, so
* it is not a valid behavior to save the instance in event receivers.
* Always use the one given by {@link EventReceiver#onReceive(Event, Unsubscribe)}.
*/
public class Unsubscribe {
private boolean unsubscribeFlag = false;
/**
* Unsubscribe the event. And current receiver will not get event again.
* References to the receiver are also removed.
*/
public void unsubscribe() {
unsubscribeFlag = true;
}
/**
* Checks whether unsubscribe flag is set
*/
public boolean isUnsubscribed() {
return unsubscribeFlag;
}
/**
* Reset the flag
*/
public void reset() {
unsubscribeFlag = false;
}
}

View File

@ -0,0 +1,37 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
package io.github.rosemoe.sora.event
fun ResultedEvent<Boolean>.getResultBoolean(): Boolean = if (isResultSet) {
result!!
} else {
false
}
inline fun <reified T : Event> EventManager.subscribeEvent(receiver: EventReceiver<T>) =
subscribeEvent(T::class.java, receiver)
inline fun <reified T : Event> EventManager.subscribeAlways(receiver: EventManager.NoUnsubscribeReceiver<T>) =
subscribeAlways(T::class.java, receiver)

View File

@ -0,0 +1,123 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
package io.github.rosemoe.sora.event
import android.os.Bundle
import android.view.ContextMenu
import android.view.MotionEvent
import android.view.inputmethod.EditorInfo
import io.github.rosemoe.sora.lang.Language
import io.github.rosemoe.sora.lang.styling.Span
import io.github.rosemoe.sora.text.CharPosition
import io.github.rosemoe.sora.text.TextRange
import io.github.rosemoe.sora.widget.CodeEditor
/**
* Editor [Language] changed
*/
class EditorLanguageChangeEvent(editor: CodeEditor, val newLanguage: Language) : Event(editor)
/**
* This event is triggered after format result is available and is applied to the editor
*/
class EditorFormatEvent(editor: CodeEditor, val isSuccess: Boolean) : Event(editor)
/**
* Called when the editor is going to be released. That's when [CodeEditor.release] is
* called. You may subscribe this event to release resources when you are holding editor-specific
* resources.
*
* Note that this event will only be triggered once on a certain editor.
*
* @author Rosemoe
*/
class EditorReleaseEvent(editor: CodeEditor) : Event(editor)
/**
* Event for ime private command execution. When [android.view.inputmethod.InputConnection.performPrivateCommand]
* is called, this event will be triggered.
* You can subscribe to this event in order to interact with your own inputmethod and thus implement
* specific features between this editor and your IME app.
*
* @see android.view.inputmethod.InputConnection.performPrivateCommand
* @author Rosemoe
*/
class ImePrivateCommandEvent(editor: CodeEditor, val action: String, val data: Bundle?) :
Event(editor)
/**
* This event is triggered when editor is building its [EditorInfo] object for IPC.
* You can customize the info to add extra information for the ime, due to implement specific
* features between this editor and your own IME. But note that [EditorInfo.inputType], [EditorInfo.initialSelStart],
* [EditorInfo.initialSelEnd] and [EditorInfo.initialCapsMode] should not be modified. They are
* managed by editor self.
*
* @author Rosemoe
*/
class BuildEditorInfoEvent(editor: CodeEditor, val editorInfo: EditorInfo) : Event(editor)
/**
* Triggered when focus state is changed
*/
class EditorFocusChangeEvent(editor: CodeEditor, val isGainFocus: Boolean) : Event(editor)
/**
* Trigger when the editor is attached to window/detached from window
*/
class EditorAttachStateChangeEvent(editor: CodeEditor, val isAttachedToWindow: Boolean) :
Event(editor)
/**
* Trigger when mouse right-clicked the editor
*/
class ContextClickEvent(
editor: CodeEditor,
position: CharPosition,
event: MotionEvent,
span: Span?,
spanRange: TextRange?
) : EditorMotionEvent(editor, position, event, span, spanRange)
/**
* Trigger when mouse hover updates
*/
class HoverEvent(
editor: CodeEditor,
position: CharPosition,
event: MotionEvent,
span: Span?,
spanRange: TextRange?
) : EditorMotionEvent(editor, position, event, span, spanRange)
/**
* Trigger when the editor needs to create context menu
* @property menu [ContextMenu] for adding menu items
* @property position Target text position of the menu
*/
class CreateContextMenuEvent(
editor: CodeEditor,
val menu: ContextMenu,
val position: CharPosition
) : Event(editor)

View File

@ -0,0 +1,75 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.graphics;
import android.graphics.Matrix;
import android.graphics.Path;
import android.graphics.RectF;
import androidx.annotation.NonNull;
/**
* Helper class for building a bubble rect
*
* @author Rosemoe
*/
public class BubbleHelper {
private final static Matrix tempMatrix = new Matrix();
/**
* Build a bubble into the given Path object. Old content in given Path is cleared.
* @param path target Path object
* @param bounds The bounds for the bubble
*/
public static void buildBubblePath(@NonNull Path path, @NonNull RectF bounds) {
path.reset();
float width = bounds.width();
float height = bounds.height();
float r = height / 2;
float sqrt2 = (float) Math.sqrt(2);
// Ensure we are convex.
width = Math.max(r + sqrt2 * r, width);
pathArcTo(path, r, r, r, 90, 180);
float o1X = width - sqrt2 * r;
pathArcTo(path, o1X, r, r, -90, 45f);
float r2 = r / 5;
float o2X = width - sqrt2 * r2;
pathArcTo(path, o2X, r, r2, -45, 90);
pathArcTo(path, o1X, r, r, 45f, 45f);
path.close();
tempMatrix.reset();
tempMatrix.postTranslate(bounds.left, bounds.top);
path.transform(tempMatrix);
}
private static void pathArcTo(@NonNull Path path, float centerX, float centerY, float radius,
float startAngle, float sweepAngle) {
path.arcTo(centerX - radius, centerY - radius, centerX + radius, centerY + radius,
startAngle, sweepAngle, false);
}
}

View File

@ -0,0 +1,59 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.graphics;
import android.graphics.Canvas;
import android.graphics.Paint;
public class BufferedDrawPoints {
private int pointCount;
private float[] points;
public BufferedDrawPoints() {
points = new float[128];
}
public void drawPoint(float cx, float cy) {
// Check buffer size and grow
if (points.length < (pointCount + 1) * 2) {
float[] newBuffer = new float[points.length << 1];
System.arraycopy(points, 0, newBuffer, 0, pointCount * 2);
points = newBuffer;
}
points[pointCount * 2] = cx;
points[pointCount * 2 + 1] = cy;
pointCount++;
}
public void commitPoints(Canvas canvas, Paint paint) {
if (pointCount == 0) {
return;
}
canvas.drawPoints(points, 0, pointCount * 2, paint);
pointCount = 0;
}
}

View File

@ -0,0 +1,60 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.graphics;
import io.github.rosemoe.sora.util.IntPair;
/**
* Utility for character position description, which is a packed (textOffset,pixelWidthOrOffset) value.
*
* @author Rosemoe
*/
public class CharPosDesc {
private CharPosDesc() {
}
/**
* Make a new character position description
*/
public static long make(int textOffset, float pixelWidthOrOffset) {
return IntPair.pack(textOffset, Float.floatToRawIntBits(pixelWidthOrOffset));
}
/**
* Get character offset in text
*/
public static int getTextOffset(long packedValue) {
return IntPair.getFirst(packedValue);
}
/**
* Get character width or offset in pixel
*/
public static float getPixelWidthOrOffset(long packedValue) {
return Float.intBitsToFloat(IntPair.getSecond(packedValue));
}
}

View File

@ -0,0 +1,51 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.graphics;
import io.github.rosemoe.sora.util.MyCharacter;
public class GraphicCharacter {
public static boolean isCombiningCharacter(int codePoint) {
return MyCharacter.isVariationSelector(codePoint) || MyCharacter.isFitzpatrick(codePoint)
|| MyCharacter.isZWJ(codePoint) || MyCharacter.isZWNJ(codePoint) ||
MyCharacter.couldBeEmoji(codePoint)
|| (Character.charCount(codePoint) == 1 && Character.isSurrogate((char) codePoint))
|| isASCIICombiningSymbol(codePoint);
}
public static boolean isASCIICombiningSymbol(int codePoint) {
return codePoint == '.' || codePoint == '/' || codePoint == '!' || codePoint == '=' ||
codePoint == '<' || codePoint == '>' || codePoint == '-';/*!(codePoint >= '0' && codePoint <= '9')
&& !(codePoint >= 'a' && codePoint <= 'z')
&& !(codePoint >= 'A' && codePoint <= 'Z');*/
}
public static boolean couldBeEmojiPart(int codePoint) {
return MyCharacter.isVariationSelector(codePoint) || MyCharacter.isFitzpatrick(codePoint)
|| MyCharacter.isZWJ(codePoint) || MyCharacter.isZWNJ(codePoint) ||
MyCharacter.couldBeEmoji(codePoint);
}
}

View File

@ -0,0 +1,376 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.graphics;
import static io.github.rosemoe.sora.lang.styling.TextStyle.isBold;
import static io.github.rosemoe.sora.lang.styling.TextStyle.isItalics;
import android.annotation.SuppressLint;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import io.github.rosemoe.sora.lang.styling.Span;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.text.ContentLine;
import io.github.rosemoe.sora.text.bidi.Directions;
import io.github.rosemoe.sora.text.bidi.TextBidi;
import io.github.rosemoe.sora.util.IntPair;
import io.github.rosemoe.sora.widget.rendering.RenderingConstants;
/**
* Manages graphical(actually measuring) operations of a text row
*/
public class GraphicTextRow {
private final static GraphicTextRow[] sCached = new GraphicTextRow[5];
private Paint paint;
private ContentLine text;
private Directions directions;
private int textStart;
private int textEnd;
private int tabWidth;
private List<Span> spans;
private boolean useCache = true;
private List<Integer> softBreaks;
private boolean quickMeasureMode;
private final Directions tmpDirections = new Directions(new long[]{IntPair.pack(0, 0)}, 0);
private GraphicTextRow() {
}
public static GraphicTextRow obtain(boolean quickMeasure) {
GraphicTextRow st;
synchronized (sCached) {
for (int i = sCached.length; --i >= 0; ) {
if (sCached[i] != null) {
st = sCached[i];
sCached[i] = null;
st.quickMeasureMode = quickMeasure;
return st;
}
}
}
st = new GraphicTextRow();
st.quickMeasureMode = quickMeasure;
return st;
}
public static void recycle(GraphicTextRow st) {
st.text = null;
st.spans = null;
st.paint = null;
st.textStart = st.textEnd = st.tabWidth = 0;
st.useCache = true;
st.softBreaks = null;
st.directions = null;
synchronized (sCached) {
for (int i = 0; i < sCached.length; ++i) {
if (sCached[i] == null) {
sCached[i] = st;
break;
}
}
}
}
public void recycle() {
recycle(this);
}
public void set(@NonNull Content content, int line, int start, int end, int tabWidth, @Nullable List<Span> spans, @NonNull Paint paint) {
this.paint = paint;
text = content.getLine(line);
directions = content.getLineDirections(line);
this.tabWidth = tabWidth;
textStart = start;
textEnd = end;
this.spans = spans;
tmpDirections.setLength(text.length());
}
public void set(@NonNull ContentLine text, @Nullable Directions dirs, int start, int end, int tabWidth, @Nullable List<Span> spans, @NonNull Paint paint) {
this.paint = paint;
this.text = text;
directions = dirs;
this.tabWidth = tabWidth;
textStart = start;
textEnd = end;
this.spans = spans;
tmpDirections.setLength(this.text.length());
}
public void setSoftBreaks(@Nullable List<Integer> softBreaks) {
this.softBreaks = softBreaks;
}
public void disableCache() {
useCache = false;
}
/**
* Build measure cache for the text
*/
public void buildMeasureCache() {
if (text.widthCache == null || text.widthCache.length < textEnd + 4) {
text.widthCache = new float[Math.max(90, text.length() + 16)];
}
measureTextInternal(textStart, textEnd, text.widthCache);
// Generate prefix sum
var cache = text.widthCache;
var pending = cache[0];
cache[0] = 0f;
for (int i = 1; i <= textEnd; i++) {
var tmp = cache[i];
cache[i] = cache[i - 1] + pending;
pending = tmp;
}
}
/**
* From {@code start} to measure characters, until measured width add next char's width is bigger
* than {@code advance}.
* <p>
* Note that the result array should not be stored.
*
* @return text offset and measured width
* @see CharPosDesc Character position description
*/
public long findOffsetByAdvance(int start, float advance) {
if (text.widthCache != null && useCache) {
var cache = text.widthCache;
var end = textEnd;
int left = start, right = end;
var base = cache[start];
while (left <= right) {
var mid = (left + right) / 2;
if (mid < start || mid >= end) {
left = mid;
break;
}
var value = cache[mid] - base;
if (value > advance) {
right = mid - 1;
} else if (value < advance) {
left = mid + 1;
} else {
left = mid;
break;
}
}
if (cache[left] - base > advance) {
left--;
}
left = Math.max(start, Math.min(end, left));
return CharPosDesc.make(left, cache[left] - base);
}
var regionItr = new TextRegionIterator(textEnd, spans, softBreaks);
float currentPosition = 0f;
// Find in each region
var lastStyle = 0L;
var chars = text.value;
float tabAdvance = paint.getSpaceWidth() * tabWidth;
int offset = start;
var first = true;
while (regionItr.hasNextRegion() && currentPosition < advance) {
if (first) {
regionItr.requireStartOffset(start);
first = false;
} else {
regionItr.nextRegion();
}
var regionStart = regionItr.getStartIndex();
var regionEnd = regionItr.getEndIndex();
regionEnd = Math.min(textEnd, regionEnd);
var style = regionItr.getSpan().getStyleBits();
if (style != lastStyle) {
if (isBold(style) != isBold(lastStyle)) {
paint.setFakeBoldText(isBold(style));
}
if (isItalics(style) != isItalics(lastStyle)) {
paint.setTextSkewX(isItalics(style) ? RenderingConstants.TEXT_SKEW_X : 0f);
}
lastStyle = style;
}
// Find in subregion
int res = -1;
{
int lastStart = regionStart;
for (int i = regionStart; i < regionEnd; i++) {
if (chars[i] == '\t') {
// Here is a tab
// Try to find advance
if (lastStart != i) {
int idx = paint.findOffsetByRunAdvance(text, lastStart, i, advance - currentPosition, useCache, quickMeasureMode);
currentPosition += paint.measureTextRunAdvance(chars, lastStart, idx, regionStart, regionEnd, quickMeasureMode);
if (idx < i) {
res = idx;
break;
} else {
if (currentPosition + tabAdvance > advance) {
res = i;
break;
} else {
currentPosition += tabAdvance;
}
}
} else {
if (currentPosition + tabAdvance > advance) {
res = i;
break;
} else {
currentPosition += tabAdvance;
}
}
lastStart = i + 1;
}
}
if (res == -1) {
int idx = paint.findOffsetByRunAdvance(text, lastStart, regionEnd, advance - currentPosition, useCache, quickMeasureMode);
currentPosition += measureText(lastStart, idx);
res = idx;
}
}
offset = res;
if (res < regionEnd) {
break;
}
if (regionEnd == textEnd) {
break;
}
}
if (lastStyle != 0L) {
paint.setFakeBoldText(false);
paint.setTextSkewX(0f);
}
return CharPosDesc.make(offset, currentPosition);
}
public float measureText(int start, int end) {
if (start < 0) {
throw new IndexOutOfBoundsException("negative start position");
}
if (start >= end) {
if (start != end)
Log.w("GraphicTextRow", "start > end. if this is caused by editor, please provide feedback", new Throwable());
return 0f;
}
var cache = text.widthCache;
if (cache != null && useCache && end < cache.length) {
return cache[end] - cache[start];
}
return measureTextInternal(start, end, null);
}
private float measureTextInternal(int start, int end, float[] widths) {
// Backup values
final var originalBold = paint.isFakeBoldText();
final var originalSkew = paint.getTextSkewX();
start = Math.max(start, textStart);
end = Math.min(end, textEnd);
var regionItr = new TextRegionIterator(end, spans, softBreaks);
float width = 0f;
// Measure for each region
var lastStyle = 0L;
var first = true;
while (regionItr.hasNextRegion()) {
if (first) {
regionItr.requireStartOffset(start);
first = false;
} else {
regionItr.nextRegion();
}
var regionStart = regionItr.getStartIndex();
var regionEnd = regionItr.getEndIndex();
regionEnd = Math.min(end, regionEnd);
if (regionStart > regionEnd || (regionStart == regionEnd && regionEnd >= end)) {
break;
}
var style = regionItr.getSpan().getStyleBits();
if (style != lastStyle) {
if (isBold(style) != isBold(lastStyle)) {
paint.setFakeBoldText(isBold(style));
}
if (isItalics(style) != isItalics(lastStyle)) {
paint.setTextSkewX(isItalics(style) ? RenderingConstants.TEXT_SKEW_X : 0f);
}
lastStyle = style;
}
int contextStart = Math.min(regionStart, regionItr.getSpanStart());
int contextEnd = Math.max(regionEnd, regionItr.getSpanEnd());
contextEnd = Math.min(textEnd, contextEnd);
width += measureTextInner(regionStart, regionEnd, contextStart, contextEnd, widths);
if (regionEnd >= end) {
break;
}
}
paint.setFakeBoldText(originalBold);
paint.setTextSkewX(originalSkew);
return width;
}
@SuppressLint("NewApi")
private float measureTextInner(int start, int end, int ctxStart, int ctxEnd, float[] widths) {
if (start >= end) {
return 0f;
}
var dirs = directions == null ?
(text.mayNeedBidi() ? TextBidi.getDirections(text) : tmpDirections)
: directions;
float width = 0;
for (int i = 0; i < dirs.getRunCount(); i++) {
int start1 = Math.max(start, dirs.getRunStart(i));
int end1 = Math.min(end, dirs.getRunEnd(i));
if (end1 > start1) {
// Can be called directly
width += paint.myGetTextRunAdvances(text.value, start1, end1 - start1, ctxStart, ctxEnd - ctxStart, dirs.isRunRtl(i), widths, widths == null ? 0 : start1, quickMeasureMode);
}
if (dirs.getRunStart(i) >= end) {
break;
}
}
float tabWidth = paint.getSpaceWidth() * this.tabWidth;
int tabCount = 0;
for (int i = start; i < end; i++) {
if (text.charAt(i) == '\t') {
tabCount++;
if (widths != null) {
widths[i] = tabWidth;
}
}
}
float extraWidth = tabCount == 0 ? 0 : tabWidth - paint.measureText("\t");
return width + extraWidth * tabCount;
}
}

View File

@ -0,0 +1,229 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.graphics;
import android.annotation.SuppressLint;
import android.graphics.Typeface;
import android.os.Build;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.text.ContentLine;
import io.github.rosemoe.sora.text.FunctionCharacters;
public class Paint extends android.graphics.Paint {
private float spaceWidth;
private float tabWidth;
private boolean renderFunctionCharacters;
private SingleCharacterWidths widths;
public Paint(boolean renderFunctionCharacters) {
super();
this.renderFunctionCharacters = renderFunctionCharacters;
spaceWidth = measureText(" ");
tabWidth = measureText("\t");
}
public void setRenderFunctionCharacters(boolean renderFunctionCharacters) {
this.renderFunctionCharacters = renderFunctionCharacters;
if (widths != null) {
widths.clearCache();
}
}
public boolean isRenderFunctionCharacters() {
return renderFunctionCharacters;
}
private void ensureCacheObject() {
if (widths == null) {
widths = new SingleCharacterWidths(1);
}
}
public void onAttributeUpdate() {
spaceWidth = measureText(" ");
tabWidth = measureText("\t");
if (widths != null)
widths.clearCache();
}
public float getSpaceWidth() {
return spaceWidth;
}
public void setTypefaceWrapped(Typeface typeface) {
super.setTypeface(typeface);
onAttributeUpdate();
}
public void setTextSizeWrapped(float textSize) {
super.setTextSize(textSize);
onAttributeUpdate();
}
public void setFontFeatureSettingsWrapped(String settings) {
super.setFontFeatureSettings(settings);
onAttributeUpdate();
}
@Override
public void setLetterSpacing(float letterSpacing) {
super.setLetterSpacing(letterSpacing);
onAttributeUpdate();
}
@SuppressLint("NewApi")
public float myGetTextRunAdvances(@NonNull char[] chars, int index, int count, int contextIndex, int contextCount, boolean isRtl, @Nullable float[] advances, int advancesIndex, boolean fast) {
if (fast) {
ensureCacheObject();
var width = 0f;
for (int i = 0; i < count; i++) {
char ch = chars[i + index];
float charWidth;
if (Character.isHighSurrogate(ch) && i + 1 < count && Character.isLowSurrogate(chars[index + i + 1])) {
charWidth = widths.measureCodePoint(Character.toCodePoint(ch, chars[index + i + 1]), this);
if (advances != null) {
advances[advancesIndex + i] = charWidth;
advances[advancesIndex + i + 1] = 0f;
}
i++;
} else if (renderFunctionCharacters && FunctionCharacters.isEditorFunctionChar(ch)) {
charWidth = widths.measureText(FunctionCharacters.getNameForFunctionCharacter(ch), this);
if (advances != null) {
advances[advancesIndex + i] = charWidth;
}
} else {
charWidth = (ch == '\t') ? tabWidth : widths.measureChar(ch, this);
if (advances != null) {
advances[advancesIndex + i] = charWidth;
}
}
width += charWidth;
}
return width;
} else {
float advance = getTextRunAdvances(chars, index, count, contextIndex, contextCount, isRtl, advances, advancesIndex);
if (renderFunctionCharacters) {
for (int i = 0;i < count;i++) {
char ch = chars[index + i];
if (FunctionCharacters.isEditorFunctionChar(ch)) {
float width = measureText(FunctionCharacters.getNameForFunctionCharacter(ch));
if (advances != null) {
advance -= advances[advancesIndex + i];
advances[advancesIndex + i] = width;
} else {
advance -= measureText(Character.toString(ch));
}
advance += width;
}
}
}
return advance;
}
}
/**
* Get the advance of text with the context positions related to shaping the characters
*/
public float measureTextRunAdvance(char[] text, int start, int end, int contextStart, int contextEnd, boolean fast) {
return myGetTextRunAdvances(text, start, end - start, contextStart, contextEnd - contextStart, false, null, 0, fast);
}
/**
* Find offset for a certain advance returned by {@link #measureTextRunAdvance(char[], int, int, int, int, boolean)}
*/
public int findOffsetByRunAdvance(ContentLine text, int start, int end, float advance, boolean useCache, boolean fast) {
if (text.widthCache != null && useCache) {
var cache = text.widthCache;
var offset = start;
var currAdvance = 0f;
for (; offset < end && currAdvance < advance; offset++) {
currAdvance += cache[offset + 1] - cache[offset];
}
if (currAdvance > advance) {
offset--;
}
return Math.max(offset, start);
}
if (fast) {
ensureCacheObject();
var width = 0f;
for (int i = start; i < end; i++) {
char ch = text.value[i];
float charWidth;
int j = i;
if (Character.isHighSurrogate(ch) && i + 1 < end && Character.isLowSurrogate(text.value[i + 1])) {
charWidth = widths.measureCodePoint(Character.toCodePoint(ch, text.value[i + 1]), this);
i++;
} else if (renderFunctionCharacters && FunctionCharacters.isEditorFunctionChar(ch)) {
charWidth = widths.measureText(FunctionCharacters.getNameForFunctionCharacter(ch), this);
} else {
charWidth = (ch == '\t') ? tabWidth : widths.measureChar(ch, this);
}
width += charWidth;
if (width > advance) {
return Math.max(start, j - 1);
}
}
return end;
}
if (renderFunctionCharacters) {
int lastEnd = start;
float current = 0f;
for (int i = start;i < end;i++) {
char ch = text.value[i];
if (FunctionCharacters.isEditorFunctionChar(ch)) {
int result = lastEnd == i ? i : breakTextImpl(text, lastEnd, i, advance - current);
if (result < i) {
return result;
}
current += measureTextRunAdvance(text.value, lastEnd, i, lastEnd, i, false);
current += measureText(FunctionCharacters.getNameForFunctionCharacter(ch));
if (current >= advance) {
return i;
}
lastEnd = i + 1;
}
}
if (lastEnd < end) {
return breakTextImpl(text, lastEnd, end, advance - current);
}
return end;
} else {
return breakTextImpl(text, start, end, advance);
}
}
private int breakTextImpl(ContentLine text, int start, int end, float advance) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
return getOffsetForAdvance(text.value, start, end, start, end, false, advance);
} else {
return start + breakText(text.value, start, end - start, advance, null);
}
}
}

View File

@ -0,0 +1,38 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.graphics;
import android.graphics.RectF;
public class RectUtils {
public static boolean contains(RectF rect, float x, float y, float extraXSpace) {
return (x >= rect.left - extraXSpace && x <= rect.right + extraXSpace && y >= rect.top && y <= rect.bottom);
}
public static boolean almostContains(RectF rect, float x, float y, float extraSpace) {
return (x >= rect.left - extraSpace && x <= rect.right + extraSpace && y >= rect.top - extraSpace && y <= rect.bottom + extraSpace);
}
}

View File

@ -0,0 +1,177 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.graphics;
import android.util.SparseArray;
import java.util.Arrays;
import io.github.rosemoe.sora.text.FunctionCharacters;
public class SingleCharacterWidths {
public final float[] widths;
public final SparseArray<Float> codePointWidths;
public final char[] buffer;
private final float[] cache;
private final int tabWidth;
private boolean handleFunctionCharacters;
public SingleCharacterWidths(int tabWidth) {
cache = new float[65536];
buffer = new char[10];
widths = new float[10];
codePointWidths = new SparseArray<>();
this.tabWidth = tabWidth;
}
public void setHandleFunctionCharacters(boolean handleFunctionCharacters) {
this.handleFunctionCharacters = handleFunctionCharacters;
}
public boolean isHandleFunctionCharacters() {
return handleFunctionCharacters;
}
public static boolean isEmoji(char ch) {
return ch == 0xd83c || ch == 0xd83d || ch == 0xd83e;
}
/**
* Clear caches of font
*/
public void clearCache() {
Arrays.fill(cache, 0);
codePointWidths.clear();
}
/**
* Measure a single character
*/
public float measureChar(char ch, Paint p) {
var rate = 1;
if (ch == '\t') {
ch = ' ';
rate = tabWidth;
}
float width = cache[ch];
if (width == 0) {
buffer[0] = ch;
width = p.measureText(buffer, 0, 1);
cache[ch] = width;
}
return width * rate;
}
/**
* Measure a single character
*/
public float measureCodePoint(int cp, Paint p) {
if (cp <= 65535) {
return measureChar((char) cp, p);
}
var width = codePointWidths.get(cp);
if (width == null) {
var count = Character.toChars(cp, buffer, 0);
width = p.measureText(buffer, 0, count);
codePointWidths.put(cp, width);
}
return width;
}
/*
* Measure text
*/
public float measureText(char[] chars, int start, int end, Paint p) {
float width = 0f;
for (int i = start; i < end; i++) {
char ch = chars[i];
if (isEmoji(ch)) {
if (i + 4 <= end) {
p.getTextWidths(chars, i, 4, widths);
if (widths[0] > 0 && widths[1] == 0 && widths[2] == 0 && widths[3] == 0) {
i += 3;
width += widths[0];
continue;
}
}
int commitEnd = Math.min(end, i + 2);
int len = commitEnd - i;
if (len >= 0) {
System.arraycopy(chars, i, buffer, 0, len);
}
width += p.measureText(buffer, 0, len);
i += len - 1;
} else if(isHandleFunctionCharacters() && FunctionCharacters.isEditorFunctionChar(ch)) {
var name = FunctionCharacters.getNameForFunctionCharacter(ch);
for (int j = 0;j < name.length();j++) {
width += measureChar(name.charAt(j), p);
}
} else {
width += measureChar(ch, p);
}
}
return width;
}
public float measureText(CharSequence str, Paint p) {
return measureText(str, 0, str.length(), p);
}
/**
* Measure text
*/
public float measureText(CharSequence str, int start, int end, Paint p) {
float width = 0f;
for (int i = start; i < end; i++) {
char ch = str.charAt(i);
if (isEmoji(ch)) {
if (i + 4 <= end) {
p.getTextWidths(str, i, i + 4, widths);
if (widths[0] > 0 && widths[1] == 0 && widths[2] == 0 && widths[3] == 0) {
i += 3;
width += widths[0];
continue;
}
}
int commitEnd = Math.min(end, i + 2);
int len = commitEnd - i;
for (int j = 0; j < len; j++) {
buffer[j] = str.charAt(i + j);
}
width += p.measureText(buffer, 0, len);
i += len - 1;
} else if(isHandleFunctionCharacters() && FunctionCharacters.isEditorFunctionChar(ch)) {
var name = FunctionCharacters.getNameForFunctionCharacter(ch);
for (int j = 0;j < name.length();j++) {
width += measureChar(name.charAt(j), p);
}
} else {
width += measureChar(ch, p);
}
}
return width;
}
}

View File

@ -0,0 +1,131 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.graphics;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import io.github.rosemoe.sora.lang.styling.Span;
import io.github.rosemoe.sora.lang.styling.SpanFactory;
import io.github.rosemoe.sora.util.RegionIterator;
import io.github.rosemoe.sora.widget.schemes.EditorColorScheme;
/**
* Helper class for {@link GraphicTextRow} to iterate text regions
*
* @author Rosemoe
*/
class TextRegionIterator extends RegionIterator {
private final List<Span> spans;
public TextRegionIterator(int length, @NonNull List<Span> spans, @Nullable List<Integer> softBreaks) {
super(length, new SpansPoints(spans), new SoftBreaksPoints(softBreaks));
this.spans = spans;
}
/**
* Move to next region, until the end index of that region is bigger than {@code index}.
* And set region start index to {@code index}.
*/
public void requireStartOffset(int index) {
if (index > getMax()) {
throw new IllegalArgumentException();
}
if (getStartIndex() != 0) {
throw new IllegalStateException();
}
do {
nextRegion();
} while (getEndIndex() <= index && hasNextRegion());
startIndex = index;
}
/**
* Get current {@link Span} for current region
*/
public Span getSpan() {
var idx = getRegionSourcePointer(0) - 1;
if (idx < 0) {
return SpanFactory.obtain(0, EditorColorScheme.TEXT_NORMAL);
}
return spans.get(idx);
}
/**
* Get start index of current {@link Span}
*/
public int getSpanStart() {
return getPointerValue(0, getRegionSourcePointer(0) - 1);
}
/**
* Get end index of current {@link Span}
*/
public int getSpanEnd() {
return getPointerValue(0, getRegionSourcePointer(0));
}
private static class SpansPoints implements RegionProvider {
private final List<Span> spans;
public SpansPoints(List<Span> spans) {
this.spans = spans;
}
@Override
public int getPointCount() {
return spans == null ? 0 : spans.size();
}
@Override
public int getPointAt(int index) {
return spans.get(index).getColumn();
}
}
private static class SoftBreaksPoints implements RegionProvider {
private final List<Integer> points;
public SoftBreaksPoints(List<Integer> points) {
this.points = points;
}
@Override
public int getPointCount() {
return points == null ? 0 : points.size();
}
@Override
public int getPointAt(int index) {
return points.get(index);
}
}
}

View File

@ -0,0 +1,151 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.lang.analysis.AnalyzeManager;
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
import io.github.rosemoe.sora.lang.format.Formatter;
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler;
import io.github.rosemoe.sora.lang.util.BaseAnalyzeManager;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.text.ContentReference;
import io.github.rosemoe.sora.text.TextRange;
import io.github.rosemoe.sora.widget.SymbolPairMatch;
/**
* Empty language
*
* @author Rosemoe
*/
public class EmptyLanguage implements Language {
public final static SymbolPairMatch EMPTY_SYMBOL_PAIRS = new SymbolPairMatch();
@NonNull
@Override
public Formatter getFormatter() {
return EmptyFormatter.INSTANCE;
}
@Override
public SymbolPairMatch getSymbolPairs() {
return EMPTY_SYMBOL_PAIRS;
}
@Override
public void requireAutoComplete(@NonNull ContentReference content, @NonNull CharPosition position, @NonNull CompletionPublisher publisher, @NonNull Bundle extraArguments) {
}
@Override
public int getInterruptionLevel() {
return INTERRUPTION_LEVEL_STRONG;
}
@Override
public NewlineHandler[] getNewlineHandlers() {
return new NewlineHandler[0];
}
@NonNull
@Override
public AnalyzeManager getAnalyzeManager() {
return EmptyAnalyzeManager.INSTANCE;
}
@Override
public int getIndentAdvance(@NonNull ContentReference content, int line, int column) {
return 0;
}
@Override
public void destroy() {
}
@Override
public boolean useTab() {
return false;
}
public static class EmptyFormatter implements Formatter {
public final static EmptyFormatter INSTANCE = new EmptyFormatter();
@Override
public void format(@NonNull Content text, @NonNull TextRange cursorRange) {
}
@Override
public void formatRegion(@NonNull Content text, @NonNull TextRange rangeToFormat, @NonNull TextRange cursorRange) {
}
@Override
public void setReceiver(@Nullable FormatResultReceiver receiver) {
}
@Override
public boolean isRunning() {
return false;
}
@Override
public void destroy() {
}
}
public static class EmptyAnalyzeManager extends BaseAnalyzeManager {
public final static EmptyAnalyzeManager INSTANCE = new EmptyAnalyzeManager();
@Override
public void insert(@NonNull CharPosition start, @NonNull CharPosition end, @NonNull CharSequence insertedContent) {
}
@Override
public void delete(@NonNull CharPosition start, @NonNull CharPosition end, @NonNull CharSequence deletedContent) {
}
@Override
public void rerun() {
}
}
}

View File

@ -0,0 +1,195 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.UiThread;
import androidx.annotation.WorkerThread;
import io.github.rosemoe.sora.lang.analysis.AnalyzeManager;
import io.github.rosemoe.sora.lang.completion.CompletionCancelledException;
import io.github.rosemoe.sora.lang.completion.CompletionHelper;
import io.github.rosemoe.sora.lang.completion.CompletionPublisher;
import io.github.rosemoe.sora.lang.format.Formatter;
import io.github.rosemoe.sora.lang.smartEnter.NewlineHandler;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.ContentReference;
import io.github.rosemoe.sora.widget.CodeEditor;
import io.github.rosemoe.sora.widget.SymbolPairMatch;
/**
* Language for editor
* <p>
* A Language helps editor to highlight text and provide auto-completion.
* Implement this interface when you want to add new language support for editor.
* <p>
* <strong>NOTE:</strong> A language must not be single instance.
* One language instance should always serve for only one editor.
* It means that you should not give one language object to other editor instances
* after it has been applied to one editor.
*
* @author Rosemoe
*/
public interface Language {
/**
* Set the thread's interrupted flag by calling {@link Thread#interrupt()}.
* <p>
* Throw {@link CompletionCancelledException} exceptions
* from {@link ContentReference} and {@link CompletionPublisher}.
* <p>
* Set thread's flag for abortion.
*/
int INTERRUPTION_LEVEL_STRONG = 0;
/**
* Throw {@link CompletionCancelledException} exceptions
* from {@link ContentReference} and {@link CompletionPublisher}.
* <p>
* Set thread's flag for abortion.
*/
int INTERRUPTION_LEVEL_SLIGHT = 1;
/**
* Throw {@link CompletionCancelledException} exceptions
* from {@link ContentReference}
* <p>
* Set thread's flag for abortion.
*/
int INTERRUPTION_LEVEL_NONE = 2;
/**
* Get {@link AnalyzeManager} of the language.
* This is called from time to time by the editor. Cache your instance please.
*/
@NonNull
AnalyzeManager getAnalyzeManager();
/**
* Get the interruption level for auto-completion.
*
* @see #INTERRUPTION_LEVEL_STRONG
* @see #INTERRUPTION_LEVEL_SLIGHT
* @see #INTERRUPTION_LEVEL_NONE
*/
int getInterruptionLevel();
/**
* Request to auto-complete the code at the given {@code position}.
* This is called in a worker thread other than UI thread.
*
* @param content Read-only reference of content
* @param position The position for auto-complete
* @param publisher The publisher used to update items
* @param extraArguments Arguments set by {@link CodeEditor#setText(CharSequence, Bundle)}
* @throws io.github.rosemoe.sora.lang.completion.CompletionCancelledException This thread can be abandoned
* by the editor framework because the auto-completion items of
* this invocation are no longer needed by the user. This can either be thrown
* by {@link ContentReference} or {@link CompletionPublisher}.
* How the exceptions will be thrown is according to
* your settings: {@link #getInterruptionLevel()}
* @see ContentReference
* @see CompletionPublisher
* @see #getInterruptionLevel()
* @see CompletionHelper#checkCancelled()
*/
@WorkerThread
void requireAutoComplete(@NonNull ContentReference content, @NonNull CharPosition position,
@NonNull CompletionPublisher publisher,
@NonNull Bundle extraArguments) throws CompletionCancelledException;
/**
* Get delta indent spaces count
*
* @param content Content of given line
* @param line 0-indexed line number
* @param column Column on the given line, where a line separator is inserted
* @return Delta count of indent spaces. It can be a negative/positive number or zero.
*/
@UiThread
int getIndentAdvance(@NonNull ContentReference content, int line, int column);
/**
* Use tab to format
*/
@UiThread
boolean useTab();
/**
* Get the code formatter for the current language.
* The formatter is expected to be the same one during the lifecycle of a language instance.
*
* @return The code formatter for the current language.
*/
@UiThread
@NonNull
Formatter getFormatter();
/**
* Returns language specified symbol pairs.
* The method is called only once when the language is applied.
*/
@UiThread
SymbolPairMatch getSymbolPairs();
/**
* Get newline handlers of this language.
* This method is called each time the user presses ENTER key.
* <p>
* Pay attention to the performance as this method is called frequently
*
* @return NewlineHandlers , maybe null
*/
@UiThread
@Nullable
NewlineHandler[] getNewlineHandlers();
/**
* Get newline handlers of this language.
* This method is called each time the user types a single character (or a single code point)
* and some text is currently selected.
* <p>
* Pay attention to the performance as this method is called frequently
*
* @return QuickQuoteHandler, maybe null
*/
@UiThread
@Nullable
default QuickQuoteHandler getQuickQuoteHandler() {
return null;
}
/**
* Destroy this {@link Language} object.
* <p>
* When called, you should stop your resource-taking actions and remove any reference
* of editor or other objects related to editor (such as references to text in editor) to avoid
* memory leaks and resource waste.
*/
@UiThread
void destroy();
}

View File

@ -0,0 +1,77 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.lang.styling.Styles;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.text.TextRange;
public interface QuickQuoteHandler {
/**
* Checks whether the given input matches the requirement to invoke this handler
*
* @param candidateCharacter The character going to be inserted. Length can be 1 or 2.
* @param text Current text in editor
* @param cursor The range of cursor
* @param style Current code styles
* @return Whether this handler consumed the event
*/
@NonNull
HandleResult onHandleTyping(@NonNull String candidateCharacter, @NonNull Content text, @NonNull TextRange cursor, @Nullable Styles style);
class HandleResult {
public final static HandleResult NOT_CONSUMED = new HandleResult(false, null);
private boolean consumed;
private TextRange newCursorRange;
public HandleResult(boolean consumed, TextRange newCursorRange) {
this.consumed = consumed;
this.newCursorRange = newCursorRange;
}
public boolean isConsumed() {
return consumed;
}
public void setConsumed(boolean consumed) {
this.consumed = consumed;
}
public TextRange getNewCursorRange() {
return newCursorRange;
}
public void setNewCursorRange(TextRange newCursorRange) {
this.newCursorRange = newCursorRange;
}
}
}

View File

@ -0,0 +1,96 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.analysis;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.text.ContentReference;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* AnalyzeManager receives text updates and do its work to start lexers/parsers to analyze the code
* so that we can provide syntax-highlighting and exact auto-completion.
*
* @author Rosemoe
*/
public interface AnalyzeManager {
/**
* Set the span receiver of the manager.
* <p>
* This is called when the {@link io.github.rosemoe.sora.lang.Language} is going to be used by
* an editor. It will also be called when the instance is no longer used.
* Make sure you check the exact receiver at the time you send results to it.
* Thus, you should save the instance at your side.
*/
void setReceiver(@Nullable StyleReceiver receiver);
/**
* Called when new text is set in editor by either {@link CodeEditor#setText(CharSequence)}
* or {@link CodeEditor#setText(CharSequence, Bundle).}
*
* @param content New text, read-only. Accesses to it are not validated. It is not recommended saving
* this object for reading. You can make a copy of the text and update it. But for
* memory saving, you may want to store it.
* @param extraArguments Arguments set by {@link CodeEditor#setText(CharSequence, Bundle)}
*/
void reset(@NonNull ContentReference content, @NonNull Bundle extraArguments);
/**
* The {@link ContentReference} set by {@link #reset(ContentReference, Bundle)} has inserted
* text with the given position and text.
*
* @param start The insertion start
* @param end The insertion end
* @param insertedContent The content inserted
*/
void insert(@NonNull CharPosition start, @NonNull CharPosition end, @NonNull CharSequence insertedContent);
/**
* The {@link ContentReference} set by {@link #reset(ContentReference, Bundle)} has deleted
* text with the given position and text.
*
* @param start The deletion start
* @param end The deletion end
* @param deletedContent The text deleted. Generated by {@link Content} object.
*/
void delete(@NonNull CharPosition start, @NonNull CharPosition end, @NonNull CharSequence deletedContent);
/**
* Rerun the analysis forcibly
*/
void rerun();
/**
* Destroy the manager. Release any resources held.
* Make sure that you will not call the receiver anymore.
*/
void destroy();
}

View File

@ -0,0 +1,600 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.analysis;
import android.os.Message;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import io.github.rosemoe.sora.lang.styling.CodeBlock;
import io.github.rosemoe.sora.lang.styling.Span;
import io.github.rosemoe.sora.lang.styling.SpanFactory;
import io.github.rosemoe.sora.lang.styling.Spans;
import io.github.rosemoe.sora.lang.styling.Styles;
import io.github.rosemoe.sora.lang.util.BaseAnalyzeManager;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.util.IntPair;
import io.github.rosemoe.sora.widget.schemes.EditorColorScheme;
/**
* Asynchronous base implementation of {@link IncrementalAnalyzeManager}
* <p>
* {@inheritDoc}
*
* @author Rosemoe
*/
public abstract class AsyncIncrementalAnalyzeManager<S, T> extends BaseAnalyzeManager implements IncrementalAnalyzeManager<S, T> {
private final static int MSG_BASE = 11451400;
private final static int MSG_INIT = MSG_BASE + 1;
private final static int MSG_MOD = MSG_BASE + 2;
private static int sThreadId = 0;
private LooperThread thread;
private volatile long runCount;
private synchronized static int nextThreadId() {
sThreadId++;
return sThreadId;
}
/**
* Run the given code block only when the receiver is currently non-null
*/
protected void withReceiver(@NonNull ReceiverConsumer consumer) {
var r = getReceiver();
if (r != null) {
consumer.accept(r);
}
}
@Override
public void insert(@NonNull CharPosition start, @NonNull CharPosition end, @NonNull CharSequence insertedText) {
if (thread != null) {
increaseRunCount();
thread.offerMessage(MSG_MOD, new TextModification(IntPair.pack(start.line, start.column), IntPair.pack(end.line, end.column), insertedText));
}
}
@Override
public void delete(@NonNull CharPosition start, @NonNull CharPosition end, @NonNull CharSequence deletedText) {
if (thread != null) {
increaseRunCount();
thread.offerMessage(MSG_MOD, new TextModification(IntPair.pack(start.line, start.column), IntPair.pack(end.line, end.column), null));
}
}
@Override
public void rerun() {
if (thread != null) {
if (thread.isAlive()) {
thread.interrupt();
thread.abort = true;
}
}
final var text = getContentRef().getReference().copyText(false);
text.setUndoEnabled(false);
thread = new LooperThread();
thread.setName("AsyncAnalyzer-" + nextThreadId());
thread.offerMessage(MSG_INIT, text);
increaseRunCount();
sendNewStyles(null);
thread.start();
}
@Override
public LineTokenizeResult<S, T> getState(int line) {
final var thread = this.thread;
if (thread == Thread.currentThread()) {
if (line >= 0 && line < thread.states.size()) {
return thread.states.get(line);
}
return null;
}
throw new SecurityException("Can not get state from non-analytical or abandoned thread");
}
@Override
public void onAbandonState(S state) {
}
@Override
public void onAddState(S state) {
}
private synchronized void increaseRunCount() {
runCount++;
}
@Override
public void destroy() {
if (thread != null) {
if (thread.isAlive()) {
thread.interrupt();
}
thread.abort = true;
}
thread = null;
super.destroy();
}
private void sendNewStyles(Styles styles) {
final var r = getReceiver();
if (r != null) {
r.setStyles(this, styles);
}
}
private void sendUpdate(Styles styles, int startLine, int endLine) {
final var r = getReceiver();
if (r != null) {
r.updateStyles(this, styles, new SequenceUpdateRange(startLine, endLine));
}
}
/**
* Compute code blocks
*
* @param text The text. can be safely accessed.
*/
public abstract List<CodeBlock> computeBlocks(Content text, CodeBlockAnalyzeDelegate delegate);
public Styles getManagedStyles() {
var thread = Thread.currentThread();
if (thread.getClass() != AsyncIncrementalAnalyzeManager.LooperThread.class) {
throw new IllegalThreadStateException();
}
return ((AsyncIncrementalAnalyzeManager<?, ?>.LooperThread) thread).styles;
}
private static class LockedSpans implements Spans {
private final Lock lock;
private final List<Line> lines;
public LockedSpans() {
lines = new ArrayList<>(128);
lock = new ReentrantLock();
}
@Override
public void adjustOnDelete(CharPosition start, CharPosition end) {
}
@Override
public void adjustOnInsert(CharPosition start, CharPosition end) {
}
@Override
public int getLineCount() {
return lines.size();
}
@Override
public Reader read() {
return new ReaderImpl();
}
@Override
public Modifier modify() {
return new ModifierImpl();
}
@Override
public boolean supportsModify() {
return true;
}
private static class Line {
public Lock lock = new ReentrantLock();
public List<Span> spans;
public Line() {
this(null);
}
public Line(List<Span> s) {
spans = s;
}
}
private class ReaderImpl implements Spans.Reader {
private Line line;
public void moveToLine(int line) {
if (line < 0 || line >= lines.size()) {
if (this.line != null) {
this.line.lock.unlock();
}
this.line = null;
} else {
if (this.line != null) {
this.line.lock.unlock();
}
var locked = false;
try {
locked = lock.tryLock(100, TimeUnit.MICROSECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
}
if (locked) {
try {
var obj = lines.get(line);
if (obj.lock.tryLock()) {
this.line = obj;
} else {
this.line = null;
}
} finally {
lock.unlock();
}
} else {
this.line = null;
}
}
}
@Override
public int getSpanCount() {
return line == null ? 1 : line.spans.size();
}
@Override
public Span getSpanAt(int index) {
return line == null ? SpanFactory.obtain(0, EditorColorScheme.TEXT_NORMAL) : line.spans.get(index);
}
@Override
public List<Span> getSpansOnLine(int line) {
var spans = new ArrayList<Span>();
var locked = false;
try {
locked = lock.tryLock(1, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (locked) {
Line obj = null;
try {
if (line < lines.size()) {
obj = lines.get(line);
}
} finally {
lock.unlock();
}
if (obj != null && obj.lock.tryLock()) {
try {
return Collections.unmodifiableList(obj.spans);
} finally {
obj.lock.unlock();
}
} else {
spans.add(getSpanAt(0));
}
} else {
spans.add(getSpanAt(0));
}
return spans;
}
}
private class ModifierImpl implements Modifier {
@Override
public void setSpansOnLine(int line, List<Span> spans) {
lock.lock();
try {
while (lines.size() <= line) {
var list = new ArrayList<Span>();
list.add(SpanFactory.obtain(0, EditorColorScheme.TEXT_NORMAL));
lines.add(new Line(list));
}
var obj = lines.get(line);
obj.lock.lock();
try {
obj.spans = spans;
} finally {
obj.lock.unlock();
}
} finally {
lock.unlock();
}
}
@Override
public void addLineAt(int line, List<Span> spans) {
lock.lock();
try {
lines.add(line, new Line(spans));
} finally {
lock.unlock();
}
}
@Override
public void deleteLineAt(int line) {
lock.lock();
try {
var obj = lines.get(line);
obj.lock.lock();
try {
lines.remove(line);
} finally {
obj.lock.unlock();
}
} finally {
lock.unlock();
}
}
}
}
private static class TextModification {
private final long start;
private final long end;
/**
* null for deletion
*/
private final CharSequence changedText;
TextModification(long start, long end, CharSequence text) {
this.start = start;
this.end = end;
changedText = text;
}
}
/**
* Helper class for analyzing code block
*/
public class CodeBlockAnalyzeDelegate {
private final LooperThread thread;
int suppressSwitch;
CodeBlockAnalyzeDelegate(@NonNull LooperThread lp) {
thread = lp;
}
public void setSuppressSwitch(int suppressSwitch) {
this.suppressSwitch = suppressSwitch;
}
void reset() {
suppressSwitch = Integer.MAX_VALUE;
}
public boolean isCancelled() {
return thread.myRunCount != runCount || thread.abort || thread.isInterrupted();
}
public boolean isNotCancelled() {
return !isCancelled();
}
}
private final class LooperThread extends Thread {
private final BlockingQueue<Message> messageQueue = new LinkedBlockingQueue<>();
volatile boolean abort;
Content shadowed;
long myRunCount;
List<LineTokenizeResult<S, T>> states = new ArrayList<>();
Styles styles;
LockedSpans spans;
CodeBlockAnalyzeDelegate delegate = new CodeBlockAnalyzeDelegate(this);
public void offerMessage(int what, @Nullable Object obj) {
var msg = Message.obtain();
msg.what = what;
msg.obj = obj;
offerMessage(msg);
}
public void offerMessage(@NonNull Message msg) {
// Result ignored: capacity is enough as it is INT_MAX
//noinspection ResultOfMethodCallIgnored
messageQueue.offer(msg);
}
private void initialize() {
styles = new Styles(spans = new LockedSpans());
S state = getInitialState();
var mdf = spans.modify();
for (int i = 0; i < shadowed.getLineCount() && !abort && !isInterrupted(); i++) {
var line = shadowed.getLine(i);
var result = tokenizeLine(line, state, i);
state = result.state;
var spans = result.spans != null ? result.spans : generateSpansForLine(result);
states.add(result.clearSpans());
onAddState(result.state);
mdf.addLineAt(i, spans);
}
styles.blocks = computeBlocks(shadowed, delegate);
styles.setSuppressSwitch(delegate.suppressSwitch);
styles.finishBuilding();
if (!abort)
sendNewStyles(styles);
}
public boolean handleMessage(@NonNull Message msg) {
try {
myRunCount = runCount;
delegate.reset();
switch (msg.what) {
case MSG_INIT:
shadowed = (Content) msg.obj;
if (!abort && !isInterrupted()) {
initialize();
}
break;
case MSG_MOD:
int updateStart = 0, updateEnd = 0;
if (!abort && !isInterrupted()) {
var mod = (TextModification) msg.obj;
int startLine = IntPair.getFirst(mod.start);
int endLine = IntPair.getFirst(mod.end);
updateStart = startLine;
if (mod.changedText == null) {
shadowed.delete(IntPair.getFirst(mod.start), IntPair.getSecond(mod.start),
IntPair.getFirst(mod.end), IntPair.getSecond(mod.end));
S state = startLine == 0 ? getInitialState() : states.get(startLine - 1).state;
// Remove states
if (endLine >= startLine + 1) {
var subList = states.subList(startLine + 1, endLine + 1);
for (LineTokenizeResult<S, T> stLineTokenizeResult : subList) {
onAbandonState(stLineTokenizeResult.state);
}
subList.clear();
}
var mdf = spans.modify();
for (int i = startLine + 1; i <= endLine; i++) {
mdf.deleteLineAt(startLine + 1);
}
int line = startLine;
while (line < shadowed.getLineCount()) {
var res = tokenizeLine(shadowed.getLine(line), state, line);
mdf.setSpansOnLine(line, res.spans != null ? res.spans : generateSpansForLine(res));
var old = states.set(line, res.clearSpans());
if (old != null) {
onAbandonState(old.state);
}
onAddState(res.state);
if (stateEquals(old == null ? null : old.state, res.state)) {
break;
}
state = res.state;
line++;
}
updateEnd = line;
} else {
shadowed.insert(IntPair.getFirst(mod.start), IntPair.getSecond(mod.start), mod.changedText);
S state = startLine == 0 ? getInitialState() : states.get(startLine - 1).state;
int line = startLine;
var spans = styles.spans.modify();
// Add Lines
while (line <= endLine) {
var res = tokenizeLine(shadowed.getLine(line), state, line);
if (line == startLine) {
spans.setSpansOnLine(line, res.spans != null ? res.spans : generateSpansForLine(res));
var old = states.set(line, res.clearSpans());
if (old != null) {
onAbandonState(old.state);
}
} else {
spans.addLineAt(line, res.spans != null ? res.spans : generateSpansForLine(res));
states.add(line, res.clearSpans());
}
onAddState(res.state);
state = res.state;
line++;
}
// line = end.line + 1, check whether the state equals
boolean flag = true;
while (line < shadowed.getLineCount() && flag) {
var res = tokenizeLine(shadowed.getLine(line), state, line);
if (stateEquals(res.state, states.get(line).state)) {
flag = false;
}
spans.setSpansOnLine(line, res.spans != null ? res.spans : generateSpansForLine(res));
var old = states.set(line, res.clearSpans());
if (old != null) {
onAbandonState(old.state);
}
onAddState(res.state);
state = res.state;
line++;
}
updateEnd = line;
}
}
// Do not update incomplete code blocks
var blocks = computeBlocks(shadowed, delegate);
if (delegate.isNotCancelled()) {
styles.blocks = blocks;
styles.finishBuilding();
styles.setSuppressSwitch(delegate.suppressSwitch);
}
if (!abort) {
sendUpdate(styles, updateStart, updateEnd);
}
break;
}
return true;
} catch (Exception e) {
Log.w("AsyncAnalysis", "Thread " + Thread.currentThread().getName() + " failed", e);
}
return false;
}
@Override
public void run() {
try {
while (!abort && !isInterrupted()) {
var msg = messageQueue.take();
if (!handleMessage(msg)) {
break;
}
msg.recycle();
}
} catch (InterruptedException e) {
// ignored
}
}
}
public interface ReceiverConsumer {
void accept(@NonNull StyleReceiver receiver);
}
}

View File

@ -0,0 +1,119 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.analysis;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.List;
import io.github.rosemoe.sora.lang.styling.Span;
/**
* Interface for line based analyze managers
*
* @param <S> State type at line endings
* @param <T> Token type
*/
public interface IncrementalAnalyzeManager<S, T> extends AnalyzeManager {
/**
* Get the initial at document start
*/
S getInitialState();
/**
* Get recorded state for subclass
*/
LineTokenizeResult<S, T> getState(int line);
/**
* Compare the two states.
* Return true if they equal
*/
boolean stateEquals(S state, S another);
/**
* Tokenize for the given line
*
* @param lineIndex -1 for unknown
*/
LineTokenizeResult<S, T> tokenizeLine(CharSequence line, S state, int lineIndex);
/**
* Generate spans for the line
*/
List<Span> generateSpansForLine(LineTokenizeResult<S, T> tokens);
/**
* Called when a State object is to be abandoned
*/
void onAbandonState(S state);
/**
* Called when a State object is to be added
*/
void onAddState(S state);
/**
* Saved state
*/
class LineTokenizeResult<S_, T_> {
/**
* State at line end
*/
public S_ state;
/**
* Tokens on this line
*/
public List<T_> tokens;
/**
* Spans. If spans are generated as well you can directly return them here to avoid
* {@link #generateSpansForLine(LineTokenizeResult)} calls.
*/
public List<Span> spans;
public LineTokenizeResult(@NonNull S_ state, @Nullable List<T_> tokens) {
this.state = state;
this.tokens = tokens;
}
public LineTokenizeResult(@NonNull S_ state, @Nullable List<T_> tokens, @Nullable List<Span> spans) {
this.state = state;
this.tokens = tokens;
this.spans = spans;
}
protected LineTokenizeResult<S_, T_> clearSpans() {
spans = null;
return this;
}
}
}

View File

@ -0,0 +1,43 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
package io.github.rosemoe.sora.lang.analysis
import java.lang.Integer.min
class SequenceUpdateRange(val startLine: Int, val endLine: Int = Int.MAX_VALUE) : StyleUpdateRange {
override fun isInRange(line: Int) = line in startLine..endLine
override fun lineIndexIterator(maxLineIndex: Int) = object : IntIterator() {
var currentLine = startLine
override fun hasNext() = currentLine <= min(endLine, maxLineIndex)
override fun nextInt() = currentLine++
}
}

View File

@ -0,0 +1,237 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.analysis;
import android.os.Bundle;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.lang.styling.Styles;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.ContentReference;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* Built-in implementation of {@link AnalyzeManager}.
* <p>
* This is a simple version without any incremental actions.
* <p>
* The analysis will always re-run when the text changes. Hopefully, it will stop previous outdated
* runs by provide a {@link Delegate} object.
*
* @param <V> The shared object type that we get for auto-completion.
*/
public abstract class SimpleAnalyzeManager<V> implements AnalyzeManager {
private final static String LOG_TAG = "SimpleAnalyzeManager";
private static int sThreadId = 0;
private final Object lock = new Object();
private StyleReceiver receiver;
private volatile ContentReference ref;
private Bundle extraArguments;
private volatile long newestRequestId;
private AnalyzeThread thread;
private V data;
private synchronized static int nextThreadId() {
sThreadId++;
return sThreadId;
}
@Override
public void setReceiver(@Nullable StyleReceiver receiver) {
this.receiver = receiver;
}
@Override
public void reset(@NonNull ContentReference content, @NonNull Bundle extraArguments) {
this.ref = content;
this.extraArguments = extraArguments;
rerun();
}
@Override
public void insert(@NonNull CharPosition start, @NonNull CharPosition end, @NonNull CharSequence insertedContent) {
rerun();
}
@Override
public void delete(@NonNull CharPosition start, @NonNull CharPosition end, @NonNull CharSequence deletedContent) {
rerun();
}
@Override
public synchronized void rerun() {
newestRequestId++;
if (thread == null || !thread.isAlive()) {
// Create new thread
Log.v(LOG_TAG, "Starting a new thread for analysis");
thread = new AnalyzeThread();
thread.setDaemon(true);
thread.setName("SplAnalyzer-" + nextThreadId());
thread.start();
}
synchronized (lock) {
lock.notify();
}
}
@Override
public void destroy() {
ref = null;
extraArguments = null;
newestRequestId = 0;
data = null;
if (thread != null && thread.isAlive()) {
thread.interrupt();
}
thread = null;
receiver = null;
}
/**
* Get extra arguments set by {@link CodeEditor#setText(CharSequence, Bundle)}
*/
public Bundle getExtraArguments() {
return extraArguments;
}
/**
* Get data set by analyze thread
*/
@Nullable
public V getData() {
return data;
}
/**
* Analyze the given input.
*
* @param text A {@link StringBuilder} instance containing the text in editor. DO NOT SAVE THE INSTANCE OR
* UPDATE IT. It is continuously used by this analyzer.
* @param delegate A delegate used to check whether this invocation is outdated. You should stop your logic
* if {@link Delegate#isCancelled()} returns true.
* @return Styles created according to the text.
*/
protected abstract Styles analyze(StringBuilder text, Delegate<V> delegate);
/**
* Analyze thread.
* <p>
* The thread will keep alive unless there is any exception or {@link AnalyzeManager#destroy()}
* is called.
*/
private class AnalyzeThread extends Thread {
/**
* Single instance for text storing
*/
private final StringBuilder textContainer = new StringBuilder();
@Override
public void run() {
Log.v(LOG_TAG, "Analyze thread started");
try {
while (!isInterrupted()) {
var text = ref;
if (text != null) {
var requestId = 0L;
Styles result = null;
V newData = null;
// Do the analysis, until the requestId matches
do {
text = ref;
if (text == null) {
break;
}
requestId = newestRequestId;
var delegate = new Delegate<V>(requestId);
// Collect line contents
textContainer.setLength(0);
textContainer.ensureCapacity(text.length());
for (int i = 0; i < text.getLineCount() && requestId == newestRequestId; i++) {
if (i != 0) {
textContainer.append(text.getLineSeparator(i - 1));
}
text.appendLineTo(textContainer, i);
}
// Invoke the implementation
result = analyze(textContainer, delegate);
newData = delegate.data;
} while (requestId != newestRequestId);
// Send result
final var receiver = SimpleAnalyzeManager.this.receiver;
if (receiver != null && result != null) {
receiver.setStyles(SimpleAnalyzeManager.this, result);
}
data = newData;
}
// Wait for next time
synchronized (lock) {
lock.wait();
}
}
} catch (InterruptedException e) {
Log.v(LOG_TAG, "Thread is interrupted.");
} catch (Exception e) {
Log.e(LOG_TAG, "Unexpected exception is thrown in the thread.", e);
}
}
}
/**
* Delegate between manager and analysis implementation
*/
public final class Delegate<T> {
private final long myRequestId;
private T data;
public Delegate(long requestId) {
myRequestId = requestId;
}
/**
* Set shared data
*/
public void setData(T value) {
data = value;
}
/**
* Check whether the operation is cancelled
*/
public boolean isCancelled() {
return myRequestId != newestRequestId;
}
}
}

View File

@ -0,0 +1,89 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.analysis;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.lang.brackets.BracketsProvider;
import io.github.rosemoe.sora.lang.diagnostic.DiagnosticsContainer;
import io.github.rosemoe.sora.lang.styling.Styles;
/**
* A {@link StyleReceiver} receives spans and other styles from analyzers.
* <p>
* The implementations of the class must make sure its code can be safely run. For example, update
* UI by posting its actions to UI thread, but not here.
* <p>
* Also, the implementations of the class should pay attention to concurrent invocations due not to
* corrupt the information it maintains.
*
* @author Rosemoe
*/
public interface StyleReceiver {
/**
* Send the styles to the receiver. You can call it in any thread.
* The implementation of this method should make sure that concurrent invocations to it are safe.
*
* @param sourceManager Source AnalyzeManager. The receiver may ignore the request if some checks on
* the sourceManager fail
*/
void setStyles(@NonNull AnalyzeManager sourceManager, @Nullable Styles styles);
/**
* Send the styles to the receiver. You can call it in any thread.
* The implementation of this method should make sure that concurrent invocations to it are safe.
*
* @param sourceManager Source AnalyzeManager. The receiver may ignore the request if some checks on
* the sourceManager fail
* @param action Sometimes you may need to synchronize your action in main thread. This ensures the given action is executed
* on main thread before the style updates.
*/
void setStyles(@NonNull AnalyzeManager sourceManager, @Nullable Styles styles, @Nullable Runnable action);
/**
* Notify the receiver the given styles object is updated, and line range is given by {@code range}
*
* @param sourceManager Source AnalyzeManager. The receiver may ignore the request if some checks on
* the sourceManager fail
* @param styles The Styles object previously set by {@link #setStyles(AnalyzeManager, Styles)}
* @param range The line range of this update
*/
default void updateStyles(@NonNull AnalyzeManager sourceManager, @NonNull Styles styles, @NonNull StyleUpdateRange range) {
setStyles(sourceManager, styles);
}
/**
* Specify new diagnostics. You can call it in any thread.
* The implementation of this method should make sure that concurrent invocations to it are safe.
*/
void setDiagnostics(@NonNull AnalyzeManager sourceManager, @Nullable DiagnosticsContainer diagnostics);
/**
* Set new provider for brackets highlighting
*/
void updateBracketProvider(@NonNull AnalyzeManager sourceManager, @Nullable BracketsProvider provider);
}

View File

@ -0,0 +1,44 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
package io.github.rosemoe.sora.lang.analysis
/**
* Describe the range of a style update
*
* @author Rosemoe
*/
interface StyleUpdateRange {
/**
* Check whether the given [line] index is in range
*/
fun isInRange(line: Int): Boolean
/**
* Get a new iterator for line indices in range
*/
fun lineIndexIterator(maxLineIndex: Int): IntIterator
}

View File

@ -0,0 +1,48 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.brackets;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.text.Content;
/**
* Interface for providing paired brackets
*
* @author Rosemoe
*/
public interface BracketsProvider {
/**
* Get left and right brackets position in text
*
* @param text The text in editor
* @param index Index of cursor in text
* @return Paired positions or null if not matched
*/
@Nullable
PairedBracket getPairedBracketAt(@NonNull Content text, int index);
}

View File

@ -0,0 +1,111 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.brackets;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.text.Content;
/**
* Compute paired bracket when queried
*
* @author Rosemoe
*/
public class OnlineBracketsMatcher implements BracketsProvider {
private final char[] pairs;
private final int limit;
/**
* @param pairs Pairs. For example: {'(', ')', '{', '}'}
* @param limit Max length to search
*/
public OnlineBracketsMatcher(char[] pairs, int limit) {
if ((pairs.length & 1) != 0) {
throw new IllegalArgumentException("pairs must have even length");
}
this.pairs = pairs;
this.limit = limit;
}
private int findIndex(char ch) {
for (int i = 0; i < pairs.length; i++) {
if (ch == pairs[i]) {
return i;
}
}
return -1;
}
private PairedBracket tryComputePaired(Content text, int index) {
char a = text.charAt(index);
int symbolIndex = findIndex(a);
if (symbolIndex != -1) {
char b = pairs[symbolIndex ^ 1];
int stack = 0;
if ((symbolIndex & 1) == 0) {
// Find forward
for (int i = index + 1; i < text.length() && i - index < limit; i++) {
char ch = text.charAt(i);
if (ch == b) {
if (stack <= 0) {
return new PairedBracket(index, i);
} else {
stack--;
}
} else if (ch == a) {
stack++;
}
}
} else {
// Find backward
for (int i = index - 1; i >= 0 && index - i < limit; i--) {
char ch = text.charAt(i);
if (ch == b) {
if (stack <= 0) {
return new PairedBracket(i, index);
} else {
stack--;
}
} else if (ch == a) {
stack++;
}
}
}
}
return null;
}
@Override
public PairedBracket getPairedBracketAt(@NonNull Content text, int index) {
PairedBracket pairedBracket = null;
if (index > 0) {
pairedBracket = tryComputePaired(text, index - 1);
}
if (pairedBracket == null) {
pairedBracket = tryComputePaired(text, index);
}
return pairedBracket;
}
}

View File

@ -0,0 +1,56 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.brackets;
/**
* Describes paired brackets
*
* @author Rosemoe
*/
public class PairedBracket {
public final int leftIndex, leftLength, rightIndex, rightLength;
/**
* Currently length is always 1.
*
* @see #PairedBracket(int, int, int, int)
*/
public PairedBracket(int leftIndex, int rightIndex) {
this(leftIndex, 1, rightIndex, 1);
}
/**
* @param leftIndex Index of left bracket in text
* @param leftLength Text length of left bracket
* @param rightIndex Index of right bracket in text
* @param rightLength Text length of right bracket
*/
public PairedBracket(int leftIndex, int leftLength, int rightIndex, int rightLength) {
this.leftIndex = leftIndex;
this.leftLength = leftLength;
this.rightIndex = rightIndex;
this.rightLength = rightLength;
}
}

View File

@ -0,0 +1,82 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.brackets;
import android.util.SparseIntArray;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.text.Content;
/**
* Collect brackets for simple languages. Not very effective. Not thread-safe.
*
* @author Rosemoe
*/
public class SimpleBracketsCollector implements BracketsProvider {
private final SparseIntArray mapping;
public SimpleBracketsCollector() {
mapping = new SparseIntArray();
}
/**
* Add new pair
*/
public void add(int start, int end) {
// add 1 to avoid zeros
mapping.put(start + 1, end + 1);
mapping.put(end + 1, start + 1);
}
/**
* Remove all pairs
*/
public void clear() {
mapping.clear();
}
private PairedBracket getForIndex(int index) {
int another = mapping.get(index + 1) - 1;
if (another > index) {
int tmp = index;
index = another;
another = tmp;
}
if (another != -1) {
return new PairedBracket(index, another);
}
return null;
}
@Override
public PairedBracket getPairedBracketAt(@NonNull Content text, int index) {
var res = index - 1 >= 0 ? getForIndex(index - 1) : null;
if (res == null) {
res = getForIndex(index);
}
return res;
}
}

View File

@ -0,0 +1,43 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion;
/**
* Thrown when the thread is abandoned by the editor framework because the editor do not need its
* new items anymore.
* <p>
* This can be thrown by {@link io.github.rosemoe.sora.text.ContentReference} and
* {@link CompletionPublisher}.
*
* @author Rosemoe
*/
public class CompletionCancelledException extends RuntimeException {
public CompletionCancelledException() {
}
public CompletionCancelledException(String message) {
super(message);
}
}

View File

@ -0,0 +1,71 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.ContentReference;
import io.github.rosemoe.sora.widget.component.EditorAutoCompletion;
/**
* Helper class for completion
*
* @author Rosemoe
*/
public class CompletionHelper {
/**
* Searches backward on the line, with the given checker to check chars.
* Returns the longest text that matches the requirement
*/
public static String computePrefix(ContentReference ref, CharPosition pos, PrefixChecker checker) {
int begin = pos.column;
var line = ref.getLine(pos.line);
for (; begin > 0; begin--) {
if (!checker.check(line.charAt(begin - 1))) {
break;
}
}
return line.substring(begin, pos.column);
}
/**
* Check whether the thread is abandoned by editor.
* Return true if it is cancelled by editor.
*/
public static boolean checkCancelled() {
var thread = Thread.currentThread();
if (thread instanceof EditorAutoCompletion.CompletionThread) {
return ((EditorAutoCompletion.CompletionThread) thread).isCancelled();
} else {
return false;
}
}
public interface PrefixChecker {
boolean check(char ch);
}
}

View File

@ -0,0 +1,154 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* The class used to save auto complete result items.
* For functionality, this class only manages the information to be displayed in list view.
* You can implement {@link CompletionItem#performCompletion(CodeEditor, Content, int, int)} or
* {@link CompletionItem#performCompletion(CodeEditor, Content, CharPosition)} to customize
* your own completion method so that you can develop complex actions.
* <p>
* For the simplest usage, see {@link SimpleCompletionItem}
*
* @author Rosemoe
* @see SimpleCompletionItem
*/
@SuppressWarnings("CanBeFinal")
public abstract class CompletionItem {
/**
* Icon for displaying in adapter
*/
@Nullable
public Drawable icon;
/**
* Text to display as title in adapter
*/
public CharSequence label;
/**
* Text to display as description in adapter
*/
public CharSequence desc;
/**
* The kind of this completion item. Based on the kind
* an icon is chosen by the editor.
*/
@Nullable
protected CompletionItemKind kind;
/**
* Use for default sort
*/
public int prefixLength = 0;
/**
* A string that should be used when comparing this item
* with other items. When null the {@link #label label}
* is used.
*/
@Nullable
public String sortText;
@Nullable
protected Object extra;
public CompletionItem(CharSequence label) {
this(label, null);
}
public CompletionItem(CharSequence label, CharSequence desc) {
this(label, desc, null);
}
public CompletionItem(CharSequence label, CharSequence desc, Drawable icon) {
this.label = label;
this.desc = desc;
this.icon = icon;
}
public CompletionItem label(CharSequence label) {
this.label = label;
return this;
}
public CompletionItem desc(CharSequence desc) {
this.desc = desc;
return this;
}
public CompletionItem kind(CompletionItemKind kind) {
this.kind = kind;
return this;
}
public CompletionItem icon(Drawable icon) {
this.icon = icon;
return this;
}
/**
* Perform this completion.
* You can implement custom logic to make your completion better(by updating selection and text
* from here).
* To make it considered as a single action, the editor will enter batch edit state before invoking
* this method. Feel free to update the text by multiple calls to {@code text}.
*
* @param editor The editor. You can set cursor position with that.
* @param text The text in editor. You can make modifications to it.
* @param position The requested completion position (the one passed to completion thread)
*/
public void performCompletion(@NonNull CodeEditor editor, @NonNull Content text, @NonNull CharPosition position) {
performCompletion(editor, text, position.line, position.column);
}
/**
* Perform this completion.
* You can implement custom logic to make your completion better(by updating selection and text
* from here).
* To make it considered as a single action, the editor will enter batch edit state before invoking
* this method. Feel free to update the text by multiple calls to {@code text}.
*
* @param editor The editor. You can set cursor position with that.
* @param text The text in editor. You can make modifications to it.
* @param line The auto-completion line
* @param column The auto-completion column
* @see #performCompletion(CodeEditor, Content, CharPosition) Editor calls this method to do completion
*/
public abstract void performCompletion(@NonNull CodeEditor editor, @NonNull Content text, int line, int column);
}

View File

@ -0,0 +1,67 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
package io.github.rosemoe.sora.lang.completion
/**
* Completion item kinds.
*/
enum class CompletionItemKind(
val value: Int,
val defaultDisplayBackgroundColor: Long = 0,
) {
Identifier(0, 0xffabb6bd),
Text(0, 0xffabb6bd),
Method(1, 0xfff4b2be),
Function(2, 0xfff4b2be),
Constructor(3, 0xfff4b2be),
Field(4, 0xfff1c883),
Variable(5, 0xfff1c883),
Class(6, 0xff85cce5),
Interface(7, 0xff99cb87),
Module(8, 0xff85cce5),
Property(9, 0xffcebcf4),
Unit(10),
Value(11, 0xfff1c883),
Enum(12, 0xff85cce5),
Keyword(13, 0xffcc7832),
Snippet(14),
Color(15, 0xfff4b2be),
Reference(17),
File(16),
Folder(18),
EnumMember(19),
Constant(20, 0xfff1c883),
Struct(21, 0xffcebcf4),
Event(22),
Operator(23, 0xffeaabb6),
TypeParameter(24, 0xfff1c883),
User(25),
Issue(26);
private val displayString = name[0].toString()
fun getDisplayChar(): String = displayString
}

View File

@ -0,0 +1,283 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion;
import android.os.Handler;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import io.github.rosemoe.sora.annotations.UnsupportedUserUsage;
import io.github.rosemoe.sora.lang.Language;
/**
* CompletionPublisher manages completion items to be added in one completion analyzing process.
* <p>
* You can only add items to the publisher, but no deletion is allowed. As you add more items, the
* publisher will update the list in UI from time to time, which is related to your threshold
* settings.({@link CompletionPublisher#setUpdateThreshold(int)}).
* There will usually be some items not displayed in screen when the thread is still running. Even
* when the actual pending item count exceeds the threshold you set, there may still be some items
* not committed because of lock failures. You can use {@link CompletionPublisher#updateList(boolean)}
* with forced flag to command the UI thread update the completion list, by waiting for the lock from
* your side to release.
* If you want to disable this feature, you may want to set it to {@link Integer#MAX_VALUE}
* <p>
* You can set a comparator by {@link CompletionPublisher#setComparator(Comparator)} to sort your
* result items, but you should not make it too complex, which will cause laggy in UI thread. It is
* recommended that you set the comparator before all your actions.
* Leaving the comparator null results the completion to be unsorted. They will be ordered by the order
* you add them.
* <p>
* After all you additions, you do not need to explicitly invoke {@link CompletionPublisher#updateList(boolean)}.
* This will automatically be called by editor framework.
* <p>
* Note that your actions may be interrupted because of {@link Thread#interrupted()}.
*/
public class CompletionPublisher {
/**
* Default value for {@link CompletionPublisher#setUpdateThreshold(int)}
*/
public final static int DEFAULT_UPDATE_THRESHOLD = 5;
private final List<CompletionItem> items;
private final List<CompletionItem> candidates;
private final Lock lock;
private final Handler handler;
private final Runnable callback;
private final int languageInterruptionLevel;
private Comparator<CompletionItem> comparator;
private int updateThreshold;
private boolean invalid = false;
public CompletionPublisher(@NonNull Handler handler, @NonNull Runnable callback, int languageInterruptionLevel) {
this.handler = handler;
this.items = new ArrayList<>();
this.candidates = new ArrayList<>();
lock = new ReentrantLock(true);
updateThreshold = DEFAULT_UPDATE_THRESHOLD;
this.callback = callback;
this.languageInterruptionLevel = languageInterruptionLevel;
}
/**
* Checks whether there is data
*/
public boolean hasData() {
return items.size() + candidates.size() > 0;
}
/**
* Get items currently in display
*/
@UnsupportedUserUsage
public List<CompletionItem> getItems() {
return items;
}
/**
* Set the max pending items in analyzing thread.
* See class javadoc for more information.
*/
public void setUpdateThreshold(int updateThreshold) {
this.updateThreshold = updateThreshold;
}
/**
* Set the result's comparator.
* <p>
* The comparator is used when publishing the completion to user.
*/
public void setComparator(@Nullable Comparator<CompletionItem> comparator) {
checkCancelled();
if (invalid) {
return;
}
this.comparator = comparator;
if (!items.isEmpty() && comparator != null) {
handler.post(() -> {
if (invalid) {
return;
}
Collections.sort(items, comparator);
callback.run();
});
}
}
/**
* Add items in the completion list.
* <p>
* According to your settings and the lock's state, these items may not immediately
* be displayed to the user.
*
* @see CompletionPublisher#setUpdateThreshold(int)
*/
public void addItems(Collection<CompletionItem> items) {
checkCancelled();
if (invalid) {
return;
}
lock.lock();
try {
candidates.addAll(items);
} finally {
lock.unlock();
}
if (candidates.size() >= updateThreshold) {
updateList();
}
}
/**
* Add a single item in completion list.
* <p>
* According to your settings and the lock's state, this item may not immediately
* be displayed to the user.
*
* @see CompletionPublisher#setUpdateThreshold(int)
*/
public void addItem(CompletionItem item) {
checkCancelled();
if (invalid) {
return;
}
lock.lock();
try {
candidates.add(item);
} finally {
lock.unlock();
}
if (candidates.size() >= updateThreshold) {
updateList();
}
}
/**
* Try to update completion in main thread.
* <p>
* If {@link Lock#tryLock()} failed, nothing will happen.
*/
public void updateList() {
updateList(false);
}
/**
* Update completion items on main thread
*
* @param forced If true, the main thread will wait for the lock. Otherwise, when the lock is
* currently available for the thread, the update will be executed.
*/
public void updateList(boolean forced) {
if (invalid) {
return;
}
handler.post(() -> {
// Lock the candidate list accordingly
if (invalid) {
callback.run();
return;
}
var locked = false;
if (forced) {
lock.lock();
locked = true;
} else {
locked = lock.tryLock();
}
if (locked) {
try {
if (candidates.isEmpty()) {
callback.run();
return;
}
final var comparator = this.comparator;
if (comparator != null) {
while (!candidates.isEmpty()) {
var candidate = candidates.remove(0);
// Insert the value by binary search
int left = 0, right = items.size();
var size = right;
while (left <= right) {
var mid = (left + right) / 2;
if (mid < 0 || mid >= size) {
left = mid;
break;
}
var cmp = comparator.compare(items.get(mid), candidate);
if (cmp < 0) {
left = mid + 1;
} else if (cmp > 0) {
right = mid - 1;
} else {
left = mid;
break;
}
}
left = Math.max(0, Math.min(size, left));
items.add(left, candidate);
}
} else {
items.addAll(candidates);
candidates.clear();
}
callback.run();
} finally {
lock.unlock();
}
}
});
}
/**
* Cancel the completion
*/
public void cancel() {
invalid = true;
}
/**
* Check whether the completion is cancelled. If so, an instance of {@link CompletionCancelledException}
* is thrown.
*/
public void checkCancelled() {
if (Thread.interrupted() || invalid) {
invalid = true;
if (languageInterruptionLevel <= Language.INTERRUPTION_LEVEL_SLIGHT) {
throw new CompletionCancelledException();
}
}
}
}

View File

@ -0,0 +1,356 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion;
import android.os.Build;
import android.os.Bundle;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import io.github.rosemoe.sora.lang.Language;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.ContentReference;
import io.github.rosemoe.sora.text.TextUtils;
import io.github.rosemoe.sora.util.MutableInt;
/**
* Identifier auto-completion.
* <p>
* You can use it to provide identifiers, but you can't update the given {@link CompletionPublisher}
* if it is used. If you have to mix the result, then you should call {@link CompletionPublisher#setComparator(Comparator)}
* with null first. Otherwise, your completion list may be corrupted. And in that case, you must do the sorting
* work by yourself and then add your items.
*
* @author Rosemoe
*/
public class IdentifierAutoComplete {
/**
* @deprecated Use {@link Comparators}
*/
@Deprecated
private final static Comparator<CompletionItem> COMPARATOR = (p1, p2) -> {
var cmp1 = asString(p1.desc).compareTo(asString(p2.desc));
if (cmp1 < 0) {
return 1;
} else if (cmp1 > 0) {
return -1;
}
return asString(p1.label).compareTo(asString(p2.label));
};
private String[] keywords;
private boolean keywordsAreLowCase;
private Map<String, Object> keywordMap;
public IdentifierAutoComplete() {
}
public IdentifierAutoComplete(String[] keywords) {
this();
setKeywords(keywords, true);
}
private static String asString(CharSequence str) {
return (str instanceof String ? (String) str : str.toString());
}
public void setKeywords(String[] keywords, boolean lowCase) {
this.keywords = keywords;
keywordsAreLowCase = lowCase;
var map = new HashMap<String, Object>();
if (keywords != null) {
for (var keyword : keywords) {
map.put(keyword, true);
}
}
keywordMap = map;
}
public String[] getKeywords() {
return keywords;
}
/**
* Make completion items for the given arguments.
* Provide the required arguments passed by {@link Language#requireAutoComplete(ContentReference, CharPosition, CompletionPublisher, Bundle)}
*
* @param prefix The prefix to make completions for.
*/
public void requireAutoComplete(
@NonNull ContentReference reference, @NonNull CharPosition position,
@NonNull String prefix, @NonNull CompletionPublisher publisher, @Nullable Identifiers userIdentifiers) {
var completionItemList = createCompletionItemList(prefix, userIdentifiers);
var comparator = Comparators.getCompletionItemComparator(reference, position, completionItemList);
publisher.addItems(completionItemList);
publisher.setComparator(comparator);
}
public List<CompletionItem> createCompletionItemList(
@NonNull String prefix, @Nullable Identifiers userIdentifiers
) {
int prefixLength = prefix.length();
if (prefixLength == 0) {
return Collections.emptyList();
}
var result = new ArrayList<CompletionItem>();
final var keywordArray = keywords;
final var lowCase = keywordsAreLowCase;
final var keywordMap = this.keywordMap;
var match = prefix.toLowerCase(Locale.ROOT);
if (keywordArray != null) {
if (lowCase) {
for (var kw : keywordArray) {
var fuzzyScore = Filters.fuzzyScoreGracefulAggressive(prefix,
prefix.toLowerCase(Locale.ROOT),
0, kw, kw.toLowerCase(Locale.ROOT), 0, FuzzyScoreOptions.getDefault());
var score = fuzzyScore == null ? -100 : fuzzyScore.getScore();
if (kw.startsWith(match) || score >= -20) {
result.add(new SimpleCompletionItem(kw, "Keyword", prefixLength, kw)
.kind(CompletionItemKind.Keyword));
}
}
} else {
for (var kw : keywordArray) {
var fuzzyScore = Filters.fuzzyScoreGracefulAggressive(prefix,
prefix.toLowerCase(Locale.ROOT),
0, kw, kw.toLowerCase(Locale.ROOT), 0, FuzzyScoreOptions.getDefault());
var score = fuzzyScore == null ? -100 : fuzzyScore.getScore();
if (kw.toLowerCase(Locale.ROOT).startsWith(match) || score >= -20) {
result.add(new SimpleCompletionItem(kw, "Keyword", prefixLength, kw)
.kind(CompletionItemKind.Keyword));
}
}
}
}
if (userIdentifiers != null) {
List<String> dest = new ArrayList<>();
userIdentifiers.filterIdentifiers(prefix, dest);
for (var word : dest) {
if (keywordMap == null || !keywordMap.containsKey(word))
result.add(new SimpleCompletionItem(word, "Identifier", prefixLength, word)
.kind(CompletionItemKind.Identifier));
}
}
return result;
}
/**
* Make completion items for the given arguments.
* Provide the required arguments passed by {@link Language#requireAutoComplete(ContentReference, CharPosition, CompletionPublisher, Bundle)}
*
* @param prefix The prefix to make completions for.
*/
@Deprecated
public void requireAutoComplete(
@NonNull String prefix, @NonNull CompletionPublisher publisher, @Nullable Identifiers userIdentifiers) {
publisher.setComparator(COMPARATOR);
publisher.setUpdateThreshold(0);
publisher.addItems(createCompletionItemList(prefix, userIdentifiers));
}
/**
* Interface for saving identifiers
*
* @author Rosemoe
* @see IdentifierAutoComplete.DisposableIdentifiers
*/
public interface Identifiers {
/**
* Filter identifiers with the given prefix
*
* @param prefix The prefix to filter
* @param dest Result list
*/
void filterIdentifiers(@NonNull String prefix, @NonNull List<String> dest);
}
/**
* This object is used only once. In other words, the object is generated every time the
* text changes, and is abandoned when next time the text change.
* <p>
* In this case, the frequent allocation of memory is unavoidable.
* And also, this class is not thread-safe.
*
* @author Rosemoe
*/
public static class DisposableIdentifiers implements Identifiers {
private final static Object SIGN = new Object();
private final List<String> identifiers = new ArrayList<>(128);
private HashMap<String, Object> cache;
public void addIdentifier(String identifier) {
if (cache == null) {
throw new IllegalStateException("begin() has not been called");
}
if (cache.put(identifier, SIGN) == SIGN) {
return;
}
identifiers.add(identifier);
}
/**
* Start building the identifiers
*/
public void beginBuilding() {
cache = new HashMap<>();
}
/**
* Free memory and finish building
*/
public void finishBuilding() {
cache.clear();
cache = null;
}
@Override
public void filterIdentifiers(@NonNull String prefix, @NonNull List<String> dest) {
for (String identifier : identifiers) {
var fuzzyScore = Filters.fuzzyScoreGracefulAggressive(prefix,
prefix.toLowerCase(Locale.ROOT),
0, identifier, identifier.toLowerCase(Locale.ROOT), 0, FuzzyScoreOptions.getDefault());
var score = fuzzyScore == null ? -100 : fuzzyScore.getScore();
if ((TextUtils.startsWith(identifier, prefix, true) || score >= -20) && !(prefix.length() == identifier.length() && TextUtils.startsWith(prefix, identifier, false))) {
dest.add(identifier);
}
}
}
}
public static class SyncIdentifiers implements Identifiers {
private final Lock lock = new ReentrantLock(true);
private final Map<String, MutableInt> identifierMap = new HashMap<>();
public void clear() {
lock.lock();
try {
identifierMap.clear();
} finally {
lock.unlock();
}
}
public void identifierIncrease(@NonNull String identifier) {
lock.lock();
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
identifierMap.computeIfAbsent(identifier, (x) -> new MutableInt(0)).increase();
} else {
var counter = identifierMap.get(identifier);
if (counter == null) {
counter = new MutableInt(0);
identifierMap.put(identifier, counter);
}
counter.increase();
}
} finally {
lock.unlock();
}
}
public void identifierDecrease(@NonNull String identifier) {
lock.lock();
try {
var count = identifierMap.get(identifier);
if (count != null) {
if (count.decreaseAndGet() <= 0) {
identifierMap.remove(identifier);
}
}
} finally {
lock.unlock();
}
}
@Override
public void filterIdentifiers(@NonNull String prefix, @NonNull List<String> dest) {
filterIdentifiers(prefix, dest, false);
}
public void filterIdentifiers(@NonNull String prefix, @NonNull List<String> dest, boolean waitForLock) {
boolean acquired;
if (waitForLock) {
lock.lock();
acquired = true;
} else {
try {
acquired = lock.tryLock(3, TimeUnit.MILLISECONDS);
} catch (InterruptedException e) {
acquired = false;
}
}
if (acquired) {
try {
for (String s : identifierMap.keySet()) {
var fuzzyScore = Filters.fuzzyScoreGracefulAggressive(prefix,
prefix.toLowerCase(Locale.ROOT),
0, s, s.toLowerCase(Locale.ROOT), 0, FuzzyScoreOptions.getDefault());
var score = fuzzyScore == null ? -100 : fuzzyScore.getScore();
if ((TextUtils.startsWith(s, prefix, true) || score >= -20) && !(prefix.length() == s.length() && TextUtils.startsWith(prefix, s, false))) {
dest.add(s);
}
}
} finally {
lock.unlock();
}
}
}
}
}

View File

@ -0,0 +1,130 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion;
import android.text.SpannableString;
import android.text.Spanned;
import android.text.style.ForegroundColorSpan;
import io.github.rosemoe.sora.text.TextUtils;
/**
* Utility class to provide some useful matching functions in generating completion.
*
* @author Rosemoe
*/
public class MatchHelper {
/**
* Color for matched text highlighting
*/
public int highlightColor = 0xff3f51b5;
/**
* Case in-sensitive
*/
public boolean ignoreCase = false;
/**
* Match case of first letter if ignoreCase=true
* <p>
* for {@link #startsWith(CharSequence, CharSequence)} only
*/
public boolean matchFirstCase = false;
public Spanned startsWith(CharSequence name, CharSequence pattern) {
return startsWith(name, pattern, matchFirstCase, ignoreCase);
}
public Spanned startsWith(CharSequence name, CharSequence pattern, boolean matchFirstCase, boolean ignoreCase) {
if (name.length() >= pattern.length()) {
final var len = pattern.length();
var matches = true;
for (int i = 0; i < len; i++) {
char a = name.charAt(i);
char b = pattern.charAt(i);
if (!(a == b || ((ignoreCase && (i != 0 || !matchFirstCase)) && Character.toLowerCase(a) == Character.toLowerCase(b)))) {
matches = false;
break;
}
}
if (matches) {
var spanned = new SpannableString(name);
spanned.setSpan(new ForegroundColorSpan(highlightColor), 0, len, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return spanned;
}
}
return null;
}
public Spanned contains(CharSequence name, CharSequence pattern) {
return contains(name, pattern, ignoreCase);
}
public Spanned contains(CharSequence name, CharSequence pattern, boolean ignoreCase) {
int index = TextUtils.indexOf(name, pattern, ignoreCase, 0);
if (index != -1) {
var spanned = new SpannableString(name);
spanned.setSpan(new ForegroundColorSpan(highlightColor), index, index + pattern.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return spanned;
}
return null;
}
/**
* Common sub-sequence
*/
public Spanned commonSub(CharSequence name, CharSequence pattern) {
return commonSub(name, pattern, ignoreCase);
}
/**
* Common sub-sequence
*/
public Spanned commonSub(CharSequence name, CharSequence pattern, boolean ignoreCase) {
if (name.length() >= pattern.length()) {
SpannableString spanned = null;
var len = pattern.length();
int j = 0;
for (int i = 0; i < len; i++) {
char p = pattern.charAt(i);
var matched = false;
for (; j < name.length() && !matched; j++) {
char s = name.charAt(j);
if (s == j || (ignoreCase && Character.toLowerCase(s) == Character.toLowerCase(p))) {
matched = true;
if (spanned == null) {
spanned = new SpannableString(name);
}
spanned.setSpan(new ForegroundColorSpan(highlightColor), j, j + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
if (!matched) {
return null;
}
}
return spanned;
}
return null;
}
}

View File

@ -0,0 +1,101 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
package io.github.rosemoe.sora.lang.completion
import android.content.res.Resources
import android.graphics.Canvas
import android.graphics.ColorFilter
import android.graphics.Paint
import android.graphics.PixelFormat
import android.graphics.drawable.Drawable
object SimpleCompletionIconDrawer {
@JvmStatic
@JvmOverloads
fun draw(kind: CompletionItemKind, circle: Boolean = true): Drawable {
return CircleDrawable(kind, circle)
}
}
internal class CircleDrawable(kind: CompletionItemKind, circle: Boolean) :
Drawable() {
private val mPaint: Paint
private val mTextPaint: Paint
private val mKind: CompletionItemKind
private val mCircle: Boolean
init {
mKind = kind
mCircle = circle
mPaint = Paint().apply {
isAntiAlias = true
color = kind.defaultDisplayBackgroundColor.toInt()
}
mTextPaint = Paint().apply {
color = -0x1
isAntiAlias = true
textSize = Resources.getSystem()
.displayMetrics.density * 14
textAlign = Paint.Align.CENTER
}
}
override fun draw(canvas: Canvas) {
val width = bounds.right.toFloat()
val height = bounds.bottom.toFloat()
if (mCircle) {
canvas.drawCircle(width / 2, height / 2, width / 2, mPaint)
} else {
canvas.drawRect(0f, 0f, width, height, mPaint)
}
canvas.save()
canvas.translate(width / 2f, height / 2f)
val textCenter = -(mTextPaint.descent() + mTextPaint.ascent()) / 2f
canvas.drawText(mKind.getDisplayChar(), 0f, textCenter, mTextPaint)
canvas.restore()
}
override fun setAlpha(p1: Int) {
mPaint.alpha = p1
mTextPaint.alpha = p1
}
override fun setColorFilter(colorFilter: ColorFilter?) {
mTextPaint.colorFilter = colorFilter
}
@Deprecated(
"Deprecated in Java",
ReplaceWith("PixelFormat.OPAQUE", "android.graphics.PixelFormat")
)
override fun getOpacity(): Int {
return PixelFormat.OPAQUE
}
}

View File

@ -0,0 +1,111 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.widget.CodeEditor;
/**
* SimpleCompletionItem represents a simple replace action for auto-completion.
* {@code prefixLength} is the length of prefix (text length you want to replace before the
* auto-completion position).
* {@code commitText} is the text you want to replace the original text.
* <p>
* Note that you must make sure the start position of replacement is on the same line as auto-completion's
* required position.
*
* @see CompletionItem
*/
public class SimpleCompletionItem extends CompletionItem {
public String commitText;
public SimpleCompletionItem(int prefixLength, String commitText) {
this(commitText, prefixLength, commitText);
}
public SimpleCompletionItem(CharSequence label, int prefixLength, String commitText) {
this(label, null, prefixLength, commitText);
}
public SimpleCompletionItem(CharSequence label, CharSequence desc, int prefixLength, String commitText) {
this(label, desc, null, prefixLength, commitText);
}
public SimpleCompletionItem(CharSequence label, CharSequence desc, Drawable icon, int prefixLength, String commitText) {
super(label, desc, icon);
this.commitText = commitText;
this.prefixLength = prefixLength;
}
@Override
public SimpleCompletionItem desc(CharSequence desc) {
super.desc(desc);
return this;
}
@Override
public SimpleCompletionItem icon(Drawable icon) {
super.icon(icon);
return this;
}
@Override
public SimpleCompletionItem label(CharSequence label) {
super.label(label);
return this;
}
@Override
public SimpleCompletionItem kind(CompletionItemKind kind) {
super.kind(kind);
if (this.icon == null) {
icon = SimpleCompletionIconDrawer.draw(kind);
}
return this;
}
public SimpleCompletionItem commit(int prefixLength, String commitText) {
this.prefixLength = prefixLength;
this.commitText = commitText;
return this;
}
@Override
public void performCompletion(@NonNull CodeEditor editor, @NonNull Content text, int line, int column) {
if (commitText == null) {
return;
}
if (prefixLength == 0) {
text.insert(line, column, commitText);
return;
}
text.replace(line, column - prefixLength, line, column, commitText);
}
}

View File

@ -0,0 +1,70 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.text.CharPosition;
import io.github.rosemoe.sora.text.Content;
import io.github.rosemoe.sora.widget.CodeEditor;
public class SimpleSnippetCompletionItem extends CompletionItem {
private final SnippetDescription snippet;
public SimpleSnippetCompletionItem(CharSequence label, SnippetDescription snippet) {
this(label, null, snippet);
}
public SimpleSnippetCompletionItem(CharSequence label, CharSequence desc, SnippetDescription snippet) {
this(label, desc, null, snippet);
}
public SimpleSnippetCompletionItem(CharSequence label, CharSequence desc, Drawable icon, SnippetDescription snippet) {
super(label, desc, icon);
this.snippet = snippet;
kind(CompletionItemKind.Snippet);
}
@Override
public void performCompletion(@NonNull CodeEditor editor, @NonNull Content text, @NonNull CharPosition position) {
int prefixLength = snippet.getSelectedLength();
var selectedText = text.subSequence(position.index - prefixLength, position.index).toString();
int actionIndex = position.index;
if (snippet.getDeleteSelected()) {
text.delete(position.index - prefixLength, position.index);
actionIndex -= prefixLength;
}
editor.getSnippetController().startSnippet(actionIndex, snippet.getSnippet(), selectedText);
}
@Override
public void performCompletion(@NonNull CodeEditor editor, @NonNull Content text, int line, int column) {
// do nothing
}
}

View File

@ -0,0 +1,37 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
package io.github.rosemoe.sora.lang.completion
import io.github.rosemoe.sora.lang.completion.snippet.CodeSnippet
/**
* @param selectedLength the text length before text, which will be deleted if deleteSelected = true
* @param snippet The code snippet. The snippet should pass [CodeSnippet.checkContent] checks
*/
data class SnippetDescription(
val selectedLength: Int,
val snippet: CodeSnippet,
val deleteSelected: Boolean = true
)

View File

@ -0,0 +1,237 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
@file:JvmName("Comparators")
package io.github.rosemoe.sora.lang.completion
import io.github.rosemoe.sora.text.CharPosition
import io.github.rosemoe.sora.text.ContentReference
import io.github.rosemoe.sora.util.CharCode
private fun CharSequence?.asString(): String {
return if (this == null) " " else if (this is String) this else this.toString()
}
fun defaultComparator(a: CompletionItem, b: CompletionItem): Int {
// check score
val p1Score = (a.extra as? SortedCompletionItem)?.score?.score ?: 0
val p2Score = (b.extra as? SortedCompletionItem)?.score?.score ?: 0
// if score biggest, it better similar to input text
if (p1Score < p2Score) {
return 1;
} else if (p1Score > p2Score) {
return -1;
}
var p1 = a.sortText.asString()
var p2 = b.sortText.asString()
// check with 'sortText'
if (p1 < p2) {
return -1;
} else if (p1 > p2) {
return 1;
}
p1 = a.label.asString()
p2 = b.label.asString()
// check with 'label'
if (p1 < p2) {
return -1;
} else if (p1 > p2) {
return 1;
}
// check with 'kind'
// if kind biggest, it better important
val kind = (b.kind?.value ?: 0) - (a.kind?.value ?: 0)
return kind
}
fun snippetUpComparator(a: CompletionItem, b: CompletionItem): Int {
if (a.kind != b.kind) {
if (a.kind == CompletionItemKind.Snippet) {
return 1;
} else if (b.kind == CompletionItemKind.Snippet) {
return -1;
}
}
return defaultComparator(a, b);
}
fun getCompletionItemComparator(
source: ContentReference,
cursorPosition: CharPosition,
completionItemList: Collection<CompletionItem>
): Comparator<CompletionItem> {
source.validateAccess()
val sourceLine = source.reference.getLine(cursorPosition.line)
var word = ""
var wordLow = ""
// picks a score function based on the number of
// items that we have to score/filter and based on the
// user-configuration
val scoreFn = FuzzyScorer { pattern,
lowPattern,
patternPos,
wordText,
lowWord,
wordPos,
options ->
if (sourceLine.length > 2000) {
fuzzyScore(pattern, lowPattern, patternPos, wordText, lowWord, wordPos, options)
} else {
fuzzyScoreGracefulAggressive(
pattern,
lowPattern,
patternPos,
wordText,
lowWord,
wordPos,
options
)
}
}
for (originItem in completionItemList) {
source.validateAccess()
val overwriteBefore = originItem.prefixLength
val wordLen = overwriteBefore
if (word.length != wordLen) {
word = if (wordLen == 0) "" else sourceLine.substring(
sourceLine.length - wordLen
)
wordLow = word.lowercase()
}
val item = SortedCompletionItem(originItem, FuzzyScore.default)
// when there is nothing to score against, don't
// event try to do. Use a const rank and rely on
// the fallback-sort using the initial sort order.
// use a score of `-100` because that is out of the
// bound of values `fuzzyScore` will return
if (wordLen == 0) {
// when there is nothing to score against, don't
// event try to do. Use a const rank and rely on
// the fallback-sort using the initial sort order.
// use a score of `-100` because that is out of the
// bound of values `fuzzyScore` will return
item.score = FuzzyScore.default
} else {
// skip word characters that are whitespace until
// we have hit the replace range (overwriteBefore)
var wordPos = 0;
while (wordPos < overwriteBefore) {
val ch = word[wordPos].code
if (ch == CharCode.Space || ch == CharCode.Tab) {
wordPos += 1;
} else {
break;
}
}
if (wordPos >= wordLen) {
// the wordPos at which scoring starts is the whole word
// and therefore the same rules as not having a word apply
item.score = FuzzyScore.default;
} else if (originItem.sortText?.isNotEmpty() == true) {
// when there is a `filterText` it must match the `word`.
// if it matches we check with the label to compute highlights
// and if that doesn't yield a result we have no highlights,
// despite having the match
// by default match `word` against the `label`
val match = scoreFn.calculateScore(
word,
wordLow,
wordPos,
originItem.sortText.asString(),
originItem.sortText.asString().lowercase(),
0,
FuzzyScoreOptions.default
) ?: continue; // NO match
// compareIgnoreCase(item.completion.filterText, item.textLabel) === 0
if (originItem.sortText === originItem.label) {
// filterText and label are actually the same -> use good highlights
item.score = match;
} else {
// re-run the scorer on the label in the hope of a result BUT use the rank
// of the filterText-match
val labelMatch = scoreFn.calculateScore(
word,
wordLow,
wordPos,
originItem.label.asString(),
originItem.label.asString().lowercase(),
0,
FuzzyScoreOptions.default
) ?: continue; // NO match
item.score = labelMatch
labelMatch.matches[0] = match.matches[0]
}
} else {
// by default match `word` against the `label`
val match = scoreFn.calculateScore(
word,
wordLow,
wordPos,
originItem.label.asString(),
originItem.label.asString().lowercase(),
0,
FuzzyScoreOptions.default
) ?: continue; // NO match
item.score = match;
}
originItem.extra = item
}
}
return Comparator { o1, o2 ->
snippetUpComparator(o1, o2)
}
}
data class SortedCompletionItem(
val completionItem: CompletionItem,
var score: FuzzyScore
)

View File

@ -0,0 +1,596 @@
/*******************************************************************************
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
******************************************************************************/
@file:JvmName("Filters")
package io.github.rosemoe.sora.lang.completion
import io.github.rosemoe.sora.util.CharCode
import io.github.rosemoe.sora.util.MyCharacter
// Migrating from vscode
// https://github.com/microsoft/vscode/blob/main/src/vs/base/common/filters.ts
private var maxLen = 32
private val minWordMatchPosArray = IntArray(2 * maxLen)
private val maxWordMatchPosArray = IntArray(2 * maxLen)
val diag = Array(maxLen) { IntArray(maxLen) } // the length of a contiguous diagonal match
val table = Array(maxLen) { IntArray(maxLen) }
val arrows = Array(maxLen) { IntArray(maxLen) }
object Arrow {
val Diag = 1
val Left = 2
val LeftLeft = 3
}
@JvmOverloads
fun isPatternInWord(
patternLow: String,
patternPos: Int,
patternLen: Int,
wordLow: String,
wordPos: Int,
wordLen: Int,
fillMinWordPosArr: Boolean = false
): Boolean {
var patternPos = patternPos
var wordPos = wordPos
while (patternPos < patternLen && wordPos < wordLen) {
if (patternLow[patternPos] == wordLow[wordPos]) {
if (fillMinWordPosArr) {
// Remember the min word position for each pattern position
minWordMatchPosArray[patternPos] = wordPos
}
patternPos += 1
}
wordPos += 1
}
return patternPos == patternLen // pattern must be exhausted
}
internal fun fillInMaxWordMatchPos(
patternLen: Int,
wordLen: Int,
patternStart: Int,
wordStart: Int,
patternLow: String,
wordLow: String
) {
var patternPos = patternLen - 1
var wordPos = wordLen - 1
while (patternPos >= patternStart && wordPos >= wordStart) {
if (patternLow[patternPos] == wordLow[wordPos]) {
maxWordMatchPosArray[patternPos] = wordPos
patternPos--
}
wordPos--
}
}
fun isUpperCaseAtPos(pos: Int, word: String, wordLow: String): Boolean {
return word[pos] != wordLow[pos]
}
fun isSeparatorAtPos(value: String, index: Int): Boolean {
if (index < 0 || index >= value.length) {
return false
}
return when (val code = value.codePointAt(index)) {
CharCode.Underline,
CharCode.Dash,
CharCode.Period,
CharCode.Space,
CharCode.Slash,
CharCode.Backslash,
CharCode.SingleQuote,
CharCode.DoubleQuote,
CharCode.Colon,
CharCode.DollarSign,
CharCode.LessThan,
CharCode.GreaterThan,
CharCode.OpenParen,
CharCode.CloseParen,
CharCode.OpenSquareBracket,
CharCode.CloseSquareBracket,
CharCode.OpenCurlyBrace,
CharCode.CloseCurlyBrace -> true
else -> MyCharacter.couldBeEmoji(code)
}
}
fun isWhitespaceAtPos(value: String, index: Int): Boolean {
if (index < 0 || index >= value.length) {
return false
}
return when (value[index].code) {
CharCode.Space,
CharCode.Tab -> true
else -> false
}
}
/**
* An array representing a fuzzy match.
*
* 0. the score
* 1. the offset at which matching started
* 2. `<match_pos_N>`
* 3. `<match_pos_1>`
* 4. `<match_pos_0>` etc
*/
class FuzzyScore(
var score: Int,
val wordStart: Int,
val matches: MutableList<Int> = mutableListOf()
) {
companion object {
/**
* No matches and value `-100`
*/
@JvmStatic
val default: FuzzyScore = FuzzyScore(-100, 0)
@JvmStatic
fun isDefault(score: FuzzyScore?): Boolean {
return score?.score == -100 && score.wordStart == 0
}
}
}
data class FuzzyScoreOptions(
val firstMatchCanBeWeak: Boolean,
val boostFullMatch: Boolean,
) {
companion object {
@JvmStatic
val default = FuzzyScoreOptions(boostFullMatch = true, firstMatchCanBeWeak = false)
}
}
fun interface FuzzyScorer {
fun calculateScore(
pattern: String,
lowPattern: String,
patternPos: Int,
word: String,
lowWord: String,
wordPos: Int,
options: FuzzyScoreOptions?
): FuzzyScore?
}
fun anyScore(
pattern: String,
lowPattern: String,
patternPos: Int,
word: String,
lowWord: String,
wordPos: Int,
): FuzzyScore {
val max = 13.coerceAtMost(pattern.length)
var patternPos = patternPos
while (patternPos < max) {
val result = fuzzyScore(
pattern, lowPattern, patternPos, word, lowWord, wordPos,
FuzzyScoreOptions(firstMatchCanBeWeak = false, boostFullMatch = true)
)
if (result != null) {
return result
}
patternPos++
}
return FuzzyScore(0, wordPos)
}
@JvmOverloads
fun fuzzyScore(
pattern: String,
patternLow: String,
patternStart: Int,
word: String,
wordLow: String,
wordStart: Int,
options: FuzzyScoreOptions? = FuzzyScoreOptions.default
): FuzzyScore? {
val patternLen = if (pattern.length > maxLen) maxLen else pattern.length
val wordLen = if (word.length > maxLen - 1) maxLen - 1 else word.length
if (patternStart >= patternLen || wordStart >= wordLen || (patternLen - patternStart) > (wordLen - wordStart)) {
return null
}
// Run a simple check if the characters of pattern occur
// (in order) at all in word. If that isn't the case we
// stop because no match will be possible
if (!isPatternInWord(patternLow, patternStart, patternLen, wordLow, wordStart, wordLen, true)) {
return null
}
// Find the max matching word position for each pattern position
// NOTE: the min matching word position was filled in above, in the `isPatternInWord` call
fillInMaxWordMatchPos(patternLen, wordLen, patternStart, wordStart, patternLow, wordLow)
var row = 1
var column = 1
var patternPos = patternStart
var wordPos: Int
val hasStrongFirstMatch = booleanArrayOf(false)
// There will be a match, fill in tables
while (patternPos < patternLen) {
// Reduce search space to possible matching word positions and to possible access from next row
val minWordMatchPos = minWordMatchPosArray[patternPos]
val maxWordMatchPos = maxWordMatchPosArray[patternPos]
val nextMaxWordMatchPos =
if (patternPos + 1 < patternLen) maxWordMatchPosArray[patternPos + 1] else wordLen
column = minWordMatchPos - wordStart + 1
wordPos = minWordMatchPos
while (wordPos < nextMaxWordMatchPos) {
var score = Int.MIN_VALUE
var canComeDiag = false
if (wordPos <= maxWordMatchPos) {
score = doScore(
pattern, patternLow, patternPos, patternStart,
word, wordLow, wordPos, wordLen, wordStart,
diag[row - 1][column - 1] == 0,
hasStrongFirstMatch
)
}
var diagScore = 0
if (score != Int.MAX_VALUE) {
canComeDiag = true
diagScore = score + table[row - 1][column - 1]
}
val canComeLeft = wordPos > minWordMatchPos
val leftScore =
if (canComeLeft) table[row][column - 1] + (if (diag[row][column - 1] > 0) -5 else 0) else 0 // penalty for a gap start
val canComeLeftLeft = wordPos > minWordMatchPos + 1 && diag[row][column - 1] > 0
val leftLeftScore =
if (canComeLeftLeft) table[row][column - 2] + (if (diag[row][column - 2] > 0) -5 else 0) else 0 // penalty for a gap start
if (canComeLeftLeft && (!canComeLeft || leftLeftScore >= leftScore) && (!canComeDiag || leftLeftScore >= diagScore)) {
// always prefer choosing left left to jump over a diagonal because that means a match is earlier in the word
table[row][column] = leftLeftScore
arrows[row][column] = Arrow.LeftLeft
diag[row][column] = 0
} else if (canComeLeft && (!canComeDiag || leftScore >= diagScore)) {
// always prefer choosing left since that means a match is earlier in the word
table[row][column] = leftScore
arrows[row][column] = Arrow.Left
diag[row][column] = 0
} else if (canComeDiag) {
table[row][column] = diagScore
arrows[row][column] = Arrow.Diag
diag[row][column] = diag[row - 1][column - 1] + 1
} else {
error("not possible")
}
column++
wordPos++
}
row++
patternPos++
}
if (!hasStrongFirstMatch[0] && options?.firstMatchCanBeWeak == false) {
return null
}
row--
column--
val result = FuzzyScore(table[row][column], wordStart)
var backwardsDiagLength = 0
var maxMatchColumn = 0
while (row >= 1) {
// Find the column where we go diagonally up
var diagColumn = column
do {
val arrow = arrows[row][diagColumn]
if (arrow == Arrow.LeftLeft) {
diagColumn -= 2
} else if (arrow == Arrow.Left) {
diagColumn -= 1
} else {
// found the diagonal
break
}
} while (diagColumn >= 1)
// Overturn the "forwards" decision if keeping the "backwards" diagonal would give a better match
if (
backwardsDiagLength > 1 // only if we would have a contiguous match of 3 characters
&& patternLow[patternStart + row - 1] == wordLow[wordStart + column - 1] // only if we can do a contiguous match diagonally
&& !isUpperCaseAtPos(
diagColumn + wordStart - 1,
word,
wordLow
) // only if the forwards chose diagonal is not an uppercase
&& backwardsDiagLength + 1 > diag[row][diagColumn] // only if our contiguous match would be longer than the "forwards" contiguous match
) {
diagColumn = column
}
if (diagColumn == column) {
// this is a contiguous match
backwardsDiagLength++
} else {
backwardsDiagLength = 1
}
if (maxMatchColumn == 0) {
// remember the last matched column
maxMatchColumn = diagColumn
}
row--
column = diagColumn - 1
result.matches.add(column)
}
if (wordLen == patternLen && options?.boostFullMatch == true) {
// the word matches the pattern with all characters!
// giving the score a total match boost (to come up ahead other words)
result.score += 2
}
// Add 1 penalty for each skipped character in the word
val skippedCharsCount = maxMatchColumn - patternLen
result.score -= skippedCharsCount
return result
}
internal fun doScore(
pattern: String, patternLow: String, patternPos: Int, patternStart: Int,
word: String, wordLow: String, wordPos: Int, wordLen: Int, wordStart: Int,
newMatchStart: Boolean,
outFirstMatchStrong: BooleanArray
): Int {
if (patternLow[patternPos] != wordLow[wordPos]) {
return Int.MIN_VALUE
}
var score = 1
var isGapLocation = false
if (wordPos == patternPos - patternStart) {
// common prefix: `foobar <-> foobaz`
// ^^^^^
score = if (pattern[patternPos] == word[wordPos]) 7 else 5
} else if (isUpperCaseAtPos(wordPos, word, wordLow) && (wordPos == 0 || !isUpperCaseAtPos(
wordPos - 1,
word,
wordLow
))
) {
// hitting upper-case: `foo <-> forOthers`
// ^^ ^
score = if (pattern[patternPos] == word[wordPos]) 7 else 5
isGapLocation = true
} else if (isSeparatorAtPos(wordLow, wordPos) && (wordPos == 0 || !isSeparatorAtPos(
wordLow,
wordPos - 1
))
) {
// hitting a separator: `. <-> foo.bar`
// ^
score = 5
} else if (isSeparatorAtPos(wordLow, wordPos - 1) || isWhitespaceAtPos(wordLow, wordPos - 1)) {
// post separator: `foo <-> bar_foo`
// ^^^
score = 5
isGapLocation = true
}
if (score > 1 && patternPos == patternStart) {
outFirstMatchStrong[0] = true
}
if (!isGapLocation) {
isGapLocation = isUpperCaseAtPos(wordPos, word, wordLow) || isSeparatorAtPos(
wordLow,
wordPos - 1
) || isWhitespaceAtPos(wordLow, wordPos - 1)
}
//
if (patternPos == patternStart) { // first character in pattern
if (wordPos > wordStart) {
// the first pattern character would match a word character that is not at the word start
// so introduce a penalty to account for the gap preceding this match
score -= if (isGapLocation) 3 else 5
}
} else {
if (newMatchStart) {
// this would be the beginning of a new match (i.e. there would be a gap before this location)
score += if (isGapLocation) 2 else 0
} else {
// this is part of a contiguous match, so give it a slight bonus, but do so only if it would not be a preferred gap location
score += if (isGapLocation) 0 else 1
}
}
if (wordPos + 1 == wordLen) {
// we always penalize gaps, but this gives unfair advantages to a match that would match the last character in the word
// so pretend there is a gap after the last character in the word to normalize things
score -= if (isGapLocation) 3 else 5
}
return score
}
fun fuzzyScoreGracefulAggressive(
pattern: String,
lowPattern: String,
patternPos: Int,
word: String,
lowWord: String,
wordPos: Int,
options: FuzzyScoreOptions?
): FuzzyScore? {
return fuzzyScoreWithPermutations(
pattern,
lowPattern,
patternPos,
word,
lowWord,
wordPos,
true,
options
)
}
fun fuzzyScoreGraceful(
pattern: String,
lowPattern: String,
patternPos: Int,
word: String,
lowWord: String,
wordPos: Int,
options: FuzzyScoreOptions?
): FuzzyScore? {
return fuzzyScoreWithPermutations(
pattern,
lowPattern,
patternPos,
word,
lowWord,
wordPos,
false,
options
)
}
internal fun fuzzyScoreWithPermutations(
pattern: String,
lowPattern: String,
patternPos: Int,
word: String,
lowWord: String,
wordPos: Int,
aggressive: Boolean,
options: FuzzyScoreOptions?
): FuzzyScore? {
var top = fuzzyScore(
pattern,
lowPattern,
patternPos,
word,
lowWord,
wordPos,
options ?: FuzzyScoreOptions.default
)
if (top != null && !aggressive) {
// when using the original pattern yield a result we`
// return it unless we are aggressive and try to find
// a better alignment, e.g. `cno` -> `^co^ns^ole` or `^c^o^nsole`.
return top
}
if (pattern.length >= 3) {
// When the pattern is long enough then try a few (max 7)
// permutations of the pattern to find a better match. The
// permutations only swap neighbouring characters, e.g
// `cnoso` becomes `conso`, `cnsoo`, `cnoos`.
val tries = 7.coerceAtMost(pattern.length - 1)
var movingPatternPos = patternPos + 1
while (movingPatternPos < tries) {
val newPattern = nextTypoPermutation(pattern, movingPatternPos)
if (newPattern != null) {
val candidate = fuzzyScore(
newPattern,
newPattern.lowercase(),
patternPos,
word,
lowWord,
wordPos,
options ?: FuzzyScoreOptions.default
)
if (candidate != null) {
candidate.score -= 3 // permutation penalty
if (top == null || candidate.score > top.score) {
top = candidate
}
}
}
movingPatternPos++
}
}
return top
}
internal fun nextTypoPermutation(pattern: String, patternPos: Int): String? {
if (patternPos + 1 >= pattern.length) {
return null
}
val swap1 = pattern[patternPos]
val swap2 = pattern[patternPos + 1]
if (swap1 == swap2) {
return null
}
return pattern.substring(0, patternPos) + swap2 + swap1 + pattern.substring(patternPos + 2)
}

View File

@ -0,0 +1,222 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion.snippet;
import androidx.annotation.NonNull;
import io.github.rosemoe.sora.text.TextUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;
import java.util.TreeSet;
public class CodeSnippet implements Cloneable {
private final List<SnippetItem> items;
private final List<PlaceholderDefinition> placeholders;
public CodeSnippet(@NonNull List<SnippetItem> items, @NonNull List<PlaceholderDefinition> placeholders) {
this.items = items;
this.placeholders = placeholders;
}
public boolean checkContent() {
int index = 0;
for (var item : items) {
if (item.getStartIndex() != index) {
return false;
}
if (item instanceof PlaceholderItem) {
if (!placeholders.contains(((PlaceholderItem) item).getDefinition())) {
return false;
}
}
index = item.getEndIndex();
}
var set = new TreeSet<Integer>();
for (var placeholder : placeholders) {
if (!set.contains(placeholder.getId())) {
set.add(placeholder.getId());
} else {
return false;
}
}
return true;
}
public List<SnippetItem> getItems() {
return items;
}
public List<PlaceholderDefinition> getPlaceholderDefinitions() {
return placeholders;
}
@NonNull
@Override
public CodeSnippet clone() {
var defs = new ArrayList<PlaceholderDefinition>(placeholders.size());
var map = new HashMap<PlaceholderDefinition, PlaceholderDefinition>();
for (PlaceholderDefinition placeholder : placeholders) {
var n = new PlaceholderDefinition(placeholder.getId(), placeholder.getChoices(), placeholder.getElements(), placeholder.getTransform());
defs.add(n);
map.put(placeholder, n);
}
var itemsClone = new ArrayList<SnippetItem>(items.size());
for (SnippetItem item : items) {
var n = item.clone();
itemsClone.add(n);
if (n instanceof PlaceholderItem) {
if (map.get(((PlaceholderItem) n).getDefinition()) != null) {
((PlaceholderItem) n).setDefinition(map.get(((PlaceholderItem) n).getDefinition()));
}
}
}
return new CodeSnippet(itemsClone, defs);
}
public static class Builder {
private final List<PlaceholderDefinition> definitions;
private List<SnippetItem> items = new ArrayList<>();
private int index;
public Builder() {
this(new ArrayList<>());
}
public Builder(@NonNull List<PlaceholderDefinition> definitions) {
this.definitions = definitions;
}
public Builder addPlainText(String text) {
if (!items.isEmpty() && items.get(items.size() - 1) instanceof PlainTextItem) {
// Merge plain texts
var item = (PlainTextItem) items.get(items.size() - 1);
item.setText(item.getText() + text);
item.setIndex(item.getStartIndex(), item.getEndIndex() + text.length());
index += text.length();
return this;
}
items.add(new PlainTextItem(text, index));
index += text.length();
return this;
}
public Builder addInterpolatedShell(String shell) {
items.add(new InterpolatedShellItem(shell, index));
return this;
}
public Builder addPlaceholder(int id) {
return addPlaceholder(id, (String) null);
}
public Builder addPlaceholder(int id, List<String> choices) {
if (choices.isEmpty()) {
return addPlaceholder(id);
} else if (choices.size() == 1) {
return addPlaceholder(id, choices.get(0));
}
addPlaceholder(id, choices.get(0));
PlaceholderDefinition def = null;
for (var definition : definitions) {
if (definition.getId() == id) {
def = definition;
break;
}
}
Objects.requireNonNull(def).setChoices(choices);
return this;
}
public Builder addPlaceholder(int id, Transform transform) {
if (transform == null) {
return addPlaceholder(id);
}
addPlaceholder(id);
PlaceholderDefinition def = null;
for (var definition : definitions) {
if (definition.getId() == id) {
def = definition;
break;
}
}
Objects.requireNonNull(def).setTransform(transform);
return this;
}
public Builder addPlaceholder(int id, String defaultValue) {
final var elements = new ArrayList<PlaceHolderElement>();
if (!android.text.TextUtils.isEmpty(defaultValue)) {
elements.add(new PlainPlaceholderElement(defaultValue));
}
return addComplexPlaceholder(id, elements);
}
public Builder addComplexPlaceholder(int id, List<PlaceHolderElement> elements) {
PlaceholderDefinition def = null;
for (var definition : definitions) {
if (definition.getId() == id) {
def = definition;
break;
}
}
if (def == null) {
def = new PlaceholderDefinition(id);
definitions.add(def);
}
def.getElements().addAll(elements);
var item = new PlaceholderItem(def, index);
items.add(item);
return this;
}
public Builder addVariable(String name, String defaultValue) {
items.add(new VariableItem(index, name, defaultValue));
return this;
}
public Builder addVariable(String name, Transform transform) {
items.add(new VariableItem(index, name, null, transform));
return this;
}
public Builder addVariable(VariableItem item) {
item.setIndex(index);
items.add(item);
return this;
}
public CodeSnippet build() {
return new CodeSnippet(items, definitions);
}
}
}

View File

@ -0,0 +1,67 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion.snippet;
public class ConditionalFormat implements FormatString {
public int group;
public String ifValue;
public String elseValue;
public String shorthand;
public int getGroup() {
return group;
}
public void setGroup(int group) {
this.group = group;
}
public String getIfValue() {
return ifValue;
}
public void setIfValue(String ifValue) {
this.ifValue = ifValue;
}
public String getElseValue() {
return elseValue;
}
public void setElseValue(String elseValue) {
this.elseValue = elseValue;
}
public void setShorthand(String shorthand) {
this.shorthand = shorthand;
}
public String getShorthand() {
return shorthand;
}
}

View File

@ -0,0 +1,29 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion.snippet;
public interface FormatString {
}

View File

@ -0,0 +1,54 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion.snippet;
import androidx.annotation.NonNull;
public class InterpolatedShellItem extends SnippetItem {
private String shellCode;
public InterpolatedShellItem(@NonNull String shellCode, int index) {
super(index);
this.shellCode = shellCode;
}
@NonNull
public String getShellCode() {
return shellCode;
}
public void setShellCode(@NonNull String shellCode) {
this.shellCode = shellCode;
}
@NonNull
@Override
public InterpolatedShellItem clone() {
var n = new InterpolatedShellItem(shellCode, getStartIndex());
n.setIndex(getStartIndex(), getEndIndex());
return n;
}
}

View File

@ -0,0 +1,27 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion.snippet;
public class NextUpperCaseFormat implements FormatString {
}

View File

@ -0,0 +1,42 @@
/*
* sora-editor - the awesome code editor for Android
* https://github.com/Rosemoe/sora-editor
* Copyright (C) 2020-2024 Rosemoe
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library 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
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301
* USA
*
* Please contact Rosemoe by email 2073412493@qq.com if you need
* additional information or have any questions
*/
package io.github.rosemoe.sora.lang.completion.snippet;
public class NoFormat implements FormatString {
private String text;
public NoFormat(String text) {
setText(text);
}
public void setText(String text) {
this.text = text;
}
public String getText() {
return text;
}
}

Some files were not shown because too many files have changed in this diff Show More