mirror of
https://gitee.com/zhangsken/AndroidIDE.git
synced 2026-10-16 07:49:44 +08:00
feat: restore opened files when project is reopened
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
Vendored
+6
-1
@@ -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()
|
||||
|
||||
|
||||
+52
-10
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
+4
-1
@@ -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.
Reference in New Issue
Block a user