Skip to content

Commit 39677cd

Browse files
Merge branch 'stage' into ADFA-5640-rename-Extension-Manager-to-Addon-Manager
2 parents 0f40bac + bb9efab commit 39677cd

642 files changed

Lines changed: 103774 additions & 3647 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/analyze.yml‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ jobs:
105105
# the unit-test compile path.
106106
FIREBASE_CONSOLE_URL: ${{ secrets.FIREBASE_CONSOLE_URL }}
107107
GLITCHTIP_DSN: ${{ secrets.GLITCHTIP_DSN }}
108+
# The aapt2/d8/Compose regression tests (ADFA-4128 bugs 5/6/8) are
109+
# assumption-guarded, so on a runner without an Android SDK they would skip
110+
# green and take that coverage with them. This turns an absent toolchain
111+
# into a hard failure instead. The runner does have an SDK - Assemble V8
112+
# Debug above could not run otherwise.
113+
REQUIRE_BUILD_TOOLCHAIN: "1"
108114
run: flox activate -d flox/base -- ./gradlew :testing:tooling:assemble :testing:common:assemble sonarqube --info --no-build-cache -x lint --continue
109115

110116
- name: Upload JaCoCo report
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
name: Spotless apply
2+
3+
# For contributors without a shell (e.g. editing in the GitHub web UI): runs spotlessApply on a
4+
# branch and appends the result as a standalone commit. The push uses GITHUB_TOKEN, so it does not
5+
# start other workflows.
6+
7+
permissions:
8+
contents: write
9+
10+
on:
11+
workflow_dispatch:
12+
inputs:
13+
branch:
14+
description: 'Branch to format (not stage or main)'
15+
required: true
16+
type: string
17+
18+
concurrency:
19+
group: spotless-apply-${{ inputs.branch }}
20+
cancel-in-progress: false
21+
22+
jobs:
23+
spotless_apply:
24+
name: Run spotlessApply and commit
25+
runs-on: self-hosted
26+
timeout-minutes: 30
27+
env:
28+
BRANCH: ${{ inputs.branch }}
29+
30+
steps:
31+
- name: Refuse protected branches
32+
run: |
33+
case "$BRANCH" in
34+
stage|main)
35+
echo "::error::Refusing to push to '$BRANCH'."
36+
exit 1
37+
;;
38+
esac
39+
40+
# fetch-depth 0: the Spotless ratchet compares against origin/stage.
41+
- name: Checkout branch
42+
uses: actions/checkout@v4
43+
with:
44+
ref: ${{ inputs.branch }}
45+
fetch-depth: 0
46+
47+
- name: Check if Nix is installed
48+
run: |
49+
if command -v nix >/dev/null 2>&1; then
50+
echo "nix_installed=true" >> $GITHUB_ENV
51+
else
52+
echo "nix_installed=false" >> $GITHUB_ENV
53+
fi
54+
55+
- name: Install Flox
56+
if: env.nix_installed == 'false'
57+
uses: flox/install-flox-action@v2
58+
59+
- name: Run spotlessApply
60+
run: |
61+
flox activate -d flox/base -- ./gradlew spotlessApply --no-daemon
62+
63+
- name: Commit and push
64+
run: |
65+
if git diff --quiet; then
66+
echo "::notice::Already formatted. Nothing to commit."
67+
exit 0
68+
fi
69+
git config user.name "ADFA"
70+
git config user.email "dev-team@appdevforall.org"
71+
git commit -am "style: spotless reformat, no functional change"
72+
git push origin "HEAD:refs/heads/$BRANCH"
73+
echo "::notice::Pushed $(git rev-parse --short HEAD) to $BRANCH."

‎.gitignore‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,8 @@ sentry.properties
109109
/composite-builds/build-deps/build/
110110

111111
/app/google-services.json
112+
/app/keystore-debug.jks
113+
112114

113115
# Written by the gradle-plugin tests, not by a Gradle task: writeInitScript() in
114116
# gradle-plugin/src/test/.../utils.kt resolves FileProvider.testHomeDir() and creates

‎ARCHITECTURE.md‎

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ Strategy: **layer-and-subsystem based**, not feature-by-feature. The Gradle buil
7171
|---|---|---|
7272
| Application | `app` | The IDE itself — activities, fragments, services, DI, agent, web server. Wires everything together. |
7373
| Build engine | `subprojects:tooling-api*`, `gradle-plugin*`, `subprojects:projects`, `subprojects:builder-model-impl` | Runs a real Gradle build of the user's project out-of-process and streams events back. |
74-
| Language tooling | `lsp:{api,java,kotlin,xml,indexing,refactor-core,ui,…}`, `lexers`, `editor*`, `editor-treesitter` | Language servers, indexing, the Sora-based editor and highlighting, and the tree-sitter document outline (`editor/.../language/outline`, rendered by `app`'s sidebar `OutlineFragment`). `lsp:refactor-core` holds the language-agnostic half of the refactorings (offset spans, block geometry, rewrite composition, name primitives) so `lsp:java` and `lsp:kotlin` share one copy; `lsp:ui` holds the Compose sheets they share. Neither depends on a language server. |
74+
| Quick Build (experimental, ADFA-4128) | `quickbuild:core`, `quickbuild:daemon`, `quickbuild:protocol`, `quickbuild:runtime` | Live-reloads the user's app on every save in seconds, by running it as a generated proxy app instead of doing a full Gradle rebuild. |
75+
| Language tooling | `lsp:{api,java,kotlin,xml,external,indexing,refactor-core,ui,…}`, `lexers`, `editor*`, `editor-treesitter` | Language servers, indexing, the Sora-based editor and highlighting, and the tree-sitter document outline (`editor/.../language/outline`, rendered by `app`'s sidebar `OutlineFragment`). `lsp:refactor-core` holds the language-agnostic half of the refactorings (offset spans, block geometry, rewrite composition, name primitives) so `lsp:java` and `lsp:kotlin` share one copy; `lsp:ui` holds the Compose sheets they share. Neither depends on a language server. `lsp:external` adapts a plugin's stdio language server to `ILanguageServer` over LSP4J; `app`'s `PluginLanguageSupport` installs plugin grammars and servers. |
7576
| UI design tooling | `layouteditor`, `uidesigner`, `xml-inflater`, `vectormaster`, `compose-preview` | Visual/XML design surfaces for the *user's* app. |
7677
| Shell | `termux:{termux-app,termux-shared,termux-view,termux-emulator}` | Embedded Termux shell and terminal. |
7778
| Plugin system | `plugin-api`, `plugin-api:plugin-builder`, `plugin-manager` | In-app plugin SDK + manager — `AndroidManifest.xml` `<meta-data>` contract, permissions, extensions. See [plugin-api.md](docs/plugin-api.md) for the API surface & compatibility policy. |
@@ -80,6 +81,7 @@ Strategy: **layer-and-subsystem based**, not feature-by-feature. The Gradle buil
8081
| Testing | `testing:{android,unit,lsp,tooling,common}` | Shared test harnesses, split by what's under test. |
8182

8283
**Dependency rules (enforced):**
84+
8385
- **`app` depends inward; libraries never depend on `app`.** Subsystems are consumed by `app`, not vice versa.
8486
- **Vendored forks are substituted, not imported ad hoc.** `composite-builds/build-deps` and `build-deps-common` provide forked `javac`/`jdt`/`layoutlib`/etc.; `settings.gradle.kts` substitutes them in for `com.itsaky.androidide.build:*`. Don't add a Maven coordinate for something already substituted.
8587
- **All module config flows through `composite-builds/build-logic`.** Every Android module gets the `v7`/`v8` ABI flavors centrally (`AndroidModuleConf.kt`) — there is no flavorless `assembleDebug`. `:plugin-api` is intentionally excluded from flavors.
@@ -97,6 +99,22 @@ These structural facts shape every module. Day-to-day build *commands* live in `
9799
- **Native lib compression** (ADFA-2306, ADFA-4729). The app manifest hard-codes `android:extractNativeLibs="true"` (required: the installer must materialize libs in `nativeLibraryDir`, e.g. `libshizuku.so` is an executable the adb shell runs from there). That attribute overrides the `jniLibs.useLegacyPackaging` DSL, so AGP packages `lib/<abi>/*.so` deflate-compressed in **every** APK — ~5.9 MB smaller (`libtree-sitter-kotlin.so` alone is 4.18 MB → 339 kB). The trap is the `recompressApk` post-step (release always, debug in CI only): its no-compress lists in `app/build.gradle.kts` must NOT contain `"so"`, or it silently re-stores the libs and undoes the saving — which is what ADFA-2306 fixed for release and ADFA-4729 for CI debug. Locally built debug APKs (including the e2e farm's) never run that step and were always fine.
98100
- **`app` package layout is by concern, not feature:** `activities`, `fragments`, `services`, `di`, `agent`, `viewmodel(s)`, `repositories`, `roomData`, `localWebServer`, `preferences`, `ui` (Compose screens live under `ui/compose`), `templates/manager` (the Manager screen's `.cgt`-parsing data layer, with direct filesystem access to `Environment.TEMPLATES_DIR` — distinct from the plugin-facing `IdeTemplateService` in `plugin-api`/`plugin-manager`), `utils`, ….
99101

102+
## Sibling Repositories & Asset Provenance
103+
104+
Large assets are not in this repo. They are `.gitignore`d and fetched at build time by the `Asset(...)` list in `app/build.gradle.kts`, so a local copy can be stale regardless of git history.
105+
106+
- **[appdevforall/dev-assets](https://github.com/appdevforall/dev-assets)** (private) stores the blobs behind that `Asset(...)` list: the per-ABI Android SDK and bootstrap, the Gradle distribution and API jars, `localMvnRepository.zip`, `core.cgt` (built from its `templates/`), and a copy of `documentation.db` (`debug/`) / `documentation.db.br` (`release/`). It uses no Git LFS: `assets-update.sh` splits each blob into 90 MB parts plus an `.md5`, and `assets-reconstruct.sh` reassembles them. Its `deploy.yml` reconstructs the parts and copies them by `scp` to the web host, which serves them at `https://appdevforall.org/dev-assets/{debug,release}/`. Local builds download over HTTP and check the `.md5`; CI (`isCiCd`) copies the same directory from the host by `scp`. Gradle tasks: `:app:assetsDownloadDebug`, `:app:assetsDownloadRelease`.
107+
- **[appdevforall/OfflineDocumentationTools](https://github.com/appdevforall/OfflineDocumentationTools)** (public) builds and edits `documentation.db` and owns its schema (`docdb-studio`, ingest scripts, the `Dokka-plugin-kdoc2json` Dokka plugin that turns KDoc into JSON for Pebble templates). See [docs/documentation-database.md](docs/documentation-database.md).
108+
- **[appdevforall/addons](https://github.com/appdevforall/addons)** (public; formerly `plugin-examples`) holds the plugins (`plugins/`, each a standalone Gradle project whose `assemblePlugin` builds a `.cgp`) and templates. Its `publish-addons.yml` builds them against this repo's `plugin-api-latest` GitHub Release and publishes the catalog to `https://addons.appdevforall.org` (the `url_discover_plugins` string).
109+
110+
**`documentation.db` provenance.** The canonical copy is on **Google Drive**; OfflineDocumentationTools' workflows edit it there. This repo's CI reads it from Drive, not from dev-assets:
111+
112+
- `compress_docdb.yml` (daily) downloads `documentation.db`, and if its MD5 changed, Brotli-compresses it and uploads `documentation.db.br` back to Drive.
113+
- `release.yml` fetches dev-assets' release assets, then its `download_documentation` job replaces `documentation.db.br` with the Drive copy.
114+
- `generate_assets.yml` (manual) fetches dev-assets' debug assets, replaces `assets/documentation.db` with the Drive copy, builds `assets-arm64-v8a.zip` / `assets-armeabi-v7a.zip` (`:app:assembleAssets`), and uploads them to Drive.
115+
116+
Local builds get `documentation.db` from dev-assets instead, which is updated by hand and can lag Drive.
117+
100118
## Technology Stack
101119

102120
| Concern | Library / Approach |
@@ -106,9 +124,9 @@ These structural facts shape every module. Day-to-day build *commands* live in `
106124
| Asynchronous work | **Kotlin Coroutines + Flow** (`StateFlow`/`SharedFlow`, `viewModelScope`, app-scoped `CoroutineScope(SupervisorJob() + Dispatchers.IO)`); **GreenRobot EventBus** for cross-subsystem events. |
107125
| Networking | Offline-first; no general REST layer. External I/O is **Google GenAI SDK** (Gemini), **on-device llama.cpp**, and **JGit** (git). Retrofit is in the catalog but effectively unused in app code. |
108126
| Database / Persistence | **Room** is the default for relational/queryable data; **filesystem + preferences (DataStore)** for non-relational settings. **Raw SQLite** (`SQLiteDatabase` / `SupportSQLiteOpenHelper`) only for justified exceptions (see policy below). |
109-
| Serialization | `kotlinx.serialization` and Gson. |
110-
| Parceling | Kotlin **`@Parcelize`** (`kotlin-parcelize` plugin) for `Parcelable` data classes — never hand-implement `Parcelable`. Do it manually only if `@Parcelize` genuinely can't express it (custom serialization logic, unsupported member types). |
111-
| AI agent | Google GenAI (cloud) + llama (local), behind `GeminiRepository` / `SwitchableGeminiRepository`, with planner/critic/executor agents in `agent/repository`. |
127+
| Serialization | `kotlinx.serialization` and Gson. |
128+
| Parceling | Kotlin **`@Parcelize`** (`kotlin-parcelize` plugin) for `Parcelable` data classes — never hand-implement `Parcelable`. Do it manually only if `@Parcelize` genuinely can't express it (custom serialization logic, unsupported member types). |
129+
| AI agent | Google GenAI (cloud) + llama (local), behind `GeminiRepository` / `SwitchableGeminiRepository`, with planner/critic/executor agents in `agent/repository`. |
112130

113131
> **Persistence policy (authoritative):** new relational/queryable persistence uses **Room** (`@Entity` + DAO + `RoomDatabase` with explicit migrations, provided via Koin). Non-relational settings use the **filesystem/preferences (DataStore)**. **Raw SQLite is the exception, not the default** — see [ADR 0001](docs/adr/0001-prefer-room-for-persistence.md).
114132
>
@@ -196,13 +214,14 @@ fun onEvent(event: PluginManagerUiEvent) = viewModelScope.launch(Dispatchers.IO)
196214

197215
Test code lives both alongside each module and in the shared `testing:{unit,android,lsp,tooling,common}` harnesses. Run with the flox wrapper, e.g. `flox activate -d flox/local -- ./gradlew :testing:unit:test` or a module's `:module:test --tests "…"`.
198216

199-
| Layer | Runner / Tools | What to test |
200-
|---|---|---|
201-
| Unit (pure JVM) | **JUnit Jupiter (5)**, some legacy **JUnit 4**; assertions via **Google Truth**; mocking via **MockK** (primary) and **Mockito-Kotlin** (legacy) | ViewModels (state transitions over a fake repository), repositories, parsers, builder/tooling logic. Keep these off the device. |
202-
| JVM + Android framework | **Robolectric** | Code needing `Context`/resources/`SQLiteOpenHelper` without an emulator. |
203-
| Instrumented / UI | **Espresso** + **AndroidX Test** + **UiAutomator**, run under **Test Orchestrator**; `mockk-android` for on-device mocks | End-to-end IDE flows (create/build/deploy, editor, terminal). |
217+
| Layer | Runner / Tools | What to test |
218+
| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ |
219+
| Unit (pure JVM) | **JUnit Jupiter (5)**, some legacy **JUnit 4**; assertions via **Google Truth**; mocking via **MockK** (primary) and **Mockito-Kotlin** (legacy) | ViewModels (state transitions over a fake repository), repositories, parsers, builder/tooling logic. Keep these off the device. |
220+
| JVM + Android framework | **Robolectric** | Code needing `Context`/resources/`SQLiteOpenHelper` without an emulator. |
221+
| Instrumented / UI | **Espresso** + **AndroidX Test** + **UiAutomator**, run under **Test Orchestrator**; `mockk-android` for on-device mocks | End-to-end IDE flows (create/build/deploy, editor, terminal). |
204222

205223
Preferences and conventions:
224+
206225
- **Assertions: Google Truth** (`assertThat(x).isEqualTo(...)`) over raw JUnit asserts.
207226
- **Mocking: MockK** for new code; relax it deliberately rather than over-stubbing.
208227
- For UDF ViewModels, drive `onEvent(...)`/method calls against a fake or mocked repository and assert the emitted `UiState` sequence (collect the `StateFlow`); assert effects by collecting the effect `SharedFlow`.

‎CLAUDE.md‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ At 2.0, look for text cut off mid-word, labels overrunning their control, action
5252

5353
See **[ARCHITECTURE.md](ARCHITECTURE.md)** — the single source of truth for the module map, layering/data flow, dependency rules, tech stack (DI, async, persistence, networking), state management, and testing strategy. Don't re-document those here; update ARCHITECTURE.md.
5454

55+
### Sibling repos
56+
57+
CoGo depends on three other `appdevforall` repos; ARCHITECTURE.md (Sibling Repositories & Asset Provenance) explains how they feed the build.
58+
59+
- **[addons](https://github.com/appdevforall/addons)** (public; formerly `plugin-examples`) - the plugins (`.cgp`) and templates published at `addons.appdevforall.org`.
60+
- **[dev-assets](https://github.com/appdevforall/dev-assets)** (private) - large binary blobs (SDK, bootstrap, Gradle, `core.cgt`, a copy of `documentation.db`) served at `appdevforall.org/dev-assets/`.
61+
- **[OfflineDocumentationTools](https://github.com/appdevforall/OfflineDocumentationTools)** (public) - the tooling that builds and edits `documentation.db` and owns its schema.
62+
5563
## Project-specific constraints
5664

5765
- **Avoid new dependencies** — the build almost certainly already has what's needed. Check `gradle/libs.versions.toml` and `build.gradle.kts` first.

‎actions/src/main/java/com/itsaky/androidide/actions/SidebarSlotExceededException.kt‎

Lines changed: 0 additions & 20 deletions
This file was deleted.

‎actions/src/main/java/com/itsaky/androidide/actions/SidebarSlotManager.kt‎

Lines changed: 24 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -5,50 +5,36 @@ import java.util.concurrent.ConcurrentHashMap
55
import java.util.concurrent.atomic.AtomicInteger
66

77
object SidebarSlotManager {
8+
private val builtInItemCount = AtomicInteger(0)
9+
private val reservedPluginSlots = ConcurrentHashMap<String, Int>()
810

9-
const val MAX_NAVIGATION_RAIL_ITEMS = 12
11+
fun setBuiltInItemCount(count: Int) {
12+
require(count >= 0) { "Built-in item count must not be negative" }
13+
builtInItemCount.set(count)
14+
}
1015

11-
private val builtInItemCount = AtomicInteger(0)
12-
private val reservedPluginSlots = ConcurrentHashMap<String, Int>()
16+
fun getBuiltInItemCount(): Int = builtInItemCount.get()
1317

14-
fun setBuiltInItemCount(count: Int) {
15-
require(count in 0..MAX_NAVIGATION_RAIL_ITEMS) {
16-
"Built-in item count must be between 0 and $MAX_NAVIGATION_RAIL_ITEMS"
17-
}
18-
builtInItemCount.set(count)
19-
}
18+
fun getReservedPluginSlotCount(): Int = reservedPluginSlots.values.sum()
2019

21-
fun getBuiltInItemCount(): Int = builtInItemCount.get()
20+
fun getTotalItemCount(): Int = builtInItemCount.get() + getReservedPluginSlotCount()
2221

23-
fun getReservedPluginSlotCount(): Int = reservedPluginSlots.values.sum()
22+
fun getDeclaredSlots(pluginId: String): Int = reservedPluginSlots[pluginId] ?: 0
2423

25-
fun getTotalItemCount(): Int = builtInItemCount.get() + getReservedPluginSlotCount()
24+
fun reservePluginSlots(
25+
pluginId: String,
26+
count: Int,
27+
) {
28+
if (count <= 0) return
29+
reservedPluginSlots[pluginId] = count
30+
}
2631

27-
fun getAvailableSlotsForPlugins(): Int =
28-
(MAX_NAVIGATION_RAIL_ITEMS - builtInItemCount.get() - getReservedPluginSlotCount())
29-
.coerceAtLeast(0)
32+
fun releasePluginSlots(pluginId: String) {
33+
reservedPluginSlots.remove(pluginId)
34+
}
3035

31-
fun canAddPluginItems(count: Int): Boolean = count <= getAvailableSlotsForPlugins()
32-
33-
fun getDeclaredSlots(pluginId: String): Int = reservedPluginSlots[pluginId] ?: 0
34-
35-
@Throws(SidebarSlotExceededException::class)
36-
fun reservePluginSlots(pluginId: String, count: Int) {
37-
if (count <= 0) return
38-
39-
val available = getAvailableSlotsForPlugins()
40-
if (count > available) {
41-
throw SidebarSlotExceededException(count, available, pluginId)
42-
}
43-
reservedPluginSlots[pluginId] = count
44-
}
45-
46-
fun releasePluginSlots(pluginId: String) {
47-
reservedPluginSlots.remove(pluginId)
48-
}
49-
50-
fun reset() {
51-
builtInItemCount.set(0)
52-
reservedPluginSlots.clear()
53-
}
36+
fun reset() {
37+
builtInItemCount.set(0)
38+
reservedPluginSlots.clear()
39+
}
5440
}

0 commit comments

Comments
 (0)