Skip to content

Commit 0985c57

Browse files
committed
Update
1 parent 1785cae commit 0985c57

5 files changed

Lines changed: 120 additions & 133 deletions

File tree

app/src/main/java/com/omarea/krscript/ui/ListItemEditor.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,6 @@ class ListItemEditor(context: Context, config: EditorNode) : ListItemClickable(c
1111

1212
init {
1313
widgetView?.visibility = View.VISIBLE
14-
widgetView?.setImageDrawable(context.getDrawable(R.drawable.kr_script))
14+
widgetView?.setImageDrawable(context.getDrawable(R.drawable.icon_edit))
1515
}
1616
}
Lines changed: 117 additions & 131 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.tool.tree.ui
22

33
import android.graphics.Typeface
4+
import android.os.Handler
5+
import android.os.Looper
46
import android.text.Editable
57
import android.text.Spanned
68
import android.text.TextWatcher
@@ -13,15 +15,7 @@ import java.util.Locale
1315

1416
/**
1517
* Lightweight syntax highlighter for plain EditText.
16-
*
17-
* Supported by default:
18-
* - Shell scripts: .sh, .bash, .zsh, .ksh
19-
* - XML files: .xml
20-
* - build.prop / .prop / .properties
21-
*
22-
* Usage:
23-
* val highlighter = SyntaxHighlighterFactory.create("sh", editText)
24-
* highlighter?.attach()
18+
* Fully optimized using Debounce mechanism and Combined Named-Group Regex.
2519
*/
2620
object SyntaxHighlighterFactory {
2721
fun create(extension: String?, editText: EditText): SyntaxHighlighter? {
@@ -60,8 +54,13 @@ abstract class BaseSyntaxHighlighter(
6054
private var watcher: TextWatcher? = null
6155
private var isHighlighting = false
6256

57+
// Handler để điều phối bộ hoãn thực thi (Debounce)
58+
private val mainHandler = Handler(Looper.getMainLooper())
59+
private val highlightRunnable = Runnable { highlight() }
60+
6361
companion object {
64-
private const val MAX_HIGHLIGHT_LENGTH = 300_000
62+
private const val MAX_HIGHLIGHT_LENGTH = 150_000 // Giới hạn an toàn để tránh lag UI
63+
private const val DEBOUNCE_DELAY_MS = 150L // Đợi 150ms sau khi dừng gõ mới tô màu
6564
}
6665

6766
protected val editText: EditText?
@@ -78,14 +77,18 @@ abstract class BaseSyntaxHighlighter(
7877
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) = Unit
7978
override fun afterTextChanged(s: Editable?) {
8079
if (isHighlighting || s == null) return
81-
highlight()
80+
81+
// Hủy lịch trình cũ, lên lịch trình mới để gom các phím gõ nhanh lại
82+
mainHandler.removeCallbacks(highlightRunnable)
83+
mainHandler.postDelayed(highlightRunnable, DEBOUNCE_DELAY_MS)
8284
}
8385
}
8486
et.addTextChangedListener(watcher)
85-
highlight()
87+
highlight() // Chạy lần đầu tiên ngay khi gắn vào
8688
}
8789

8890
override fun detach() {
91+
mainHandler.removeCallbacks(highlightRunnable)
8992
val et = editText ?: return
9093
watcher?.let { et.removeTextChangedListener(it) }
9194
watcher = null
@@ -97,27 +100,30 @@ abstract class BaseSyntaxHighlighter(
97100
if (isHighlighting) return
98101

99102
if (text.length > MAX_HIGHLIGHT_LENGTH) {
100-
// File quá lớn: tô màu lại toàn bộ trên mỗi lần gõ sẽ làm treo UI thread (ANR).
101-
// Bỏ qua tô màu cú pháp cho các file lớn, người dùng vẫn gõ/sửa bình thường.
103+
// File quá lớn: Bỏ qua để giữ hiệu năng gõ mượt mà cho người dùng
102104
return
103105
}
104106

105107
isHighlighting = true
106108
try {
107109
applyHighlight(text)
108110
} catch (_: Throwable) {
109-
// Không để một lỗi ở bước tô màu cú pháp (vd. regex gặp nội dung bất thường)
110-
// làm crash toàn bộ app — bỏ qua lần tô màu này là đủ, nội dung gõ vẫn giữ nguyên.
111+
// Tránh crash ứng dụng nếu có lỗi phân tích cú pháp xảy ra ngoài ý muốn
111112
} finally {
112113
isHighlighting = false
113114
}
114115
}
115116

116117
protected fun clearSpans(text: Editable) {
117-
val spans = text.getSpans(0, text.length, Any::class.java)
118-
for (span in spans) {
119-
when (span) {
120-
is ForegroundColorSpan, is StyleSpan -> text.removeSpan(span)
118+
// Chỉ bóc tách đúng các loại Spans do hệ thống tô màu này định nghĩa
119+
val colorSpans = text.getSpans(0, text.length, ForegroundColorSpan::class.java)
120+
for (span in colorSpans) {
121+
text.removeSpan(span)
122+
}
123+
val styleSpans = text.getSpans(0, text.length, StyleSpan::class.java)
124+
for (span in styleSpans) {
125+
if (span.style == Typeface.BOLD) {
126+
text.removeSpan(span)
121127
}
122128
}
123129
}
@@ -176,65 +182,43 @@ class ShellSyntaxHighlighter(editText: EditText) : BaseSyntaxHighlighter(
176182
"true", "false", "type", "command", "eval", "let"
177183
)
178184

185+
private val shellRegex = Regex(
186+
"(?<COMMENT>(?m)#.*$)" +
187+
"|(?<STRING>\"(?:\\\\.|[^\"\\\\])*\"|'(?:[^']*)'|`(?:\\\\.|[^`\\\\])*`)" +
188+
"|(?<COMMAND>\\$\\((?:[^()]*|\\([^()]*\\))*\\))" +
189+
"|(?<VARIABLE>\\$\\{[A-Za-z_][A-Za-z0-9_]*[^}]*\\}|\\$[A-Za-z_][A-Za-z0-9_]*)" +
190+
"|(?<NUMBER>(?<![A-Za-z0-9_])(?:0x[0-9A-Fa-f]+|[0-9]+)(?![A-Za-z0-9_]))" +
191+
"|(?<WORD>(?<![A-Za-z0-9_])[A-Za-z_][A-Za-z0-9_]*(?![A-Za-z0-9_]))" +
192+
"|(?<PUNCTUATION>&&|\\|\\||\\||;|\\(|\\)|\\{|\\}|\\[\\[|\\]\\]|<|>)"
193+
)
194+
179195
override fun applyHighlight(text: Editable) {
180196
clearSpans(text)
181197
val s = text.toString()
182198

183-
// Comments
184-
Regex("""(?m)#.*$""").findAll(s).forEach {
185-
color(text, it.range.first, it.range.last + 1, commentColor())
186-
}
187-
188-
// Strings: double quotes, single quotes, backticks
189-
// Lưu ý: nhánh escape (\\.) và nhánh "mọi ký tự khác" phải KHÔNG được chồng lấn nhau
190-
// (cả hai cùng khớp được ký tự '\') — nếu chồng lấn, một chuỗi chưa đóng (thiếu dấu
191-
// đóng ") kèm nhiều dấu '\' sẽ khiến regex engine backtrack theo cấp số nhân và ném
192-
// StackOverflowError, làm crash app ngay khi người dùng đang gõ dở. Loại bỏ '\\' khỏi
193-
// lớp ký tự [^"\\] để hai nhánh không còn khớp trùng nhau nữa.
194-
Regex(""""(?:\\.|[^"\\])*"""").findAll(s).forEach {
195-
color(text, it.range.first, it.range.last + 1, stringColor())
196-
}
197-
Regex("""'(?:[^']*)'""").findAll(s).forEach {
198-
color(text, it.range.first, it.range.last + 1, stringColor())
199-
}
200-
Regex("""`(?:\\.|[^`\\])*`""").findAll(s).forEach {
201-
color(text, it.range.first, it.range.last + 1, stringColor())
202-
}
203-
204-
// Command substitution
205-
Regex("""\$\((?:[^()]*|\([^()]*\))*\)""").findAll(s).forEach {
206-
color(text, it.range.first, it.range.last + 1, builtinColor())
207-
}
208-
209-
// Variables
210-
Regex("""\$\{[A-Za-z_][A-Za-z0-9_]*[^}]*}""").findAll(s).forEach {
211-
color(text, it.range.first, it.range.last + 1, numberColor())
212-
}
213-
Regex("""\$[A-Za-z_][A-Za-z0-9_]*""").findAll(s).forEach {
214-
color(text, it.range.first, it.range.last + 1, numberColor())
215-
}
216-
217-
// Numbers
218-
Regex("""(?<![A-Za-z0-9_])(?:0x[0-9A-Fa-f]+|[0-9]+)(?![A-Za-z0-9_])""").findAll(s).forEach {
219-
color(text, it.range.first, it.range.last + 1, numberColor())
220-
}
199+
shellRegex.findAll(s).forEach { result ->
200+
val groups = result.groups
201+
val start = result.range.first
202+
val end = result.range.last + 1
221203

222-
// Keywords / builtins
223-
Regex("""(?<![A-Za-z0-9_])([A-Za-z_][A-Za-z0-9_]*)(?![A-Za-z0-9_])""").findAll(s).forEach {
224-
val word = it.value
225204
when {
226-
keywords.contains(word) -> {
227-
color(text, it.range.first, it.range.last + 1, keywordColor())
228-
bold(text, it.range.first, it.range.last + 1)
205+
groups["COMMENT"] != null -> color(text, start, end, commentColor())
206+
groups["STRING"] != null -> color(text, start, end, stringColor())
207+
groups["COMMAND"] != null -> color(text, start, end, builtinColor())
208+
groups["VARIABLE"] != null -> color(text, start, end, numberColor())
209+
groups["NUMBER"] != null -> color(text, start, end, numberColor())
210+
groups["PUNCTUATION"] != null -> color(text, start, end, punctuationColor())
211+
groups["WORD"] != null -> {
212+
val word = result.value
213+
if (keywords.contains(word)) {
214+
color(text, start, end, keywordColor())
215+
bold(text, start, end)
216+
} else if (builtins.contains(word)) {
217+
color(text, start, end, builtinColor())
218+
}
229219
}
230-
builtins.contains(word) -> color(text, it.range.first, it.range.last + 1, builtinColor())
231220
}
232221
}
233-
234-
// Punctuation/operators
235-
Regex("""&&|\|\||\||;|\(|\)|\{|\}|\[\[|\]\]|<|>""").findAll(s).forEach {
236-
color(text, it.range.first, it.range.last + 1, punctuationColor())
237-
}
238222
}
239223
}
240224

@@ -247,44 +231,47 @@ class XmlSyntaxHighlighter(editText: EditText) : BaseSyntaxHighlighter(
247231
numberColor = 0xFFAA66CC.toInt(),
248232
punctuationColor = 0xFF808080.toInt(),
249233
) {
234+
private val xmlRegex = Regex(
235+
"(?<COMMENT>(?s)<!--.*?-->)" +
236+
"|(?<CDATA>(?s)<!\\[CDATA\\[.*?\\]\\]>)" +
237+
"|(?<TAG></?[A-Za-z_][A-Za-z0-9_:\\-\\.]*)" +
238+
"|(?<ATTR>\\b[A-Za-z_][A-Za-z0-9_:\\-\\.]*(?=\\s*=))" +
239+
"|(?<VALUE>\"(?:\\\\.|[^\"\\\\])*\")" +
240+
"|(?<ENTITY>&(?:amp|lt|gt|apos|quot|#x?[0-9A-Fa-f]+);)" +
241+
"|(?<PUNCTUATION></?|/>|>[^<]*)"
242+
)
243+
250244
override fun applyHighlight(text: Editable) {
251245
clearSpans(text)
252246
val s = text.toString()
253247

254-
// XML comments
255-
Regex("""(?s)<!--.*?-->""").findAll(s).forEach {
256-
color(text, it.range.first, it.range.last + 1, commentColor())
257-
}
258-
259-
// CDATA
260-
Regex("""(?s)<!\[CDATA\[.*?\]\]>""").findAll(s).forEach {
261-
color(text, it.range.first, it.range.last + 1, stringColor())
262-
}
248+
xmlRegex.findAll(s).forEach { result ->
249+
val groups = result.groups
250+
val start = result.range.first
251+
val end = result.range.last + 1
263252

264-
// Tags
265-
Regex("""</?[A-Za-z_][A-Za-z0-9_:\-\.]*""").findAll(s).forEach {
266-
color(text, it.range.first, it.range.last + 1, keywordColor())
267-
bold(text, it.range.first, it.range.last + 1)
268-
}
269-
270-
// Attribute names (the part before =")
271-
Regex("""\b[A-Za-z_][A-Za-z0-9_:\-\.]*(?=\s*=)""").findAll(s).forEach {
272-
color(text, it.range.first, it.range.last + 1, builtinColor())
273-
}
274-
275-
// Attribute values (xem ghi chú về backtracking ở ShellSyntaxHighlighter phía trên)
276-
Regex(""""(?:\\.|[^"\\])*"""").findAll(s).forEach {
277-
color(text, it.range.first, it.range.last + 1, stringColor())
278-
}
279-
280-
// Entities
281-
Regex("""&(?:amp|lt|gt|apos|quot|#x?[0-9A-Fa-f]+);""").findAll(s).forEach {
282-
color(text, it.range.first, it.range.last + 1, numberColor())
283-
}
284-
285-
// Angle brackets / punctuation
286-
Regex("""</?|/>|>""").findAll(s).forEach {
287-
color(text, it.range.first, it.range.last + 1, punctuationColor())
253+
when {
254+
groups["COMMENT"] != null -> color(text, start, end, commentColor())
255+
groups["CDATA"] != null -> color(text, start, end, stringColor())
256+
groups["TAG"] != null -> {
257+
color(text, start, end, keywordColor())
258+
bold(text, start, end)
259+
}
260+
groups["ATTR"] != null -> color(text, start, end, builtinColor())
261+
groups["VALUE"] != null -> color(text, start, end, stringColor())
262+
groups["ENTITY"] != null -> color(text, start, end, numberColor())
263+
groups["PUNCTUATION"] != null -> {
264+
// Chỉ tô màu ký tự phân tách thực tế (<, >, />, </) không bao gồm phần text trần bên ngoài
265+
val rawValue = result.value
266+
val punctEnd = when {
267+
rawValue.startsWith("</") -> start + 2
268+
rawValue.startsWith("/>") -> start + 2
269+
rawValue.startsWith("<") || rawValue.startsWith(">") -> start + 1
270+
else -> end
271+
}
272+
color(text, start, punctEnd, punctuationColor())
273+
}
274+
}
288275
}
289276
}
290277
}
@@ -298,39 +285,38 @@ class PropSyntaxHighlighter(editText: EditText) : BaseSyntaxHighlighter(
298285
numberColor = 0xFFAA66CC.toInt(),
299286
punctuationColor = 0xFF808080.toInt(),
300287
) {
288+
private val propRegex = Regex(
289+
"(?<COMMENT>(?m)^\\s*[#!].*$)" +
290+
"|(?<KEYVALUE>(?m)^\\s*([A-Za-z0-9_.-]+)(\\s*=))" +
291+
"|(?<STRING>\"(?:\\\\.|[^\"\\\\])*\")" +
292+
"|(?<NUMBER>(?<![A-Za-z0-9_])(?:0x[0-9A-Fa-f]+|[0-9]+)(?![A-Za-z0-9_]))" +
293+
"|(?<SEPARATOR>=)"
294+
)
295+
301296
override fun applyHighlight(text: Editable) {
302297
clearSpans(text)
303298
val s = text.toString()
304299

305-
// Comments
306-
Regex("""(?m)^\s*[#!].*$""").findAll(s).forEach {
307-
color(text, it.range.first, it.range.last + 1, commentColor())
308-
}
300+
propRegex.findAll(s).forEach { result ->
301+
val groups = result.groups
302+
val start = result.range.first
303+
val end = result.range.last + 1
309304

310-
// Key = value
311-
Regex("""(?m)^\s*([A-Za-z0-9_.-]+)(\s*=)""").findAll(s).forEach {
312-
val line = it.value
313-
val keyEnd = line.indexOf('=')
314-
if (keyEnd > 0) {
315-
color(text, it.range.first, it.range.first + keyEnd, keywordColor())
316-
color(text, it.range.first + keyEnd, it.range.first + keyEnd + 1, punctuationColor())
317-
bold(text, it.range.first, it.range.first + keyEnd)
305+
when {
306+
groups["COMMENT"] != null -> color(text, start, end, commentColor())
307+
groups["KEYVALUE"] != null -> {
308+
val line = result.value
309+
val keyEnd = line.indexOf('=')
310+
if (keyEnd > 0) {
311+
color(text, start, start + keyEnd, keywordColor())
312+
color(text, start + keyEnd, start + keyEnd + 1, punctuationColor())
313+
bold(text, start, start + keyEnd)
314+
}
315+
}
316+
groups["STRING"] != null -> color(text, start, end, stringColor())
317+
groups["NUMBER"] != null -> color(text, start, end, numberColor())
318+
groups["SEPARATOR"] != null -> color(text, start, end, punctuationColor())
318319
}
319320
}
320-
321-
// Quoted values (xem ghi chú về backtracking ở ShellSyntaxHighlighter phía trên)
322-
Regex(""""(?:\\.|[^"\\])*"""").findAll(s).forEach {
323-
color(text, it.range.first, it.range.last + 1, stringColor())
324-
}
325-
326-
// Numbers
327-
Regex("""(?<![A-Za-z0-9_])(?:0x[0-9A-Fa-f]+|[0-9]+)(?![A-Za-z0-9_])""").findAll(s).forEach {
328-
color(text, it.range.first, it.range.last + 1, numberColor())
329-
}
330-
331-
// Separators
332-
Regex("""=""").findAll(s).forEach {
333-
color(text, it.range.first, it.range.last + 1, punctuationColor())
334-
}
335321
}
336-
}
322+
}
1.57 KB
Loading
392 Bytes
Loading

app/src/main/res/layout/dialog_loading.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@
3333
<Button
3434
android:id="@+id/dialog_cancel_button"
3535
style="@style/dialogWarningBtn"
36-
android:layout_weight="1"
3736
android:visibility="gone"
37+
android:layout_width="wrap_content"
38+
android:layout_height="wrap_content"
3839
android:layout_gravity="center_horizontal"
3940
android:text="@string/btn_cancel" />
4041

0 commit comments

Comments
 (0)