Skip to content

Commit 60a9593

Browse files
ADFA-4376: Update file tree with plugin files (#1501)
* feat(ADFA-4376): Dispatch file tree events for plugin files * feat(ADFA-4376): Update file tree with plugin files * refactor(ADFA-4376): Reload before recursing
1 parent 8e33ec6 commit 60a9593

4 files changed

Lines changed: 132 additions & 10 deletions

File tree

‎app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,13 @@ import androidx.appcompat.app.ActionBarDrawerToggle
5555
import androidx.collection.MutableIntIntMap
5656
import androidx.core.content.ContextCompat
5757
import androidx.core.graphics.Insets
58-
import androidx.core.hardware.display.DisplayManagerCompat
5958
import androidx.core.view.GravityCompat
6059
import androidx.core.view.ViewCompat
6160
import androidx.core.view.WindowInsetsCompat
6261
import androidx.core.view.updateLayoutParams
6362
import androidx.core.view.updatePadding
6463
import androidx.fragment.app.Fragment
64+
import androidx.fragment.app.FragmentManager
6565
import androidx.lifecycle.Lifecycle
6666
import androidx.lifecycle.lifecycleScope
6767
import androidx.lifecycle.repeatOnLifecycle
@@ -117,7 +117,6 @@ import com.itsaky.androidide.preferences.internal.BuildPreferences
117117
import com.itsaky.androidide.preferences.internal.GeneralPreferences
118118
import com.itsaky.androidide.projects.IProjectManager
119119
import com.itsaky.androidide.projects.ProjectManagerImpl
120-
import com.itsaky.androidide.resources.R as ResR
121120
import com.itsaky.androidide.services.debug.DebuggerService
122121
import com.itsaky.androidide.tasks.cancelIfActive
123122
import com.itsaky.androidide.ui.CodeEditorView
@@ -792,6 +791,8 @@ abstract class BaseEditorActivity :
792791
closeKeyboard()
793792
// Dismiss autocomplete and other editor windows
794793
provideCurrentEditor()?.editor?.ensureWindowsDismissed()
794+
// Reflect files changed while the drawer was closed (e.g. by a plugin).
795+
getFileTreeFragment()?.refreshExpandedNodes()
795796
}
796797
}
797798

@@ -1136,15 +1137,22 @@ abstract class BaseEditorActivity :
11361137
}
11371138

11381139
open fun getFileTreeFragment(): FileTreeFragment? {
1139-
if (filesTreeFragment == null) {
1140-
filesTreeFragment =
1141-
supportFragmentManager.findFragmentByTag(
1142-
FileTreeFragment.TAG,
1143-
) as FileTreeFragment?
1140+
if (filesTreeFragment?.isAdded != true) {
1141+
filesTreeFragment = findFileTreeFragment(supportFragmentManager)
11441142
}
11451143
return filesTreeFragment
11461144
}
11471145

1146+
private fun findFileTreeFragment(manager: FragmentManager): FileTreeFragment? {
1147+
for (fragment in manager.fragments) {
1148+
if (fragment is FileTreeFragment) {
1149+
return fragment
1150+
}
1151+
findFileTreeFragment(fragment.childFragmentManager)?.let { return it }
1152+
}
1153+
return null
1154+
}
1155+
11481156
fun doSetStatus(
11491157
text: CharSequence,
11501158
@GravityInt gravity: Int = Gravity.CENTER,

‎app/src/main/java/com/itsaky/androidide/fragments/sidebar/FileTreeFragment.kt‎

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import com.itsaky.androidide.dnd.FileDragResult
3737
import com.itsaky.androidide.dnd.FileDragStarter
3838
import com.itsaky.androidide.eventbus.events.filetree.FileClickEvent
3939
import com.itsaky.androidide.eventbus.events.filetree.FileLongClickEvent
40+
import com.itsaky.androidide.eventbus.events.filetree.PluginFilesChangedEvent
4041
import com.itsaky.androidide.events.CollapseTreeNodeRequestEvent
4142
import com.itsaky.androidide.events.ExpandTreeNodeRequestEvent
4243
import com.itsaky.androidide.events.ListProjectFilesRequestEvent
@@ -66,6 +67,15 @@ class FileTreeFragment : BottomSheetDialogFragment(), TreeNodeClickListener,
6667
private var binding: LayoutEditorFileTreeBinding? = null
6768
private var fileTreeView: AndroidTreeView? = null
6869

70+
// Root of the current tree; used to walk expanded nodes on refresh.
71+
private var treeRoot: TreeNode? = null
72+
73+
private val pluginRefreshRunnable = Runnable {
74+
if (isVisible && context != null) {
75+
refreshExpandedNodes()
76+
}
77+
}
78+
6979
private val viewModel by viewModels<FileTreeViewModel>(ownerProducer = { requireActivity() })
7080
private val fileDragStarter by lazy(LazyThreadSafetyMode.NONE) {
7181
FileDragStarter(requireContext())
@@ -112,10 +122,12 @@ class FileTreeFragment : BottomSheetDialogFragment(), TreeNodeClickListener,
112122
super.onDestroyView()
113123
EventBus.getDefault().unregister(this)
114124

125+
binding?.root?.removeCallbacks(pluginRefreshRunnable)
115126
saveTreeState()
116127

117128
binding = null
118129
fileTreeView = null
130+
treeRoot = null
119131
_dropController = null
120132
}
121133

@@ -271,6 +283,18 @@ class FileTreeFragment : BottomSheetDialogFragment(), TreeNodeClickListener,
271283
listProjectFiles()
272284
}
273285

286+
@Suppress("unused", "UNUSED_PARAMETER")
287+
@Subscribe(threadMode = MAIN)
288+
fun onPluginFilesChanged(event: PluginFilesChangedEvent) {
289+
if (!isVisible || context == null) {
290+
return
291+
}
292+
val root = binding?.root ?: return
293+
// Debounce bursts of writes into a single refresh.
294+
root.removeCallbacks(pluginRefreshRunnable)
295+
root.postDelayed(pluginRefreshRunnable, PLUGIN_REFRESH_DEBOUNCE_MS)
296+
}
297+
274298
@Suppress("unused")
275299
@Subscribe(threadMode = MAIN)
276300
fun onGetExpandTreeNodeRequest(event: ExpandTreeNodeRequestEvent) {
@@ -309,6 +333,7 @@ class FileTreeFragment : BottomSheetDialogFragment(), TreeNodeClickListener,
309333
val projectRoot = TreeNode.root(projectDir)
310334
projectRoot.viewHolder = FileTreeViewHolder(context, externalDropHandler)
311335
rootNode.addChild(projectRoot, false)
336+
treeRoot = rootNode
312337

313338
binding!!.horizontalCroll.visibility = View.GONE
314339
binding!!.horizontalCroll.visibility = View.VISIBLE
@@ -334,6 +359,67 @@ class FileTreeFragment : BottomSheetDialogFragment(), TreeNodeClickListener,
334359
}
335360
}
336361

362+
/**
363+
* Re-lists expanded folders whose on-disk contents changed, so plugin-created
364+
* files appear without a manual refresh.
365+
*/
366+
fun refreshExpandedNodes() {
367+
if (binding == null || context == null) return
368+
369+
treeRoot?.let(::refreshChangedDirectories)
370+
}
371+
372+
private fun refreshChangedDirectories(node: TreeNode) {
373+
node.children.forEach { child ->
374+
val file = child.value ?: return@forEach
375+
376+
if (!file.isDirectory || !child.isExpanded) {
377+
return@forEach
378+
}
379+
380+
if (!hasDirectoryContentsChanged(child)) {
381+
refreshChangedDirectories(child)
382+
return@forEach
383+
}
384+
385+
val expandedPaths = hashSetOf<String>()
386+
collectExpandedPaths(child, expandedPaths)
387+
relistPreservingExpansion(child, expandedPaths)
388+
}
389+
}
390+
391+
private fun collectExpandedPaths(node: TreeNode, into: MutableSet<String>) {
392+
for (child in node.children) {
393+
val file = child.value ?: continue
394+
if (file.isDirectory && child.isExpanded) {
395+
into.add(file.absolutePath)
396+
collectExpandedPaths(child, into)
397+
}
398+
}
399+
}
400+
401+
private fun relistPreservingExpansion(node: TreeNode, expandedPaths: Set<String>) {
402+
listNode(node) {
403+
expandNode(node, false)
404+
for (child in node.children) {
405+
val file = child.value ?: continue
406+
if (file.isDirectory && file.absolutePath in expandedPaths) {
407+
relistPreservingExpansion(child, expandedPaths)
408+
}
409+
}
410+
}
411+
}
412+
413+
private fun hasDirectoryContentsChanged(node: TreeNode): Boolean {
414+
val displayed = node.children.mapNotNull { it.value?.name }.toHashSet()
415+
val files = node.value.listFiles() ?: return displayed.isNotEmpty()
416+
if (files.size != displayed.size) {
417+
return true
418+
}
419+
420+
return files.any { it.name !in displayed }
421+
}
422+
337423
private fun createTreeView(node: TreeNode): AndroidTreeView? {
338424
return if (context == null) {
339425
null
@@ -409,6 +495,9 @@ class FileTreeFragment : BottomSheetDialogFragment(), TreeNodeClickListener,
409495
// Should be same as defined in layout/activity_layouteditor.xml
410496
const val TAG = "editor.fileTree"
411497

498+
// Debounce window for coalescing write bursts into one refresh.
499+
private const val PLUGIN_REFRESH_DEBOUNCE_MS = 250L
500+
412501
@JvmStatic
413502
fun newInstance(): FileTreeFragment {
414503
return FileTreeFragment()

‎eventbus-events/src/main/java/com/itsaky/androidide/eventbus/events/filetree/FileTreeEvents.kt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,11 @@ data class FileClickEvent(val file: File) : Event()
3535
* @author Akash Yadav
3636
*/
3737
class FileLongClickEvent(val file: File) : Event()
38+
39+
/**
40+
* Event dispatched when a plugin creates or deletes a file via the IDE file service,
41+
* so the file tree can refresh immediately.
42+
*
43+
* @param file The file that was created or deleted.
44+
*/
45+
class PluginFilesChangedEvent(val file: File) : Event()

‎plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeFileServiceImpl.kt‎

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
package com.itsaky.androidide.plugins.manager.services
22

3+
import com.itsaky.androidide.eventbus.events.filetree.PluginFilesChangedEvent
34
import com.itsaky.androidide.plugins.PluginPermission
45
import com.itsaky.androidide.plugins.services.IdeFileService
6+
import org.greenrobot.eventbus.EventBus
57
import java.io.BufferedInputStream
68
import java.io.File
79
import java.io.FileOutputStream
@@ -40,6 +42,7 @@ class IdeFileServiceImpl(
4042
return try {
4143
file.parentFile?.mkdirs()
4244
file.writeText(content)
45+
notifyFilesChanged(file)
4346
true
4447
} catch (e: Exception) {
4548
false
@@ -53,6 +56,7 @@ class IdeFileServiceImpl(
5356
return try {
5457
file.parentFile?.mkdirs()
5558
file.appendText(content)
59+
notifyFilesChanged(file)
5660
true
5761
} catch (e: Exception) {
5862
false
@@ -107,6 +111,7 @@ class IdeFileServiceImpl(
107111
return try {
108112
file.parentFile?.mkdirs()
109113
file.writeBytes(data)
114+
notifyFilesChanged(file)
110115
true
111116
} catch (e: Exception) {
112117
false
@@ -119,11 +124,13 @@ class IdeFileServiceImpl(
119124

120125
return try {
121126
file.parentFile?.mkdirs()
122-
BufferedInputStream(input).use { buffered ->
127+
val written = BufferedInputStream(input).use { buffered ->
123128
FileOutputStream(file).use { output ->
124129
copyInterruptible(buffered, output)
125130
}
126131
}
132+
notifyFilesChanged(file)
133+
written
127134
} catch (e: Exception) {
128135
FAILED_WRITE
129136
}
@@ -135,12 +142,17 @@ class IdeFileServiceImpl(
135142

136143
return try {
137144
if (!file.exists()) {
138-
true
139-
} else if (file.isDirectory) {
145+
return true
146+
}
147+
val deleted = if (file.isDirectory) {
140148
file.deleteRecursively()
141149
} else {
142150
file.delete()
143151
}
152+
if (deleted) {
153+
notifyFilesChanged(file)
154+
}
155+
deleted
144156
} catch (e: Exception) {
145157
false
146158
}
@@ -180,6 +192,11 @@ class IdeFileServiceImpl(
180192
return total
181193
}
182194

195+
private fun notifyFilesChanged(file: File) {
196+
// Refresh the file tree; guarded so a missing EventBus can't break the write action .
197+
runCatching { EventBus.getDefault().post(PluginFilesChangedEvent(file)) }
198+
}
199+
183200
private fun ensureWritePermission() {
184201
if (!hasAnyWritePermission()) {
185202
throw SecurityException(

0 commit comments

Comments
 (0)