Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ flox activate -d flox/local -- ./gradlew <task>
- **Debug APK (arm64, the usual target):** `flox activate -d flox/local -- ./gradlew :app:assembleV8Debug --parallel --max-workers=6`
- **Single unit test:** `flox activate -d flox/local -- ./gradlew :module:test --tests "com.itsaky.androidide.SomeTest"`
- **Module unit tests:** `flox activate -d flox/local -- ./gradlew :testing:unit:test`
- **Fast iteration:** during multi-file/multi-module changes, verify with targeted `:module:compileV8DebugKotlin`/`compileV8DebugJavaWithJavac` invocations (batch several modules into one Gradle call) rather than a full assemble. Reserve `:app:assembleV8Debug` for final end-to-end verification — it's slow (multi-minute) in this multi-module project, and running it after every small change adds up.

When the user names a CI/CD job ("the sonar job", "the analyze workflow"), read `.github/workflows/*.yml` — the YAML is the authoritative gradle/shell invocation. Don't reverse-engineer it from gradle tasks or build files.

Expand All @@ -37,7 +38,7 @@ See **[ARCHITECTURE.md](ARCHITECTURE.md)** — the single source of truth for th
- **Avoid new dependencies** — the build almost certainly already has what's needed. Check `gradle/libs.versions.toml` and `build.gradle.kts` first.
- **Persistence:** prefer **Room** for relational data and the filesystem/preferences for settings; raw SQLite only for justified exceptions — see [ADR 0001](docs/adr/0001-prefer-room-for-persistence.md).
- **Protect the two Android system bars** in any UI work: the top status bar (clock, notifications, status icons) and the bottom navigation bar (home, back, recents). Don't draw over or intercept them.
- **Plan and size before building.** Prefer **one PR per ticket/use case** — don't force-split a coherent change (splitting has its own overhead when later edits span the pieces). When a change is large, break it into **reviewable commits** — mechanical/refactor commits separate from behavioral ones — and offer review-by-commit. Treat ~500 LOC / ~10 files as a signal to reach for that commit structure, not a hard cap; the ceiling rises as LLM-assisted review matures.
- **Plan and size before building.** Prefer **one PR per ticket/use case** — don't force-split a coherent change (splitting has its own overhead when later edits span the pieces). When a change is large, break it into **reviewable commits** — mechanical/refactor commits separate from behavioral ones — and offer review-by-commit. Treat ~500 LOC / ~10 files as a signal to reach for that commit structure, not a hard cap; the ceiling rises as LLM-assisted review matures. For staged multi-commit refactors (e.g. removing a dependency across many files/modules), order stages easiest-to-hardest and independently compile/test each stage (see Build & test's fast-iteration guidance) before moving to the next, so a failure is isolated to the stage that caused it.
- **Keep docs in step with code.** When you change code, update the docs that describe it in the same change — a module's `README.md`, `ARCHITECTURE.md`, or an ADR — so a doc never outlives the API it documents (see REVIEW.md, Code quality). If the doc fix is out of scope, file a ticket rather than let it drift.
- `.androidide_root` is a sentinel file tests use to locate the project root — don't delete it.
- Avoid http or https links which go off-device. When such links are unavoidable, warn the user beforehand and offer to cancel the action.
Expand Down
1 change: 0 additions & 1 deletion actions/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ dependencies {
implementation(libs.common.editor)
implementation(libs.common.kotlin)
implementation(libs.common.kotlin.coroutines.android)
implementation(libs.common.utilcode)
implementation(libs.google.auto.service.annotations)

implementation(libs.androidx.core.ktx)
Expand Down
1 change: 0 additions & 1 deletion app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,6 @@ dependencies {

implementation(platform(libs.sora.bom))
implementation(libs.common.editor)
implementation(libs.common.utilcode)
implementation(libs.common.glide)
implementation(libs.common.jsoup)
implementation(libs.common.kotlin.coroutines.android)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
package com.itsaky.androidide.actions


import com.blankj.utilcode.util.FileIOUtils
import com.itsaky.androidide.actions.observers.FileActionObserver
import com.itsaky.androidide.eventbus.events.file.FileCreationEvent
import com.itsaky.androidide.utils.FileIOUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
Expand All @@ -13,33 +12,39 @@ import org.greenrobot.eventbus.EventBus
import java.io.File

class FileActionManager {
private val scope = CoroutineScope(Dispatchers.IO)
private val scope = CoroutineScope(Dispatchers.IO)

fun createFile(baseDir: File, path: String, content: String, observer: FileActionObserver? = null) {
scope.launch {
val result = try {
val targetFile = File(baseDir, path)
fun createFile(
baseDir: File,
path: String,
content: String,
observer: FileActionObserver? = null,
) {
scope.launch {
val result =
try {
val targetFile = File(baseDir, path)

val formattedContent = StringEscapeUtils.unescapeJava(content)
if (!FileIOUtils.writeFileFromString(targetFile, formattedContent)) {
Result.failure(Exception("Failed to write to file at path: $path"))
} else {
EventBus.getDefault().post(FileCreationEvent(targetFile))
Result.success(targetFile)
}
} catch (e: Exception) {
Result.failure(e)
}
val formattedContent = StringEscapeUtils.unescapeJava(content)
if (!FileIOUtils.writeFileFromString(targetFile, formattedContent)) {
Result.failure(Exception("Failed to write to file at path: $path"))
} else {
EventBus.getDefault().post(FileCreationEvent(targetFile))
Result.success(targetFile)
}
} catch (e: Exception) {
Result.failure(e)
}

withContext(Dispatchers.Main) {
if (result.isSuccess) {
val file = result.getOrNull()
observer?.onActionSuccess("File '${file?.name}' created successfully.", file)
} else {
val error = result.exceptionOrNull()?.message ?: "An unknown error occurred."
observer?.onActionFailure("Error: $error")
}
}
}
}
withContext(Dispatchers.Main) {
if (result.isSuccess) {
val file = result.getOrNull()
observer?.onActionSuccess("File '${file?.name}' created successfully.", file)
} else {
val error = result.exceptionOrNull()?.message ?: "An unknown error occurred."
observer?.onActionFailure("Error: $error")
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,28 +18,29 @@
package com.itsaky.androidide.actions.filetree

import android.content.Context
import com.blankj.utilcode.util.ClipboardUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.idetooltips.TooltipTag
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.utils.copyToClipboard
import com.itsaky.androidide.utils.flashSuccess

/**
* Action to copy the absolute path of the selected file.
*
* @author Akash Yadav
*/
class CopyPathAction(context: Context, override val order: Int) :
BaseFileTreeAction(context, labelRes = R.string.copy_path, iconRes = R.drawable.ic_copy) {
class CopyPathAction(
context: Context,
override val order: Int,
) : BaseFileTreeAction(context, labelRes = R.string.copy_path, iconRes = R.drawable.ic_copy) {
override val id: String = "ide.editor.fileTree.copyPath"

override val id: String = "ide.editor.fileTree.copyPath"
override fun retrieveTooltipTag(isAlternateContext: Boolean): String =
TooltipTag.PROJECT_ITEM_COPYPATH
override fun retrieveTooltipTag(isAlternateContext: Boolean): String = TooltipTag.PROJECT_ITEM_COPYPATH

override suspend fun execAction(data: ActionData) {
val file = data.requireFile()
ClipboardUtils.copyText("[AndroidIDE] Copied File Path", file.absolutePath)
flashSuccess(R.string.copied)
}
override suspend fun execAction(data: ActionData) {
val file = data.requireFile()
data[Context::class.java]!!.copyToClipboard(file.absolutePath, label = "[AndroidIDE] Copied File Path")
flashSuccess(R.string.copied)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ package com.itsaky.androidide.actions.filetree

import android.app.ProgressDialog
import android.content.Context
import com.blankj.utilcode.util.FileUtils
import com.itsaky.androidide.actions.ActionData
import com.itsaky.androidide.actions.requireFile
import com.itsaky.androidide.eventbus.events.file.FileDeletionEvent
Expand All @@ -28,6 +27,7 @@ import com.itsaky.androidide.projects.FileManager
import com.itsaky.androidide.resources.R
import com.itsaky.androidide.tasks.executeAsync
import com.itsaky.androidide.utils.DialogUtils
import com.itsaky.androidide.utils.FileUtils
import com.itsaky.androidide.utils.FlashType
import com.itsaky.androidide.utils.flashMessage
import com.itsaky.androidide.utils.showWithLongPressTooltip
Expand All @@ -39,74 +39,77 @@ import java.io.File
*
* @author Akash Yadav
*/
class DeleteAction(context: Context, override val order: Int) :
BaseFileTreeAction(context, labelRes = R.string.delete_file, iconRes = R.drawable.ic_delete) {
class DeleteAction(
context: Context,
override val order: Int,
) : BaseFileTreeAction(context, labelRes = R.string.delete_file, iconRes = R.drawable.ic_delete) {
override val id: String = "ide.editor.fileTree.delete"

override val id: String = "ide.editor.fileTree.delete"
override fun retrieveTooltipTag(isAlternateContext: Boolean): String =
TooltipTag.PROJECT_ITEM_DELETE
override fun retrieveTooltipTag(isAlternateContext: Boolean): String = TooltipTag.PROJECT_ITEM_DELETE

override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val file = data.requireFile()
val lastHeld = data.getTreeNode()
DialogUtils.newMaterialDialogBuilder(context)
.setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { dialogInterface, _ ->
dialogInterface.dismiss()
@Suppress("DEPRECATION")
val progressDialog =
ProgressDialog.show(context, null, context.getString(R.string.please_wait), true, false)
executeAsync({ FileUtils.delete(file) }) {
progressDialog.dismiss()
override suspend fun execAction(data: ActionData) {
val context = data.requireActivity()
val file = data.requireFile()
val lastHeld = data.getTreeNode()
DialogUtils
.newMaterialDialogBuilder(context)
.setNegativeButton(R.string.no, null)
.setPositiveButton(R.string.yes) { dialogInterface, _ ->
dialogInterface.dismiss()
@Suppress("DEPRECATION")
val progressDialog =
ProgressDialog.show(context, null, context.getString(R.string.please_wait), true, false)
executeAsync({ FileUtils.delete(file) }) {
progressDialog.dismiss()

val deleted = it ?: false
val deleted = it ?: false

flashMessage(
if (deleted) R.string.deleted else R.string.delete_failed,
if (deleted) FlashType.SUCCESS else FlashType.ERROR
)
flashMessage(
if (deleted) R.string.deleted else R.string.delete_failed,
if (deleted) FlashType.SUCCESS else FlashType.ERROR,
)

if (!deleted) {
return@executeAsync
}
if (!deleted) {
return@executeAsync
}

notifyFileDeleted(file, context)
notifyFileDeleted(file, context)

if (lastHeld != null) {
val parent = lastHeld.parent
parent.deleteChild(lastHeld)
requestExpandNode(parent)
} else {
requestFileListing()
}
if (lastHeld != null) {
val parent = lastHeld.parent
parent.deleteChild(lastHeld)
requestExpandNode(parent)
} else {
requestFileListing()
}

val frag = context.getEditorForFile(file)
if (frag != null) {
context.closeFile(context.findIndexOfEditorByFile(frag.file))
}
}
}
.setTitle(R.string.title_confirm_delete)
.setMessage(
context.getString(
R.string.msg_confirm_delete,
String.format("%s [%s]", file.name, file.absolutePath)
)
)
.setCancelable(false)
.showWithLongPressTooltip(
context = context,
tooltipTag = TooltipTag.PROJECT_CONFIRM_DELETE
)
}
val frag = context.getEditorForFile(file)
if (frag != null) {
context.closeFile(context.findIndexOfEditorByFile(frag.file))
}
}
}.setTitle(R.string.title_confirm_delete)
.setMessage(
context.getString(
R.string.msg_confirm_delete,
String.format("%s [%s]", file.name, file.absolutePath),
),
).setCancelable(false)
.showWithLongPressTooltip(
context = context,
tooltipTag = TooltipTag.PROJECT_CONFIRM_DELETE,
)
}

private fun notifyFileDeleted(file: File, context: Context) {
val deletionEvent = FileDeletionEvent(file)
private fun notifyFileDeleted(
file: File,
context: Context,
) {
val deletionEvent = FileDeletionEvent(file)

// Notify FileManager first
FileManager.onFileDeleted(deletionEvent)
// Notify FileManager first
FileManager.onFileDeleted(deletionEvent)

EventBus.getDefault().post(deletionEvent.putData(context))
}
EventBus.getDefault().post(deletionEvent.putData(context))
}
}
Loading
Loading