@@ -35,7 +35,10 @@ import com.tool.tree.ui.SyntaxHighlighterFactory
3535import kotlinx.coroutines.Dispatchers
3636import kotlinx.coroutines.launch
3737import kotlinx.coroutines.withContext
38+ import org.json.JSONArray
39+ import org.json.JSONObject
3840import java.io.File
41+ import java.security.MessageDigest
3942
4043class TextEditorActivity : AppCompatActivity () {
4144
@@ -47,6 +50,8 @@ class TextEditorActivity : AppCompatActivity() {
4750 private const val EXTRA_DIR = " dir"
4851 private const val UNDO_HISTORY_LIMIT = 200
4952 private const val UNDO_DEBOUNCE_MS = 700L
53+ private const val UNDO_CACHE_DEBOUNCE_MS = 400L
54+ private const val UNDO_CACHE_DIR = " editor_undo_cache"
5055
5156 private const val EXTRA_PLACEHOLDER = " placeholder"
5257
@@ -56,12 +61,6 @@ class TextEditorActivity : AppCompatActivity() {
5661 " py" to " python"
5762 )
5863
59- // Cho phép dòng đầu tiên của nội dung khai báo ngôn ngữ thực tế của file, ví dụ:
60- // "# python", "#!/usr/bin/env python", hoặc shebang trỏ tới binary Termux
61- // được bundle riêng trong thư mục dữ liệu của app (com.tool.tree), ví dụ:
62- // #!/data/data/com.tool.tree/files/home/termux/bin/python
63- // #!/data/data/com.tool.tree/files/home/bin/bash
64- // (vẫn hỗ trợ cả dạng /data/user/<id>/... phòng khi thiết bị multi-user)
6564 private val FIRST_LINE_LANG_PATTERN = Regex (
6665 """ ^#!?\s*(?:/usr/bin/env\s+)?(?:/data/(?:data|user/\d+)/com\.tool\.tree/\S*/)?(python3?|py|bash|sh|shell)\b""" ,
6766 RegexOption .IGNORE_CASE
@@ -120,6 +119,7 @@ class TextEditorActivity : AppCompatActivity() {
120119 private var redoButton: ImageButton ? = null
121120 private val undoHandler = Handler (Looper .getMainLooper())
122121 private val commitPendingUndoRunnable = Runnable { commitPendingUndoSnapshot() }
122+ private val persistUndoCacheRunnable = Runnable { persistUndoCacheToDisk() }
123123
124124 override fun onCreate (savedInstanceState : Bundle ? ) {
125125 super .onCreate(savedInstanceState)
@@ -159,12 +159,107 @@ class TextEditorActivity : AppCompatActivity() {
159159 loadFileContent()
160160 }
161161
162+ override fun onPause () {
163+ commitPendingUndoSnapshot()
164+ persistUndoCacheToDisk()
165+ super .onPause()
166+ }
167+
162168 override fun onDestroy () {
163169 syntaxHighlighter?.detach()
164170 undoHandler.removeCallbacksAndMessages(null )
165171 super .onDestroy()
166172 }
167173
174+ // ---------------------------------------------------------------------
175+ // Cache undo/redo xuống đĩa: cho phép khôi phục lịch sử undo/redo nếu
176+ // Activity bị hệ thống hủy (xoay màn hình, thiếu RAM) hoặc người dùng
177+ // rời màn hình soạn thảo mà chưa lưu, rồi quay lại mở đúng file đó.
178+ // Cache chỉ được khôi phục khi nội dung file tại thời điểm mở lại trùng
179+ // khớp với nội dung đã có lúc ghi cache (tránh áp lịch sử undo sai lệch
180+ // lên một nội dung file đã thay đổi bởi nơi khác).
181+ // ---------------------------------------------------------------------
182+
183+ private fun undoCacheFile (): File {
184+ val digest = MessageDigest .getInstance(" SHA-256" )
185+ .digest(absoluteFilePath.toByteArray(Charsets .UTF_8 ))
186+ .joinToString(" " ) { " %02x" .format(it) }
187+ val dir = File (cacheDir, UNDO_CACHE_DIR ).apply { mkdirs() }
188+ return File (dir, " $digest .json" )
189+ }
190+
191+ private fun snapshotToJson (snapshot : EditorSnapshot ) = JSONObject ().apply {
192+ put(" text" , snapshot.text)
193+ put(" cursor" , snapshot.cursor)
194+ }
195+
196+ private fun jsonToSnapshot (json : JSONObject ) =
197+ EditorSnapshot (json.optString(" text" , " " ), json.optInt(" cursor" , 0 ))
198+
199+ private fun scheduleUndoCachePersist () {
200+ undoHandler.removeCallbacks(persistUndoCacheRunnable)
201+ undoHandler.postDelayed(persistUndoCacheRunnable, UNDO_CACHE_DEBOUNCE_MS )
202+ }
203+
204+ private fun persistUndoCacheToDisk () {
205+ undoHandler.removeCallbacks(persistUndoCacheRunnable)
206+ if (! ::binding.isInitialized) return
207+ val baseContent = savedContent
208+ val undoSnapshot = undoStack.toList()
209+ val redoSnapshot = redoStack.toList()
210+ val pending = pendingUndoSnapshot
211+ lifecycleScope.launch(Dispatchers .IO ) {
212+ try {
213+ if (undoSnapshot.isEmpty() && redoSnapshot.isEmpty() && pending == null ) {
214+ undoCacheFile().delete()
215+ return @launch
216+ }
217+ val root = JSONObject ().apply {
218+ put(" baseContent" , baseContent)
219+ put(" undo" , JSONArray ().apply { undoSnapshot.forEach { put(snapshotToJson(it)) } })
220+ put(" redo" , JSONArray ().apply { redoSnapshot.forEach { put(snapshotToJson(it)) } })
221+ pending?.let { put(" pending" , snapshotToJson(it)) }
222+ }
223+ undoCacheFile().writeText(root.toString())
224+ } catch (_: Exception ) {
225+ }
226+ }
227+ }
228+
229+ /* * Phải được gọi từ Main thread, sau khi [savedContent] đã được thiết lập. */
230+ private fun restoreUndoCacheFromDisk () {
231+ try {
232+ val file = undoCacheFile()
233+ if (! file.exists()) return
234+ val root = JSONObject (file.readText())
235+ val baseContent = if (root.has(" baseContent" )) root.getString(" baseContent" ) else null
236+ if (baseContent == null || baseContent != savedContent) {
237+ file.delete()
238+ return
239+ }
240+
241+ undoStack.clear()
242+ redoStack.clear()
243+ root.optJSONArray(" undo" )?.let { arr ->
244+ for (i in 0 until arr.length()) undoStack.addLast(jsonToSnapshot(arr.getJSONObject(i)))
245+ }
246+ root.optJSONArray(" redo" )?.let { arr ->
247+ for (i in 0 until arr.length()) redoStack.addLast(jsonToSnapshot(arr.getJSONObject(i)))
248+ }
249+ pendingUndoSnapshot = root.optJSONObject(" pending" )?.let { jsonToSnapshot(it) }
250+ refreshUndoRedoButtons()
251+ } catch (_: Exception ) {
252+ }
253+ }
254+
255+ private fun clearUndoCache () {
256+ undoHandler.removeCallbacks(persistUndoCacheRunnable)
257+ try {
258+ undoCacheFile().delete()
259+ } catch (_: Exception ) {
260+ }
261+ }
262+
168263 private fun setupKeyboardInsets () {
169264 mainListBaseBottomMargin = (binding.mainList.layoutParams as ViewGroup .MarginLayoutParams ).bottomMargin
170265 val extraGapPx = (24 * resources.displayMetrics.density).toInt()
@@ -224,12 +319,6 @@ class TextEditorActivity : AppCompatActivity() {
224319
225320 private fun fileExtension () = File (absoluteFilePath).name.substringAfterLast(' .' , " " ).lowercase()
226321
227- /* *
228- * Đọc dòng đầu tiên của nội dung đang soạn thảo để xem có khai báo ngôn ngữ
229- * dạng "# python", "# bash", "#!/usr/bin/env python"... hay không. Nếu có,
230- * ngôn ngữ này sẽ được ưu tiên hơn phần mở rộng file thực tế (vd: file.sh
231- * nhưng dòng đầu ghi "# python" thì vẫn coi là python).
232- */
233322 private fun detectFirstLineLanguageOverride (): String? {
234323 val firstLine = binding.editorContent.text?.toString()
235324 ?.lineSequence()?.firstOrNull()?.trim()
@@ -246,10 +335,6 @@ class TextEditorActivity : AppCompatActivity() {
246335 syntaxHighlighter?.detach()
247336 val override = detectFirstLineLanguageOverride()
248337 lastLanguageOverride = override
249- // SyntaxHighlighterFactory chọn bộ tô màu dựa trên phần mở rộng của đường dẫn
250- // truyền vào. Khi có override từ dòng đầu, ta đưa vào một "đường dẫn ảo" có
251- // đúng phần mở rộng tương ứng để factory chọn đúng ngôn ngữ, còn việc đọc/ghi
252- // file vẫn dùng absoluteFilePath thật như bình thường.
253338 val highlightPath = when (override ) {
254339 " python" -> " $absoluteFilePath .__lang_override.py"
255340 " bash" -> " $absoluteFilePath .__lang_override.bash"
@@ -262,10 +347,6 @@ class TextEditorActivity : AppCompatActivity() {
262347 )?.apply { attach() }
263348 }
264349
265- /* *
266- * Gọi lại khi nội dung dòng đầu tiên thay đổi trong lúc soạn thảo, để cập nhật
267- * lại màu cú pháp và trạng thái nút "Run" (test) cho phù hợp ngôn ngữ mới.
268- */
269350 private fun refreshLanguageOverrideIfChanged () {
270351 val current = detectFirstLineLanguageOverride()
271352 if (current != lastLanguageOverride) {
@@ -344,6 +425,7 @@ class TextEditorActivity : AppCompatActivity() {
344425 undoHandler.postDelayed(commitPendingUndoRunnable, UNDO_DEBOUNCE_MS )
345426 refreshUndoRedoButtons()
346427 refreshLanguageOverrideIfChanged()
428+ scheduleUndoCachePersist()
347429 }
348430 })
349431 }
@@ -357,6 +439,7 @@ class TextEditorActivity : AppCompatActivity() {
357439 if (undoStack.size >= UNDO_HISTORY_LIMIT ) undoStack.removeFirst()
358440 undoStack.addLast(snapshot)
359441 refreshUndoRedoButtons()
442+ scheduleUndoCachePersist()
360443 }
361444
362445 private inline fun withHighlightSuspended (block : () -> Unit ) {
@@ -380,6 +463,7 @@ class TextEditorActivity : AppCompatActivity() {
380463 redoStack.addLast(EditorSnapshot (currentText, currentCursor))
381464 applyHistorySnapshot(target)
382465 refreshUndoRedoButtons()
466+ persistUndoCacheToDisk()
383467 }
384468
385469 private fun performRedo () {
@@ -395,6 +479,7 @@ class TextEditorActivity : AppCompatActivity() {
395479 undoStack.addLast(EditorSnapshot (currentText, currentCursor))
396480 applyHistorySnapshot(target)
397481 refreshUndoRedoButtons()
482+ persistUndoCacheToDisk()
398483 }
399484
400485 private fun applyHistorySnapshot (snapshot : EditorSnapshot ) {
@@ -454,6 +539,10 @@ class TextEditorActivity : AppCompatActivity() {
454539 pendingUndoSnapshot = null
455540 undoStack.clear()
456541 redoStack.clear()
542+
543+ // Nếu có cache undo/redo được lưu từ phiên trước cho đúng nội dung
544+ // file này, khôi phục lại để người dùng có thể undo/redo tiếp.
545+ restoreUndoCacheFromDisk()
457546 refreshUndoRedoButtons()
458547
459548 binding.editorContent.hint = when {
@@ -518,6 +607,7 @@ class TextEditorActivity : AppCompatActivity() {
518607 commitPendingUndoSnapshot()
519608
520609 if (! hasUnsavedChanges()) {
610+ clearUndoCache()
521611 finish()
522612 return
523613 }
@@ -532,6 +622,7 @@ class TextEditorActivity : AppCompatActivity() {
532622 }
533623 }),
534624 onCancel = DialogHelper .DialogButton (getString(R .string.editor_discard), Runnable {
625+ clearUndoCache()
535626 finish()
536627 })
537628 )
@@ -584,6 +675,10 @@ class TextEditorActivity : AppCompatActivity() {
584675 isSaving = false
585676 if (success) {
586677 savedContent = content
678+ // Cập nhật lại "baseContent" của cache theo nội dung vừa lưu,
679+ // để lịch sử undo/redo hiện có vẫn còn dùng được nếu quay
680+ // lại chỉnh sửa tiếp mà chưa đóng màn hình.
681+ persistUndoCacheToDisk()
587682 }
588683 if (showToast) {
589684 Toast .makeText(
@@ -660,4 +755,4 @@ class TextEditorActivity : AppCompatActivity() {
660755 ).apply { isCancelable = false }
661756 .show(supportFragmentManager, " editor-run-test" )
662757 }
663- }
758+ }
0 commit comments