修复BUG,优化UI
This commit is contained in:
parent
ba74f5fb98
commit
79207f7c5b
|
@ -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
|
@ -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()
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>() {
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -8,5 +8,5 @@
|
|||
android:topRightRadius="16dp" />
|
||||
<stroke
|
||||
android:width="1dp"
|
||||
android:color="?attr/textFillColor" />
|
||||
android:color="?attr/colorOnSecondaryContainer" />
|
||||
</shape>
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -12,5 +12,4 @@
|
|||
android:id="@+id/pager"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</LinearLayout>
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
||||
|
|
21
assistantCoreLibrary/proguard-rules.pro
vendored
21
assistantCoreLibrary/proguard-rules.pro
vendored
|
@ -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
|
|
@ -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
|
||||
|
|
21
dialog/proguard-rules.pro
vendored
21
dialog/proguard-rules.pro
vendored
|
@ -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
|
|
@ -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
1
editor/.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/build
|
37
editor/build.gradle
Normal file
37
editor/build.gradle
Normal 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')
|
||||
}
|
27
editor/src/main/AndroidManifest.xml
Normal file
27
editor/src/main/AndroidManifest.xml
Normal 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>
|
68
editor/src/main/java/io/github/rosemoe/sora/I18nConfig.java
Normal file
68
editor/src/main/java/io/github/rosemoe/sora/I18nConfig.java
Normal 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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
131
editor/src/main/java/io/github/rosemoe/sora/event/Event.java
Normal file
131
editor/src/main/java/io/github/rosemoe/sora/event/Event.java
Normal 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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
37
editor/src/main/java/io/github/rosemoe/sora/event/Utils.kt
Normal file
37
editor/src/main/java/io/github/rosemoe/sora/event/Utils.kt
Normal 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)
|
|
@ -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)
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
||||
}
|
229
editor/src/main/java/io/github/rosemoe/sora/graphics/Paint.java
Normal file
229
editor/src/main/java/io/github/rosemoe/sora/graphics/Paint.java
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
195
editor/src/main/java/io/github/rosemoe/sora/lang/Language.java
Normal file
195
editor/src/main/java/io/github/rosemoe/sora/lang/Language.java
Normal 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();
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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++
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
)
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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
Loading…
Reference in New Issue
Block a user