Skip to content
Merged
18 changes: 18 additions & 0 deletions app/src/main/kotlin/org/cosmicide/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import org.cosmicide.common.Analytics
import org.cosmicide.common.IndexManager
import org.cosmicide.common.MemoryMonitor
import org.cosmicide.common.MemoryUtils
import org.cosmicide.common.Prefs
import org.cosmicide.editor.lsp.handleLspShowDocument
import org.cosmicide.plugin.CosmicPluginHost
Expand Down Expand Up @@ -81,6 +84,8 @@ import java.util.TimeZone

class App : Application() {

private lateinit var memoryMonitor: MemoryMonitor

companion object {

/**
Expand Down Expand Up @@ -117,6 +122,8 @@ class App : Application() {
instance = WeakReference(this)
HookManager.context = WeakReference(this)

IndexManager.init(this)

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
HiddenApiBypass.addHiddenApiExemptions()
}
Expand All @@ -125,13 +132,24 @@ class App : Application() {

CosmicPluginHost.init(this)

memoryMonitor = MemoryMonitor(intervalMs = 5000L) { pressure ->
if (pressure == MemoryUtils.MemoryPressure.CRITICAL) {
android.util.Log.w("App", "Critical memory pressure: throttling LSP servers")
}
}
memoryMonitor.start()

Analytics.setAnalyticsCollectionEnabled(Prefs.analyticsEnabled)
applyLSP4JHooks()

}

override fun onTerminate() {
super.onTerminate()
if (::memoryMonitor.isInitialized) {
memoryMonitor.stop()
}
IndexManager.shutdown()
}

fun loadTextmateTheme() {
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/kotlin/org/cosmicide/common/AppDispatchers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.cosmicide.common

import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.asCoroutineDispatcher

object AppDispatchers {
val IO = MemoryAwareDispatchers.IO.asCoroutineDispatcher()
val Main = Dispatchers.Main
}
61 changes: 22 additions & 39 deletions app/src/main/kotlin/org/cosmicide/editor/lsp/LspEditorAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import kotlinx.coroutines.CoroutineScope
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
Expand All @@ -45,9 +47,6 @@ import java.io.InputStream
import java.io.OutputStream
import java.net.URI
import java.net.URL
import java.nio.file.AtomicMoveNotSupportedException
import java.nio.file.Files
import java.nio.file.StandardCopyOption
import java.security.MessageDigest
import java.util.concurrent.CompletableFuture
import java.util.concurrent.ConcurrentHashMap
Expand All @@ -65,7 +64,7 @@ fun CodeEditor.configureLspLanguage(
val lspProject = LspProjects.forRoot(request.project.root.absolutePath)
ensureRequestTimeouts()

CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(AppDispatchers.IO).launch {
ensureInitializationTimeout(definition.initializationTimeoutMillis)
val fileExtensions = definition.fileExtensions.toList()
val requestedFileExtension = fileExtensions.firstOrNull {
Expand Down Expand Up @@ -176,7 +175,7 @@ fun CodeEditor.configureLspLanguage(

fun CodeEditor.disposeLspLanguage() {
val lspEditors = LspProjects.editorsFor(this)
CoroutineScope(Dispatchers.IO).launch {
CoroutineScope(AppDispatchers.IO).launch {
lspEditors.forEach(LspEditor::dispose)
}
}
Expand Down Expand Up @@ -448,23 +447,14 @@ private fun createTextMateLanguage(
return createTextMateLanguage(definition, grammarText)
}

val cacheFile = grammarCacheFile(context, grammarLink)
var cachedGrammarText: String? = null
if (cacheFile.isFile) {
cachedGrammarText = runCatching { cacheFile.inputStream().readGrammarText() }
.onFailure {
Log.w(TAG, "Discarding unreadable grammar cache ${cacheFile.name}", it)
cacheFile.delete()
}
.getOrNull()
}
var cachedGrammarText: String? = readGrammarViaIndexManager(grammarLink)

if (cachedGrammarText != null && isTextMateGrammarCacheFresh(cacheFile)) {
if (cachedGrammarText != null) {
try {
return createTextMateLanguage(definition, cachedGrammarText)
} catch (e: Exception) {
Log.w(TAG, "Discarding invalid grammar cache ${cacheFile.name}", e)
cacheFile.delete()
Log.w(TAG, "Discarding invalid grammar cache for $grammarLink", e)
IndexManager.invalidateProject("grammar:$grammarLink")
cachedGrammarText = null
}
}
Expand All @@ -479,7 +469,7 @@ private fun createTextMateLanguage(

return try {
createTextMateLanguage(definition, refreshedGrammarText).also {
runCatching { cacheGrammar(cacheFile, refreshedGrammarText) }
runCatching { cacheGrammarViaIndexManager(grammarLink, refreshedGrammarText) }
.onFailure { error ->
Log.w(TAG, "Unable to cache grammar from $grammarLink", error)
}
Expand Down Expand Up @@ -552,27 +542,20 @@ private fun grammarCacheFile(context: Context, grammarLink: String): File {
.resolve("$cacheKey.grammar")
}

private fun cacheGrammar(cacheFile: File, grammarText: String) {
val temporaryFile = File.createTempFile(cacheFile.name, ".tmp", cacheFile.parentFile)
try {
temporaryFile.writeText(grammarText, Charsets.UTF_8)
try {
Files.move(
temporaryFile.toPath(),
cacheFile.toPath(),
StandardCopyOption.ATOMIC_MOVE,
StandardCopyOption.REPLACE_EXISTING
)
} catch (_: AtomicMoveNotSupportedException) {
Files.move(
temporaryFile.toPath(),
cacheFile.toPath(),
StandardCopyOption.REPLACE_EXISTING
)
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")
}
cacheFile.setLastModified(System.currentTimeMillis())
} finally {
temporaryFile.delete()
String(segment.readBlock(0, segment.size.toInt()), Charsets.UTF_8)
} catch (e: Exception) {
null
}
}

Expand Down
7 changes: 6 additions & 1 deletion app/src/main/kotlin/org/cosmicide/model/ProjectRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import org.cosmicide.project.Project
import org.cosmicide.project.ProjectTypeProvider
import org.cosmicide.util.FileUtil
import java.io.File
import java.util.concurrent.ConcurrentHashMap

interface ProjectRepository {
fun projects(): List<Project>
Expand All @@ -17,13 +18,16 @@ internal class FileSystemProjectRepository(
private val projectTypeProviders: () -> List<ProjectTypeProvider> = { emptyList() }
) : ProjectRepository {
private val root = projectsDirectory.canonicalFile
private val languageCache = ConcurrentHashMap<String, Language>()

override fun projects(): List<Project> = root
.listFiles { file -> file.isDirectory }
?.sortedByDescending(File::lastModified)
.orEmpty()
.map { projectRoot ->
Project(projectRoot, detectLanguage(projectRoot))
Project(projectRoot, languageCache.getOrPut(projectRoot.absolutePath) {
detectLanguage(projectRoot)
})
}

override fun delete(project: Project) {
Expand All @@ -32,6 +36,7 @@ internal class FileSystemProjectRepository(
"Projects can only be deleted from the configured projects directory"
}
require(target.isDirectory) { "Project no longer exists: ${project.name}" }
languageCache.remove(project.root.absolutePath)
check(target.deleteRecursively()) { "Could not delete ${project.name}" }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.ensureActive
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.cosmicide.common.AppDispatchers
import org.cosmicide.common.Prefs
import org.cosmicide.exec.ProcessExecutor
import org.cosmicide.exec.linux.LinuxProcessRunner
Expand Down Expand Up @@ -44,15 +45,15 @@ internal class AndroidCommandExecutionService(context: Context) :
override suspend fun execute(
request: CommandRequest,
onOutput: (String) -> Unit
): CommandResult = withContext(Dispatchers.IO) {
): CommandResult = withContext(AppDispatchers.IO) {
require(request.workingDirectory.isDirectory) {
"Working directory does not exist: ${request.workingDirectory.absolutePath}"
}

val process = start(request, redirectErrorStream = true)
val captured = StringBuilder()
val callerJob = currentCoroutineContext()[Job]
val cancellationWatcher = CoroutineScope(Dispatchers.IO).launch {
val cancellationWatcher = CoroutineScope(AppDispatchers.IO).launch {
while (callerJob?.isActive == true) delay(CANCELLATION_POLL_MILLIS.milliseconds)
if (callerJob?.isCancelled == true && process.isAlive) process.destroy()
}
Expand Down
23 changes: 18 additions & 5 deletions app/src/main/kotlin/org/cosmicide/plugin/CosmicPluginHost.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ object CosmicPluginHost {
@Volatile
private var initialized = false

@Volatile
private var pluginsLoaded = false

var pluginManager: AndroidPluginManager? = null
private set

Expand Down Expand Up @@ -76,23 +79,33 @@ object CosmicPluginHost {
throwable
)
}
manager.loadInstalledPlugins().forEach { result ->
result.onFailure { descriptorId, reason, throwable ->
Log.w(TAG, "Failed to load plugin $descriptorId: $reason", throwable)
}
}
}

initialized = true
}
}

private fun ensurePluginsLoaded() {
if (pluginsLoaded) return
synchronized(this) {
if (pluginsLoaded) return
pluginManager?.loadInstalledPlugins()?.forEach { result ->
result.onFailure { descriptorId, reason, throwable ->
Log.w(TAG, "Failed to load plugin $descriptorId: $reason", throwable)
}
}
pluginsLoaded = true
}
}

fun <T> enabledExtensions(point: org.cosmicide.plugin.api.ExtensionPoint<T>): List<T>
where T : Any, T : ConfigurableExtension {
ensurePluginsLoaded()
return extensionRegistry.extensions(point).filter(extensionSettings::isEnabled)
}

fun configurableExtensions(): List<ExtensionSettingsItem> {
ensurePluginsLoaded()
return buildList {
addRegistrations(EditorExtensionPoints.LANGUAGE_PROVIDER, "Editor languages")
addRegistrations(EditorExtensionPoints.LSP_SERVER_PROVIDER, "Language servers")
Expand Down
7 changes: 4 additions & 3 deletions app/src/main/kotlin/org/cosmicide/plugin/PluginMarketplace.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.cosmicide.common.AppDispatchers
import org.cosmicide.plugin.api.PluginDescriptor
import org.cosmicide.plugin.api.PluginHandle
import org.cosmicide.plugin.api.PluginLoadResult
Expand Down Expand Up @@ -51,13 +52,13 @@ class PluginMarketplace(
private val appContext = context.applicationContext

suspend fun fetch(repositoryUrl: String): List<PluginRepositoryEntry> =
withContext(Dispatchers.IO) {
withContext(AppDispatchers.IO) {
val json = downloadText(repositoryUrl, MAX_INDEX_BYTES)
parsePluginRepository(json)
}

suspend fun install(entry: PluginRepositoryEntry): PluginInstallResult =
withContext(Dispatchers.IO) {
withContext(AppDispatchers.IO) {
val pluginManager = checkNotNull(manager()) { "Plugin runtime is not initialized" }
val pluginRoot = FileUtil.pluginDir.apply { mkdirs() }
val token = UUID.randomUUID().toString()
Expand Down Expand Up @@ -120,7 +121,7 @@ class PluginMarketplace(
}
}

suspend fun uninstall(pluginId: String) = withContext(Dispatchers.IO) {
suspend fun uninstall(pluginId: String) = withContext(AppDispatchers.IO) {
val pluginManager = checkNotNull(manager()) { "Plugin runtime is not initialized" }
val target = FileUtil.pluginDir.resolve(pluginId)
require(target.isDirectory) {
Expand Down
39 changes: 39 additions & 0 deletions common/src/main/java/org/cosmicide/common/BlockCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package org.cosmicide.common

import java.util.LinkedHashMap

class BlockCache(private val maxBlocks: Int = 256) {

private val cache = object : LinkedHashMap<String, ByteArray>(64, 0.75f, true) {
override fun removeEldestEntry(eldest: MutableMap.MutableEntry<String, ByteArray>?): Boolean {
return size > maxBlocks
}
}

fun getOrLoad(key: String, loader: () -> ByteArray): ByteArray {
synchronized(cache) {
cache[key]?.let { return it }
}

val data = loader()
synchronized(cache) {
cache[key] = data
}
return data
}

fun invalidate(key: String) {
synchronized(cache) {
cache.remove(key)
}
}

fun clear() {
synchronized(cache) {
cache.clear()
}
}

val size: Int
get() = synchronized(cache) { cache.size }
}
Loading
Loading