fix(editor): file modification status is not updated while closing files (fixes #1485)

This commit is contained in:
Akash Yadav
2023-11-25 23:31:48 +05:30
parent b2ff61043b
commit e1382f13a7
5 changed files with 142 additions and 132 deletions
-1
View File
@@ -1,4 +1,3 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="ANTLRGenerationPreferences"> <component name="ANTLRGenerationPreferences">
<option name="perGrammarGenerationSettings"> <option name="perGrammarGenerationSettings">
@@ -465,6 +465,8 @@ abstract class BaseEditorActivity :
viewContainer.displayedChild = 0 viewContainer.displayedChild = 0
} }
} }
invalidateOptionsMenu()
} }
setupNoEditorView() setupNoEditorView()
@@ -28,7 +28,6 @@ import androidx.appcompat.view.menu.MenuBuilder
import androidx.collection.MutableIntObjectMap import androidx.collection.MutableIntObjectMap
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
import com.blankj.utilcode.util.ImageUtils import com.blankj.utilcode.util.ImageUtils
import com.blankj.utilcode.util.ThreadUtils
import com.itsaky.androidide.R.string import com.itsaky.androidide.R.string
import com.itsaky.androidide.actions.ActionData import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TOOLBAR import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TOOLBAR
@@ -51,7 +50,6 @@ import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SaveResult import com.itsaky.androidide.models.SaveResult
import com.itsaky.androidide.projects.ProjectManagerImpl import com.itsaky.androidide.projects.ProjectManagerImpl
import com.itsaky.androidide.tasks.executeAsync import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.tasks.executeAsyncProvideError
import com.itsaky.androidide.ui.CodeEditorView import com.itsaky.androidide.ui.CodeEditorView
import com.itsaky.androidide.utils.DialogUtils.newYesNoDialog import com.itsaky.androidide.utils.DialogUtils.newYesNoDialog
import com.itsaky.androidide.utils.IntentUtils.openImage import com.itsaky.androidide.utils.IntentUtils.openImage
@@ -109,6 +107,9 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
} }
} }
editorViewModel._filesModified.observe(this) { invalidateOptionsMenu() }
editorViewModel._filesSaving.observe(this) { invalidateOptionsMenu() }
editorViewModel.observeFiles(this) { editorViewModel.observeFiles(this) {
// rewrite the cached files index if there are any opened files // rewrite the cached files index if there are any opened files
val currentFile = val currentFile =
@@ -413,41 +414,40 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
} }
override suspend fun saveAllResult(progressConsumer: ((Int, Int) -> Unit)?): SaveResult { override suspend fun saveAllResult(progressConsumer: ((Int, Int) -> Unit)?): SaveResult {
setFilesSaving(true) return performFileSave {
val result = SaveResult() val result = SaveResult()
for (i in 0 until editorViewModel.getOpenedFileCount()) { for (i in 0 until editorViewModel.getOpenedFileCount()) {
saveResultInternal(i, result, false) saveResultInternal(i, result)
progressConsumer?.invoke(i + 1, editorViewModel.getOpenedFileCount()) progressConsumer?.invoke(i + 1, editorViewModel.getOpenedFileCount())
}
return@performFileSave result
} }
setFilesSaving(false)
return result
} }
override suspend fun saveResult(index: Int, result: SaveResult) { override suspend fun saveResult(index: Int, result: SaveResult) {
saveResultInternal(index, result, true) performFileSave {
saveResultInternal(index, result)
}
} }
private suspend fun saveResultInternal(index: Int, result: SaveResult, updateSaving: Boolean) { private suspend fun saveResultInternal(
index: Int,
result: SaveResult
) : Boolean {
if (index < 0 || index >= editorViewModel.getOpenedFileCount()) { if (index < 0 || index >= editorViewModel.getOpenedFileCount()) {
return return false
} }
val frag = getEditorAtIndex(index) ?: return val frag = getEditorAtIndex(index) ?: return false
val fileName = frag.file?.name ?: return val fileName = frag.file?.name ?: return false
if (updateSaving) {
setFilesSaving(true)
}
run { run {
// Must be called before frag.save() // Must be called before frag.save()
// Otherwise, it'll always return false // Otherwise, it'll always return false
val modified = frag.isModified val modified = frag.isModified
if (!frag.save()) { if (!frag.save()) {
if (updateSaving) { return false
setFilesSaving(false)
}
return
} }
val isGradle = fileName.endsWith(".gradle") || fileName.endsWith(".gradle.kts") val isGradle = fileName.endsWith(".gradle") || fileName.endsWith(".gradle.kts")
@@ -461,22 +461,11 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
} }
} }
var modified = false val hasUnsaved = hasUnsavedFiles()
for (file in editorViewModel.getOpenedFiles()) {
val editor = getEditorForFile(file) ?: continue
modified = modified || editor.isModified
}
val finalModified = modified
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
editorViewModel.apply {
setFilesModified(finalModified)
}
if (updateSaving) { editorViewModel.areFilesModified = hasUnsaved
setFilesSaving(false)
}
// set tab as unmodified // set tab as unmodified
val tab = binding.tabs.getTabAt(index) ?: return@withContext val tab = binding.tabs.getTabAt(index) ?: return@withContext
@@ -484,85 +473,105 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
tab.text = tab.text!!.substring(startIndex = 1) tab.text = tab.text!!.substring(startIndex = 1)
} }
} }
return true
}
private fun hasUnsavedFiles() = editorViewModel.getOpenedFiles().any { file ->
getEditorForFile(file)?.isModified == true
}
private suspend inline fun <T : Any?> performFileSave(crossinline action: suspend () -> T) : T {
setFilesSaving(true)
try {
return action()
} finally {
setFilesSaving(false)
}
} }
private suspend fun setFilesSaving(saving: Boolean) { private suspend fun setFilesSaving(saving: Boolean) {
withContext(Dispatchers.Main.immediate) { withContext(Dispatchers.Main.immediate) {
invalidateOptionsMenu() editorViewModel.areFilesSaving = saving
editorViewModel.setFilesSaving(saving)
} }
} }
private fun onEditorContentChanged() {
editorViewModel.setFilesModified(true)
invalidateOptionsMenu()
}
override fun areFilesModified(): Boolean { override fun areFilesModified(): Boolean {
return editorViewModel.areFilesModified() return editorViewModel.areFilesModified
} }
override fun areFilesSaving(): Boolean { override fun areFilesSaving(): Boolean {
return editorViewModel.areFilesSaving() return editorViewModel.areFilesSaving
} }
override fun closeFile(index: Int, runAfter: () -> Unit) { override fun closeFile(index: Int, runAfter: () -> Unit) {
if (index >= 0 && index < editorViewModel.getOpenedFileCount()) { if (index < 0 || index >= editorViewModel.getOpenedFileCount()) {
val opened: File = editorViewModel.getOpenedFile(index)
log.info("Closing file:", opened)
val editor = getEditorAtIndex(index)
if (editor != null && editor.isModified) {
notifyFilesUnsaved(listOf(editor)) { closeFile(index, runAfter) }
return
}
editor?.close() ?: run {
log.error("Cannot save file before close. Editor instance is null")
}
editorViewModel.removeFile(index)
binding.apply {
tabs.removeTabAt(index)
editorContainer.removeViewAt(index)
}
updateTabs()
} else {
log.error("Invalid file index. Cannot close.") log.error("Invalid file index. Cannot close.")
return return
} }
val opened = editorViewModel.getOpenedFile(index)
log.info("Closing file:", opened)
val editor = getEditorAtIndex(index)
if (editor?.isModified == true) {
log.info("File has been modified:", opened)
notifyFilesUnsaved(listOf(editor)) {
closeFile(index, runAfter)
}
return
}
editor?.close() ?: run {
log.error("Cannot save file before close. Editor instance is null")
}
editorViewModel.removeFile(index)
binding.apply {
tabs.removeTabAt(index)
editorContainer.removeViewAt(index)
}
editorViewModel.areFilesModified = hasUnsavedFiles()
updateTabs()
runAfter() runAfter()
} }
override fun closeOthers() { override fun closeOthers() {
if (editorViewModel.getOpenedFileCount() == 0) {
return
}
val unsavedFiles = val unsavedFiles =
editorViewModel.getOpenedFiles().map(::getEditorForFile) editorViewModel.getOpenedFiles().map(::getEditorForFile)
.filter { it != null && it.isModified } .filter { it != null && it.isModified }
if (unsavedFiles.isEmpty()) { if (unsavedFiles.isNotEmpty()) {
val file = editorViewModel.getCurrentFile()
var index = 0
// keep closing the file at index 0
// if openedFiles[0] == file, then keep closing files at index 1
while (editorViewModel.getOpenedFileCount() != 1) {
val editor = getEditorAtIndex(index)
// Index of files changes as we keep close files
// So we compare the files instead of index
if (editor != null) {
if (file != editor.file) {
closeFile(index)
} else {
index = 1
}
} else {
log.error("Unable to save file at index:", index)
}
}
} else {
notifyFilesUnsaved(unsavedFiles) { closeOthers() } notifyFilesUnsaved(unsavedFiles) { closeOthers() }
return
}
val file = editorViewModel.getCurrentFile()
var index = 0
// keep closing the file at index 0
// if openedFiles[0] == file, then keep closing files at index 1
while (editorViewModel.getOpenedFileCount() != 1) {
val editor = getEditorAtIndex(index)
if (editor == null) {
log.error("Unable to save file at index:", index)
continue
}
// Index of files changes as we keep close files
// So we compare the files instead of index
if (file != editor.file) {
closeFile(index)
} else {
index = 1
}
} }
} }
@@ -650,7 +659,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
@Subscribe(threadMode = ThreadMode.MAIN) @Subscribe(threadMode = ThreadMode.MAIN)
fun onDocumentChange(event: DocumentChangeEvent) { fun onDocumentChange(event: DocumentChangeEvent) {
// update content modification status // update content modification status
onEditorContentChanged() editorViewModel.areFilesModified = true
val index = findIndexOfEditorByFile(event.file.toFile()) val index = findIndexOfEditorByFile(event.file.toFile())
if (index == -1) { if (index == -1) {
@@ -667,7 +676,7 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
} }
private fun updateTabs() { private fun updateTabs() {
executeAsyncProvideError({ editorActivityScope.launch {
val files = editorViewModel.getOpenedFiles() val files = editorViewModel.getOpenedFiles()
val dupliCount = mutableMapOf<String, Int>() val dupliCount = mutableMapOf<String, Int>()
val names = MutableIntObjectMap<String>() val names = MutableIntObjectMap<String>()
@@ -690,15 +699,8 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
names[i] = name names[i] = name
} }
names withContext(Dispatchers.Main) {
}) { result, error -> names.forEach { index, name ->
if (result == null || error != null) {
log.error("Failed to compute names for file tabs", error)
return@executeAsyncProvideError
}
ThreadUtils.runOnUiThread {
result.forEach { index, name ->
binding.tabs.getTabAt(index)?.text = name binding.tabs.getTabAt(index)?.text = name
} }
} }
@@ -208,7 +208,7 @@ class CodeEditorView(
* Mark this files as saved. Even if it not saved. * Mark this files as saved. Even if it not saved.
*/ */
fun markAsSaved() { fun markAsSaved() {
notifySaved() editor?.markUnmodified()
} }
/** /**
@@ -245,6 +245,7 @@ class CodeEditorView(
_binding?.rwProgress?.isVisible = false _binding?.rwProgress?.isVisible = false
} }
markUnmodified()
notifySaved() notifySaved()
return true return true
@@ -24,10 +24,12 @@ import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import com.blankj.utilcode.util.FileUtils import com.blankj.utilcode.util.FileUtils
import com.google.gson.GsonBuilder import com.google.gson.GsonBuilder
import com.itsaky.androidide.models.OpenedFile
import com.itsaky.androidide.models.OpenedFilesCache import com.itsaky.androidide.models.OpenedFilesCache
import com.itsaky.androidide.projects.IProjectManager import com.itsaky.androidide.projects.IProjectManager
import com.itsaky.androidide.tasks.executeAsync import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.utils.Environment import com.itsaky.androidide.utils.Environment
import com.itsaky.androidide.utils.ILogger
import java.io.File import java.io.File
/** ViewModel for data used in [com.itsaky.androidide.activities.editor.EditorActivityKt] */ /** ViewModel for data used in [com.itsaky.androidide.activities.editor.EditorActivityKt] */
@@ -41,12 +43,12 @@ class EditorViewModel : ViewModel() {
internal val _startDrawerOpened = MutableLiveData(false) internal val _startDrawerOpened = MutableLiveData(false)
internal val _isSyncNeeded = MutableLiveData(false) internal val _isSyncNeeded = MutableLiveData(false)
internal val _filesModified = MutableLiveData(false)
internal val _filesSaving = MutableLiveData(false)
private val _openedFiles = MutableLiveData<OpenedFilesCache>() private val _openedFiles = MutableLiveData<OpenedFilesCache>()
private val _isBoundToBuildService = MutableLiveData(false) private val _isBoundToBuildService = MutableLiveData(false)
private val files = MutableLiveData<MutableList<File>>(ArrayList()) private val _files = MutableLiveData<MutableList<File>>(ArrayList())
private val fileModified = MutableLiveData(false)
private val fileSaving = MutableLiveData(false)
/** /**
* Holds information about the currently selected editor fragment. First value in the pair is the * Holds information about the currently selected editor fragment. First value in the pair is the
@@ -54,6 +56,19 @@ class EditorViewModel : ViewModel() {
*/ */
private val mCurrentFile = MutableLiveData<Pair<Int, File?>?>(null) private val mCurrentFile = MutableLiveData<Pair<Int, File?>?>(null)
var areFilesModified: Boolean
get() = _filesModified.value ?: false
set(value) {
ILogger.instance().debug("update areFilesModified = $value", RuntimeException())
_filesModified.value = value
}
var areFilesSaving: Boolean
get() = _filesSaving.value ?: false
set(value) {
_filesSaving.value = value
}
var openedFilesCache: OpenedFilesCache? var openedFilesCache: OpenedFilesCache?
get() = _openedFiles.value get() = _openedFiles.value
set(value) { set(value) {
@@ -108,15 +123,24 @@ class EditorViewModel : ViewModel() {
_isSyncNeeded.value = value _isSyncNeeded.value = value
} }
internal var files: MutableList<File>
get() = this._files.value ?: EMPTY_FILES
set(value) {
this._files.value = value
}
companion object {
private val EMPTY_FILES = mutableListOf<File>()
}
/** /**
* Add the given file to the list of opened files. * Add the given file to the list of opened files.
* *
* @param file The file that has been opened. * @param file The file that has been opened.
*/ */
fun addFile(file: File) { fun addFile(file: File) {
val files = files.value ?: mutableListOf()
files.add(file) files.add(file)
this.files.value = files this.files = files
} }
/** /**
@@ -125,17 +149,16 @@ class EditorViewModel : ViewModel() {
* @param index The index of the closed file. * @param index The index of the closed file.
*/ */
fun removeFile(index: Int) { fun removeFile(index: Int) {
val files = files.value ?: mutableListOf()
files.removeAt(index) files.removeAt(index)
this.files.value = files this.files = files
if (this.files.value?.isEmpty() == true) { if (this.files.isEmpty()) {
mCurrentFile.value = null mCurrentFile.value = null
} }
} }
fun removeAllFiles() { fun removeAllFiles() {
files.value = mutableListOf() this.files = EMPTY_FILES
setCurrentFile(-1, null) setCurrentFile(-1, null)
} }
@@ -145,8 +168,8 @@ class EditorViewModel : ViewModel() {
} }
fun updateFile(index: Int, newFile: File) { fun updateFile(index: Int, newFile: File) {
val files = files.value ?: return
files[index] = newFile files[index] = newFile
this.files = files
} }
/** /**
@@ -156,7 +179,7 @@ class EditorViewModel : ViewModel() {
* @return The file at the given index. * @return The file at the given index.
*/ */
fun getOpenedFile(index: Int): File { fun getOpenedFile(index: Int): File {
return files.value!![index] return files[index]
} }
/** /**
@@ -165,7 +188,7 @@ class EditorViewModel : ViewModel() {
* @return The number of files opened. * @return The number of files opened.
*/ */
fun getOpenedFileCount(): Int { fun getOpenedFileCount(): Int {
return files.value!!.size return files.size
} }
/** /**
@@ -174,7 +197,7 @@ class EditorViewModel : ViewModel() {
* @return The list of opened files. * @return The list of opened files.
*/ */
fun getOpenedFiles(): List<File> { fun getOpenedFiles(): List<File> {
return files.value ?: mutableListOf() return files
} }
/** /**
@@ -184,7 +207,7 @@ class EditorViewModel : ViewModel() {
* @param observer The observer. * @param observer The observer.
*/ */
fun observeFiles(lifecycleOwner: LifecycleOwner?, observer: Observer<MutableList<File>?>?) { fun observeFiles(lifecycleOwner: LifecycleOwner?, observer: Observer<MutableList<File>?>?) {
files.observe(lifecycleOwner!!, observer!!) _files.observe(lifecycleOwner!!, observer!!)
} }
fun getCurrentFileIndex(): Int { fun getCurrentFileIndex(): Int {
@@ -195,23 +218,6 @@ class EditorViewModel : ViewModel() {
return mCurrentFile.value?.second return mCurrentFile.value?.second
} }
fun setFilesModified(modified: Boolean) {
fileModified.value = modified
}
fun areFilesModified(): Boolean {
val modified = fileModified.value
return modified != null && modified
}
fun setFilesSaving(saving: Boolean) {
fileSaving.value = saving
}
fun areFilesSaving(): Boolean {
return fileSaving.value ?: false
}
fun readOpenedFiles(result: (OpenedFilesCache?) -> Unit) { fun readOpenedFiles(result: (OpenedFilesCache?) -> Unit) {
executeAsync({ executeAsync({
val file = getOpenedFilesCache(false) val file = getOpenedFilesCache(false)