diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ecd8baa8c..6aa7554fd 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -9,6 +9,9 @@
xmlns:tools="http://schemas.android.com/tools">
+
+
+
@@ -74,5 +77,14 @@
android:name="android.app.shortcuts"
android:resource="@xml/shortcuts" />
+
+
+
+
diff --git a/app/src/main/kotlin/org/cosmicide/MainActivity.kt b/app/src/main/kotlin/org/cosmicide/MainActivity.kt
index 91999ef76..6ed0c6ce4 100644
--- a/app/src/main/kotlin/org/cosmicide/MainActivity.kt
+++ b/app/src/main/kotlin/org/cosmicide/MainActivity.kt
@@ -7,11 +7,16 @@
package org.cosmicide
+import android.Manifest
+import android.content.pm.PackageManager
+import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.CompositionLocalProvider
@@ -24,6 +29,7 @@ import org.cosmicide.common.Prefs
import org.cosmicide.editor.EditorExtensionPoints
import org.cosmicide.editor.lsp.LspEditorLanguageProvider
import org.cosmicide.plugin.CosmicPluginHost
+import org.cosmicide.service.KeepAliveService
import org.cosmicide.ui.IDENavigation
import org.cosmicide.ui.donation.DonationPromptTracker
import org.cosmicide.ui.editor.resolveTheme
@@ -41,6 +47,7 @@ class MainActivity : ComponentActivity() {
if (savedInstanceState == null) {
DonationPromptTracker.recordLaunch(applicationContext)
}
+ startKeepAliveService()
val appContainer = AppContainer(applicationContext)
setContent {
@@ -95,6 +102,19 @@ class MainActivity : ComponentActivity() {
LspEditorLanguageProvider.updateColors(colorScheme)
}
+ private fun startKeepAliveService() {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU &&
+ ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
+ != PackageManager.PERMISSION_GRANTED
+ ) {
+ ActivityCompat.requestPermissions(
+ this, arrayOf(Manifest.permission.POST_NOTIFICATIONS), 100
+ )
+ }
+ runCatching { KeepAliveService.start(this) }
+ .onFailure { Log.w(TAG, "Could not start keep-alive service", it) }
+ }
+
private companion object {
const val TAG = "MainActivity"
}
diff --git a/app/src/main/kotlin/org/cosmicide/editor/lsp/ComposeSignatureHelpLayout.kt b/app/src/main/kotlin/org/cosmicide/editor/lsp/ComposeSignatureHelpLayout.kt
index cac78c65c..0dccaed4e 100644
--- a/app/src/main/kotlin/org/cosmicide/editor/lsp/ComposeSignatureHelpLayout.kt
+++ b/app/src/main/kotlin/org/cosmicide/editor/lsp/ComposeSignatureHelpLayout.kt
@@ -117,8 +117,11 @@ class ComposeSignatureHelpLayout : SignatureHelpLayout {
}
}
- // SignatureHelpWindow measures before attaching its PopupWindow.
- createComposition(ComposeViewContext(window.editor))
+ // SignatureHelpWindow measures before attaching its PopupWindow. Only possible once
+ // the editor is attached; otherwise the strategy creates composition on attach.
+ if (window.editor.isAttachedToWindow) {
+ createComposition(ComposeViewContext(window.editor))
+ }
}
return composeView
}
@@ -171,7 +174,11 @@ class ComposeSignatureHelpLayout : SignatureHelpLayout {
* initial empty state.
*/
private fun composeBeforeHostMeasurement() {
- if (!::composeView.isInitialized || composeView.isAttachedToWindow) return
+ if (
+ !::composeView.isInitialized ||
+ composeView.isAttachedToWindow ||
+ !window.editor.isAttachedToWindow
+ ) return
composeView.disposeComposition()
composeView.createComposition(ComposeViewContext(window.editor))
}
diff --git a/app/src/main/kotlin/org/cosmicide/editor/lsp/HoverLayout.kt b/app/src/main/kotlin/org/cosmicide/editor/lsp/HoverLayout.kt
index 9169e753b..c22df9a5e 100644
--- a/app/src/main/kotlin/org/cosmicide/editor/lsp/HoverLayout.kt
+++ b/app/src/main/kotlin/org/cosmicide/editor/lsp/HoverLayout.kt
@@ -102,8 +102,11 @@ class HoverLayout : HoverLayout {
}
}
- // HoverWindow measures before attaching the PopupWindow.
- createComposition(ComposeViewContext(window.editor))
+ // HoverWindow measures before attaching the PopupWindow. Only possible once the
+ // editor is attached; otherwise the strategy creates composition on attach.
+ if (window.editor.isAttachedToWindow) {
+ createComposition(ComposeViewContext(window.editor))
+ }
}
}
diff --git a/app/src/main/kotlin/org/cosmicide/editor/lsp/LspEditorAdapter.kt b/app/src/main/kotlin/org/cosmicide/editor/lsp/LspEditorAdapter.kt
index bf7ca3dcf..123dc129a 100644
--- a/app/src/main/kotlin/org/cosmicide/editor/lsp/LspEditorAdapter.kt
+++ b/app/src/main/kotlin/org/cosmicide/editor/lsp/LspEditorAdapter.kt
@@ -32,7 +32,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.cosmicide.common.AppDispatchers
-import org.cosmicide.common.IndexManager
import org.cosmicide.editor.LspServerConnection
import org.cosmicide.editor.LspServerDefinition
import org.cosmicide.editor.LspServerRequest
@@ -47,6 +46,7 @@ import java.io.InputStream
import java.io.OutputStream
import java.net.URI
import java.net.URL
+
import java.security.MessageDigest
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
@@ -164,6 +164,9 @@ fun CodeEditor.configureLspLanguage(
)
}
} catch (e: Exception) {
+ withContext(Dispatchers.Main) {
+ editable = true
+ }
lspEditor.dispose()
Log.w(TAG, "Failed to connect to ${definition.displayName}", e)
LspLogStore.error(definition.displayName, "Failed to connect", e)
@@ -447,44 +450,24 @@ private fun createTextMateLanguage(
return createTextMateLanguage(definition, grammarText)
}
- var cachedGrammarText: String? = readGrammarViaIndexManager(grammarLink)
+ val cachedGrammarText = runCatching {
+ grammarCacheFile(context, grammarLink).takeIf(File::exists)?.readText()
+ }.getOrNull()
if (cachedGrammarText != null) {
try {
return createTextMateLanguage(definition, cachedGrammarText)
} catch (e: Exception) {
Log.w(TAG, "Discarding invalid grammar cache for $grammarLink", e)
- IndexManager.invalidateProject("grammar:$grammarLink")
- cachedGrammarText = null
+ runCatching { grammarCacheFile(context, grammarLink).delete() }
}
}
- val refreshedGrammarText = try {
- openGrammarStream(context, grammarLink).readGrammarText()
- } catch (refreshFailure: Exception) {
- val staleGrammarText = cachedGrammarText ?: throw refreshFailure
- Log.w(TAG, "Grammar refresh failed; using stale cache for $grammarLink", refreshFailure)
- return createTextMateLanguage(definition, staleGrammarText)
- }
-
- return try {
- createTextMateLanguage(definition, refreshedGrammarText).also {
- runCatching { cacheGrammarViaIndexManager(grammarLink, refreshedGrammarText) }
- .onFailure { error ->
- Log.w(TAG, "Unable to cache grammar from $grammarLink", error)
- }
- }
- } catch (refreshFailure: Exception) {
- val staleGrammarText = cachedGrammarText ?: throw refreshFailure
- Log.w(
- TAG,
- "Refreshed grammar is invalid; using stale cache for $grammarLink",
- refreshFailure
- )
- runCatching { createTextMateLanguage(definition, staleGrammarText) }
- .getOrElse { staleFailure ->
- refreshFailure.addSuppressed(staleFailure)
- throw refreshFailure
+ val refreshedGrammarText = openGrammarStream(context, grammarLink).readGrammarText()
+ return createTextMateLanguage(definition, refreshedGrammarText).also {
+ runCatching { grammarCacheFile(context, grammarLink).writeText(refreshedGrammarText) }
+ .onFailure { error ->
+ Log.w(TAG, "Unable to cache grammar from $grammarLink", error)
}
}
}
@@ -542,23 +525,6 @@ private fun grammarCacheFile(context: Context, grammarLink: String): File {
.resolve("$cacheKey.grammar")
}
-private fun cacheGrammarViaIndexManager(grammarLink: String, grammarText: String) {
- val key = "grammar:${grammarLink}"
- IndexManager.getOrBuildIndex(key, "text") { grammarText.toByteArray(Charsets.UTF_8) }
-}
-
-private fun readGrammarViaIndexManager(grammarLink: String): String? {
- val key = "grammar:${grammarLink}"
- return try {
- val segment = IndexManager.getOrBuildIndex(key, "text") {
- throw IllegalStateException("Grammar not cached: $grammarLink")
- }
- String(segment.readBlock(0, segment.size.toInt()), Charsets.UTF_8)
- } catch (e: Exception) {
- null
- }
-}
-
private fun openGrammarStream(context: Context, link: String): InputStream {
val uri = link.toUri()
return when (uri.scheme?.lowercase()) {
diff --git a/app/src/main/kotlin/org/cosmicide/service/KeepAliveService.kt b/app/src/main/kotlin/org/cosmicide/service/KeepAliveService.kt
new file mode 100644
index 000000000..75175179f
--- /dev/null
+++ b/app/src/main/kotlin/org/cosmicide/service/KeepAliveService.kt
@@ -0,0 +1,77 @@
+/*
+ * This file is part of Cosmic IDE.
+ * Cosmic IDE is a 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.
+ * Cosmic IDE 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 Cosmic IDE. If not, see .
+ */
+
+package org.cosmicide.service
+
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.os.Build
+import android.os.IBinder
+import org.cosmicide.MainActivity
+import org.cosmicide.R
+
+/**
+ * Foreground service that keeps the app process alive in the background, like Termux.
+ */
+class KeepAliveService : Service() {
+
+ override fun onCreate() {
+ super.onCreate()
+ startForegroundCompat()
+ }
+
+ override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
+ startForeground(NotificationId, buildNotification(), ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE)
+ } else {
+ startForeground(NotificationId, buildNotification())
+ }
+ return START_STICKY
+ }
+
+ override fun onBind(intent: Intent?): IBinder? = null
+
+ private fun startForegroundCompat() = onStartCommand(null, 0, 0)
+
+ private fun buildNotification(): Notification {
+ val channelId = "keepalive"
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ val channel = NotificationChannel(
+ channelId, "Foreground service", NotificationManager.IMPORTANCE_MIN
+ )
+ getSystemService(NotificationManager::class.java).createNotificationChannel(channel)
+ }
+
+ val contentIntent = PendingIntent.getActivity(
+ this, 0,
+ Intent(this, MainActivity::class.java),
+ PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+ )
+
+ return Notification.Builder(this, channelId)
+ .setContentTitle(getString(R.string.app_name))
+ .setContentText(getString(R.string.keep_alive_notification_text))
+ .setSmallIcon(R.drawable.ic_launcher_foreground)
+ .setOngoing(true)
+ .setContentIntent(contentIntent)
+ .build()
+ }
+
+ companion object {
+ private const val NotificationId = 1
+
+ fun start(context: Context) {
+ context.startForegroundService(Intent(context, KeepAliveService::class.java))
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/cosmicide/ui/editor/EditorToolbar.kt b/app/src/main/kotlin/org/cosmicide/ui/editor/EditorToolbar.kt
index f8d50817e..ee47e0eb1 100644
--- a/app/src/main/kotlin/org/cosmicide/ui/editor/EditorToolbar.kt
+++ b/app/src/main/kotlin/org/cosmicide/ui/editor/EditorToolbar.kt
@@ -226,6 +226,10 @@ internal fun EditorToolbar(
}
}
DropdownMenuItem(text = { Text("Editor") }, children = {
+ DropdownMenuItem(text = { Text("Find & Replace") }, onClick = {
+ editor.beginSearchMode()
+ showMenu = false
+ })
DropdownMenuItem(text = { Text("Format") }, onClick = {
editor.formatCodeAsync()
showMenu = false
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 981ce54a8..cb8b96734 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -25,6 +25,7 @@
Create java class
Terminal
+ Cosmic IDE is running in the background
Copy
Quick fixes