feat: restore opened files when project is reopened

This commit is contained in:
Akash Yadav
2022-12-22 15:59:47 +05:30
parent 8ef3d56967
commit aed7853e25
13 changed files with 209 additions and 38 deletions
+3
View File
@@ -105,11 +105,14 @@ dependencies {
// Annotation processors
kapt common.glide_ap
kapt common.eventbus_ap
kapt google.auto_service
implementation common.editor
implementation common.utilcode
implementation common.glide
implementation common.jsoup
implementation google.auto_service_annotations
implementation google.gson
implementation google.guava
+6 -1
View File
@@ -26,6 +26,11 @@
-keep class org.w3c.** { *; }
-keep class org.xml.** { *; }
# Services
-keepclassmembers class ** {
@com.google.auto.service.AutoService <methods>;
}
# EventBus
-keepattributes *Annotation*
-keepclassmembers class ** {
@@ -51,7 +56,7 @@
<init>(...);
}
-keep class com.itsaky.androidide.editor.IEditor { *; }
-keep class * extends com.itsaky.androidide.inflater.IAttributeAdapter { *; }
-keep class * extends com.itsaky.androidide.inflater.IViewAdapter { *; }
-keep class * extends com.itsaky.androidide.inflater.drawable.IDrawableParser {
<init>(...);
android.graphics.drawable.Drawable parse();
@@ -72,6 +72,8 @@ import com.itsaky.androidide.lookup.Lookup
import com.itsaky.androidide.lsp.models.DiagnosticItem
import com.itsaky.androidide.models.DiagnosticGroup
import com.itsaky.androidide.models.LogLine
import com.itsaky.androidide.models.OpenedFile
import com.itsaky.androidide.models.OpenedFilesCache
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SearchResult
import com.itsaky.androidide.projects.ProjectManager.getProjectDirPath
@@ -159,6 +161,7 @@ abstract class BaseEditorActivity :
protected abstract fun doSaveAll(): Boolean
protected abstract fun doDismissSearchProgress()
protected abstract fun doConfirmProjectClose()
protected abstract fun getOpenedFiles() : List<OpenedFile>
protected open fun preDestroy() {
viewModel.isConfigChange = !isDestroying
@@ -489,7 +492,7 @@ abstract class BaseEditorActivity :
viewModel._isBuildInProgress.observe(this) { onBuildStatusChanged() }
viewModel._isInitializing.observe(this) { onBuildStatusChanged() }
viewModel._statusText.observe(this) { binding.bottomSheet.setStatus(it.first, it.second) }
viewModel.observeFiles(this) { files ->
binding.apply {
if (files == null || files.isEmpty()) {
@@ -499,9 +502,12 @@ abstract class BaseEditorActivity :
tabs.visibility = View.VISIBLE
viewContainer.displayedChild = 0
}
val currentFile = provideCurrentEditor()?.editor?.file?.absolutePath ?: return@observeFiles
getOpenedFiles().also { viewModel.writeOpenedFiles(OpenedFilesCache(currentFile, it)) }
}
}
setupNoEditorView()
setupBottomSheet()
@@ -22,6 +22,7 @@ import android.content.Context
import android.os.Bundle
import android.text.TextUtils
import android.view.Menu
import android.view.View
import android.view.ViewGroup.LayoutParams
import androidx.appcompat.view.menu.MenuBuilder
import androidx.core.view.GravityCompat
@@ -33,6 +34,8 @@ import com.itsaky.androidide.actions.ActionItem.Location.EDITOR_TOOLBAR
import com.itsaky.androidide.actions.ActionsRegistry.Companion.getInstance
import com.itsaky.androidide.editor.ui.IDEEditor
import com.itsaky.androidide.interfaces.IEditorHandler
import com.itsaky.androidide.models.OpenedFile
import com.itsaky.androidide.models.OpenedFilesCache
import com.itsaky.androidide.models.Range
import com.itsaky.androidide.models.SaveResult
import com.itsaky.androidide.projects.ProjectManager.generateSources
@@ -72,7 +75,13 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
return getEditorAtIndex(index)
}
override fun preDestroy() {
super.preDestroy()
viewModel.removeAllFiles()
}
override fun onCreate(savedInstanceState: Bundle?) {
mBuildEventListener.setActivity(this)
super.onCreate(savedInstanceState)
viewModel._displayedFile.observe(this) { this.binding.editorContainer.displayedChild = it }
@@ -81,15 +90,6 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
if (opened) openDrawer(GravityCompat.END) else closeDrawer(GravityCompat.END)
}
}
if (viewModel.getOpenedFileCount() != binding.tabs.tabCount) {
// The activity proabably has been restarted after a configuration change
// restore the opened files
val files = viewModel.getOpenedFiles()
viewModel.removeAllFiles()
files.forEach(this::openFile)
}
}
@SuppressLint("RestrictedApi")
@@ -100,6 +100,39 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
return true
}
override fun onPause() {
super.onPause()
val current = getCurrentEditor()?.editor?.file?.absolutePath ?: return
getOpenedFiles().also {
val cache = OpenedFilesCache(current, it)
viewModel.writeOpenedFiles(cache)
viewModel.openedFilesCache = if (!isFinishing) cache else null
}
}
override fun onStart() {
super.onStart()
try {
log.debug("try to reopen recently opened files")
if (viewModel.openedFilesCache != null) {
viewModel.openedFilesCache?.apply {
this.allFiles.forEach { openFile(File(it.filePath), it.selection) }
openFile(File(selectedFile))
}
} else {
viewModel.readOpenedFiles {
it ?: return@readOpenedFiles
it.allFiles.forEach { file -> openFile(File(file.filePath), file.selection) }
openFile(File(it.selectedFile))
}
}
} catch (err: Throwable) {
log.error("Failed to reopen recently opened files", err)
}
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
ensureToolbarMenu(menu)
return true
@@ -159,10 +192,13 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
override fun openFileAndGetIndex(file: File, selection: Range?): Int {
val openedFileIndex = findIndexOfEditorByFile(file)
if (openedFileIndex != -1) {
log.error("File is already opened. File: $file")
return openedFileIndex
}
if (!file.exists()) {
return -1
}
val position = viewModel.getOpenedFileCount()
log.info("Opening file at index:", position, "file: ", file)
@@ -387,6 +423,12 @@ open class EditorHandlerActivity : ProjectHandlerActivity(), IEditorHandler {
runAfter()
}
override fun getOpenedFiles() =
viewModel.getOpenedFiles().mapNotNull {
val editor = getEditorForFile(it)?.editor ?: return@mapNotNull null
OpenedFile(it.absolutePath, editor.cursorLSPRange)
}
private fun notifyFilesUnsaved(unsavedEditors: List<CodeEditorView?>, invokeAfter: Runnable) {
if (isDestroying) {
// Do not show unsaved files dialog if the activity is being destroyed
@@ -53,6 +53,7 @@ import com.itsaky.androidide.utils.RecursiveFileSearcher
import com.itsaky.toaster.Toaster.Type.ERROR
import com.itsaky.toaster.toast
import java.io.File
import java.util.ServiceLoader
import java.util.concurrent.CompletableFuture
import java.util.regex.Pattern
import java.util.stream.Collectors
@@ -92,8 +93,6 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBuildEventListener.setActivity(this as EditorHandlerActivity)
startServices()
}
@@ -238,6 +237,11 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
notifyProjectUpdate()
ThreadUtils.runOnUiThread { postProjectInit() }
}
protected open fun preProjectInit() {
setStatus(getString(string.msg_initializing_project))
viewModel.isInitializing = true
}
protected open fun postProjectInit() {
initialSetup()
@@ -403,11 +407,6 @@ abstract class ProjectHandlerActivity : BaseEditorActivity(), IProjectHandler {
connectClient(IDELanguageClientImpl.getInstance())
}
private fun preProjectInit() {
setStatus(getString(string.msg_initializing_project))
viewModel.isInitializing = true
}
open fun getProgressSheet(msg: Int): ProgressSheet? {
doDismissSearchProgress()
@@ -0,0 +1,30 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.models
import com.google.gson.annotations.SerializedName
/**
* A file that is opened in editor.
*
* @author Akash Yadav
*/
data class OpenedFile(
@SerializedName("file") val filePath: String,
@SerializedName("selection") var selection: Range
)
@@ -0,0 +1,23 @@
/*
* This file is part of AndroidIDE.
*
* AndroidIDE is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* AndroidIDE 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with AndroidIDE. If not, see <https://www.gnu.org/licenses/>.
*/
package com.itsaky.androidide.models
/**
* @author Akash Yadav
*/
data class OpenedFilesCache(val selectedFile: String, val allFiles: List<OpenedFile>)
@@ -22,6 +22,13 @@ import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import androidx.lifecycle.ViewModel
import com.blankj.utilcode.util.FileUtils
import com.google.gson.Gson
import com.google.gson.GsonBuilder
import com.itsaky.androidide.models.OpenedFilesCache
import com.itsaky.androidide.projects.ProjectManager
import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.utils.Environment
import java.io.File
/** ViewModel for data used in [com.itsaky.androidide.activities.editor.EditorActivityKt] */
@@ -33,10 +40,12 @@ class EditorViewModel : ViewModel() {
internal val _statusText = MutableLiveData<Pair<CharSequence, @GravityInt Int>>("" to CENTER)
internal val _displayedFile = MutableLiveData(-1)
internal val _fileTreeDrawerOpened = MutableLiveData(false)
private val _openedFiles = MutableLiveData<OpenedFilesCache>()
private val _isBoundToBuildService = MutableLiveData(false)
private val _isConfigurationChange = MutableLiveData(false)
private val files = MutableLiveData<MutableList<File>>(ArrayList())
private val fileModified = MutableLiveData(false)
/**
@@ -45,6 +54,12 @@ class EditorViewModel : ViewModel() {
*/
private val mCurrentFile = MutableLiveData<Pair<Int, File?>?>(null)
var openedFilesCache: OpenedFilesCache?
get() = _openedFiles.value
set(value) {
this._openedFiles.value = value
}
var isBoundToBuildSerice: Boolean
get() = _isBoundToBuildService.value ?: false
set(value) {
@@ -179,4 +194,40 @@ class EditorViewModel : ViewModel() {
val modified = fileModified.value
return modified != null && modified
}
fun readOpenedFiles(result: (OpenedFilesCache?) -> Unit) {
executeAsync({
val file = getOpenedFilesCache()
if (file.length() == 0L) {
return@executeAsync null
}
return@executeAsync Gson().fromJson(file.readText(), OpenedFilesCache::class.java)
}) {
result(it)
}
}
fun writeOpenedFiles(cache: OpenedFilesCache) {
executeAsync {
val file = getOpenedFilesCache()
val gson = GsonBuilder().setPrettyPrinting().create()
val string = gson.toJson(cache)
file.writeText(string)
}
}
private fun getOpenedFilesCache(): File {
var file = Environment.getProjectCacheDir(ProjectManager.projectPath)
file = File(file, "editor/openedFiles.json")
if (file.exists()) {
FileUtils.rename(file, "${file.name}.bak")
file.createNewFile()
}
if (file.parentFile?.exists() == false) {
file.parentFile?.mkdirs()
}
return file
}
}
@@ -44,6 +44,7 @@ public final class Environment {
private static final String DEFAULT_ANDROID_HOME = DEFAULT_HOME + "/android-sdk";
public static final String DEFAULT_PREFIX = DEFAULT_ROOT + "/usr";
private static final String DEFAULT_JAVA_HOME = DEFAULT_PREFIX + "/opt/openjdk";
private static final String ANDROIDIDE_PROJECT_CACHE_DIR = ".androidide";
private static final ILogger LOG = ILogger.newInstance("Environment");
private static final List<String> blacklist = new ArrayList<>();
public static File ROOT;
@@ -214,7 +215,7 @@ public final class Environment {
ENV_VARS.put("SHELL", SHELL.getAbsolutePath());
ENV_VARS.put("CONFIG_SHELL", SHELL.getAbsolutePath());
ENV_VARS.put("TERM", "screen");
// https://github.com/termux/termux-tools/blob/f2736f7f8232cd19cf52bca9b0ac9afb8ad9e562/scripts/termux-setup-package-manager.in#L3
ENV_VARS.put("TERMUX_APP_PACKAGE_MANAGER", "apt");
ENV_VARS.put("TERMUX_PKG_NO_MIRROR_SELECT", "true");
@@ -262,4 +263,12 @@ public final class Environment {
}
return blacklist;
}
public static File getProjectCacheDir(String projectDir) {
return new File(projectDir, ANDROIDIDE_PROJECT_CACHE_DIR);
}
public static File getProjectCacheDir(File projectDir) {
return new File(projectDir, ANDROIDIDE_PROJECT_CACHE_DIR);
}
}
+10 -9
View File
@@ -58,15 +58,16 @@ ext {
]
google = [
material : 'com.google.android.material:material:1.7.0',
gson : 'com.google.code.gson:gson:2.10',
guava : 'com.google.guava:guava:31.1-android',
guava_jre : 'com.google.guava:guava:31.1-jre',
auto_val_annotations: 'com.google.auto.value:auto-value-annotations:1.10.1',
auto_val_ap : 'com.google.auto.value:auto-value:1.10',
auto_service : 'com.google.auto.service:auto-service:1.0.1',
oss_licenses : 'com.google.android.gms:play-services-oss-licenses:17.0.0',
protobuf : 'com.google.protobuf:protobuf-java:3.10.0'
material : 'com.google.android.material:material:1.7.0',
gson : 'com.google.code.gson:gson:2.10',
guava : 'com.google.guava:guava:31.1-android',
guava_jre : 'com.google.guava:guava:31.1-jre',
auto_val_annotations : 'com.google.auto.value:auto-value-annotations:1.10.1',
auto_val_ap : 'com.google.auto.value:auto-value:1.10',
auto_service_annotations: 'com.google.auto.service:auto-service-annotations:1.0.1',
auto_service : 'com.google.auto.service:auto-service:1.0.1',
oss_licenses : 'com.google.android.gms:play-services-oss-licenses:17.0.0',
protobuf : 'com.google.protobuf:protobuf-java:3.10.0'
]
aapt = [
@@ -81,8 +81,7 @@ public class IDEEditor extends CodeEditor implements com.itsaky.androidide.edito
private ILanguageServer languageServer;
private SignatureHelpWindow signatureHelpWindow;
private DiagnosticWindow diagnosticWindow;
@Nullable
ILanguageClient languageClient;
@Nullable ILanguageClient languageClient;
public IDEEditor(Context context) {
this(context, null);
@@ -413,17 +412,17 @@ public class IDEEditor extends CodeEditor implements com.itsaky.androidide.edito
@Override
public void setLanguageServer(ILanguageServer server) {
this.languageServer = server;
if (server != null) {
this.languageClient = server.getClient();
}
}
@Override
public void setLanguageClient(@Nullable final ILanguageClient languageClient) {
this.languageClient = languageClient;
}
/**
* Append the given text at the end of the editor's content.
*
@@ -823,7 +822,7 @@ public class IDEEditor extends CodeEditor implements com.itsaky.androidide.edito
return;
}
final var closeEvent = new DocumentCloseEvent(getFile().toPath());
final var closeEvent = new DocumentCloseEvent(getFile().toPath(), getCursorLSPRange());
EventBus.getDefault().post(closeEvent);
}
@@ -33,7 +33,10 @@ data class DocumentOpenEvent(var openedFile: Path, var text: String, var version
DocumentEvent(openedFile)
/** Dispatched when the given file is closed. Always dispatched after [DocumentOpenEvent]. */
data class DocumentCloseEvent(var closedFile: Path) : DocumentEvent(closedFile)
data class DocumentCloseEvent
@JvmOverloads
constructor(var closedFile: Path, val selectionRange: Range = Range.NONE) :
DocumentEvent(closedFile)
/**
* Dispatched when the content of the given opened document changes. The change can be either
Binary file not shown.