Skip to content

feat(android): Fabric library scaffold + MVP (closes #33, refs #32)#34

Merged
LeslieOA merged 2 commits into
developfrom
feat/android-mvp
May 13, 2026
Merged

feat(android): Fabric library scaffold + MVP (closes #33, refs #32)#34
LeslieOA merged 2 commits into
developfrom
feat/android-mvp

Conversation

@LeslieOA

Copy link
Copy Markdown
Member

First slice of Android support (#32). Closes #33.

What's in this PR

Library

  • Wipe legacy Expo-module stub in `android/`. New bare-RN Fabric component setup wired to RN's gradle plugin, targeting react-native 0.81.
  • `SourceEditorPackage.kt` (autolinked entry) + `SourceEditorViewManager.kt` (codegen-driven, `SourceEditorManagerInterface` impl) + `SourceEditorView.kt` (FrameLayout owning a single Sora-Editor `CodeEditor` child). Event subscriptions on `ContentChangeEvent` / `SelectionChangeEvent` dispatch Fabric events via `UIManagerHelper.getEventDispatcherForReactTag`.
  • Sora-Editor pinned to 0.23.6 (latest Maven Central — GitHub's 0.24.x tags are unpublished pre-releases). LGPL-2.1, consumed dynamically as a Gradle `implementation` AAR.
  • `AndroidManifest.xml` is namespace-mode (no `package=` attr; AGP 7.3+ convention).
  • MVP prop/event surface only: `text`, `editable`, `onChangeText`, `onSelectionChange`, `focus`/`blur`. `font` / `theme` / `language` highlighting / `lineNumbers` / `contentInsets` are stubbed no-ops on the ViewManager interface; future PRs in Android support — track #32 wire them.

Example app (renamed: `ios-app` → `expo-app`)

  • Same Expo CNG project now serves both iOS and Android (it's the same toolchain). The bare RN `macos-app` stays separate.
  • `app.json`: add android block (package id, adaptive icon, minSdk 24, newArchEnabled).
  • `App.android.tsx`: minimal RN-core demo (no Expo UI — that's iOS/macOS only). Language picker via core Pressables, status bar with live selection + focus/blur buttons.
  • Root `package.json` scripts: `android:` mirroring `ios:` (install, prebuild, start, clear, run, run:device, dev) — both target the same expo-app dir.

Config

  • `react-native.config.js`: `dependency.platforms.android` flipped from `null` to `{}` so autolinking picks up `SourceEditorPackage`.
  • `package.json` codegen: `android.javaPackageName: "sh.workspace.sourceeditor"`.

Test plan

  • `./gradlew :app:assembleDebug` from `example/expo-app/android` — BUILD SUCCESSFUL, APK produced.
  • Expo's `react-native-config` autolinking (`--platform android`) discovers our library with the right package import path.
  • Needs your machine: `npm run android:install && npm run android:prebuild && npm run android:dev` on an emulator/device. `App.android.tsx` should render the editor, language chips switch, status bar updates as you type/select, focus/blur buttons work.
  • Needs your machine: `npm run ios:run` smoke test to confirm the `ios-app → expo-app` rename didn't regress iOS.

Out of scope (separate PRs against #32)

`font`, `theme`, `language` highlighting, `lineNumbers`, `contentInsets`. CI Android job. Docs/CHANGELOG.

🤖 Generated with Claude Code

LeslieOA and others added 2 commits May 13, 2026 16:18
Wipe the legacy Expo-module stub in android/; replace with a bare-RN
Fabric component library targeting react-native 0.81. Backing library
is Sora-Editor 0.24.5 (LGPL-2.1, consumed as an unmodified AAR).

MVP surface only — text, editable, onChangeText, onSelectionChange,
focus/blur. Future PRs wire font, theme, language highlighting,
lineNumbers, contentInsets.

Files
- android/build.gradle: bare RN library setup. com.facebook.react
  plugin, modern react-android coordinate (not legacy react-native:+),
  Sora-Editor dep via Maven Central. minSdk 24 to match Sora's floor.
- android/gradle.properties: namespaced fallback props for safeExtGet.
- android/src/main/AndroidManifest.xml: namespace-mode (no `package=`,
  required by AGP 7.3+).
- src/main/java/sh/workspace/sourceeditor/SourceEditorPackage.kt:
  autolinked entry. TurboReactPackage with empty module info provider
  (this library is Fabric-view-only, no TurboModules).
- src/main/java/sh/workspace/sourceeditor/SourceEditorViewManager.kt:
  codegen-driven ViewGroupManager. Implements SourceEditorManagerInterface
  by delegating to SourceEditorView; future props left as no-op
  stubs to satisfy the interface.
- src/main/java/sh/workspace/sourceeditor/SourceEditorView.kt:
  FrameLayout owning a single Sora CodeEditor child. Subscribes to
  ContentChangeEvent + SelectionChangeEvent, dispatches Fabric events.
- src/main/java/sh/workspace/sourceeditor/events/{OnChangeTextEvent,
  OnSelectionChangeEvent}.kt: Fabric Event subclasses with codegen-
  compatible "topChangeText" / "topSelectionChange" names.

Config
- package.json: codegen `android.javaPackageName`,
  files[] includes android/build.gradle + gradle.properties + src/,
  android:* dev/run scripts mirroring macos:*, "android" keyword.
- react-native.config.js: dependency.platforms.android flipped from
  null to {} so autolinking picks up SourceEditorPackage.

Verification of the AAR build + example app + first run on an Android
emulator is the next checkpoint. Not yet wired into CI.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
#33)

Continuation of the bare-RN Fabric library scaffold from the previous
commit. Android library now compiles cleanly inside the Expo CNG host.

Renames example/ios-app → example/expo-app — the Expo CNG project now
serves both iOS and Android (it's the same toolchain). The bare RN
macos-app stays separate (Expo doesn't support macOS).

Library
- android/build.gradle: pin Sora-Editor to 0.23.6 (the latest on Maven
  Central; GitHub's 0.24.x tags are pre-release builds not published
  there).
- android/src/.../SourceEditorView.kt: drop the (CharSequence, Boolean)
  setText overload — Sora's signature is (CharSequence, Bundle?), the
  Bundle is for state restoration, not a cursorAtEnd flag.

Example app
- example/expo-app/app.json: add android block (package id, adaptive
  icon), expo-build-properties android (minSdk 24 to match Sora's floor,
  newArchEnabled). App name dropped the "(iOS)" suffix; slug renamed to
  source-editor-example.
- example/expo-app/App.android.tsx: NEW. Minimal RN-core demo (no Expo
  UI — that's iOS/macOS only). Wires SourceEditor with text/editable,
  language picker via core Pressables, status bar showing live
  selection + focus/blur buttons.
- example/expo-app/metro.config.js: comment update — no longer iOS-only.
- example/expo-app/package.json: name → "expo-app".

Root
- package.json: drop ios:* paths from `example/ios-app/` to
  `example/expo-app/`. Add android:* scripts mirroring ios:* (install,
  prebuild, start, clear, run, run:device, dev) targeting the same
  Expo project.

Verified
- ./gradlew :app:assembleDebug → BUILD SUCCESSFUL. APK produced.
- Expo's react-native-config autolinking (`--platform android`) sees
  @workspace-sh/react-native-source-editor with the right package import
  path (`import sh.workspace.sourceeditor.SourceEditorPackage;`).

Not yet verified (needs an emulator/device — yours):
- App.android.tsx renders + actually round-trips text changes.
- iOS path: the rename is safe (existing prebuild keeps working until
  next ios:prebuild), but worth a smoke `npm run ios:run` to confirm.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@LeslieOA LeslieOA merged commit 464b88e into develop May 13, 2026
1 of 2 checks passed
LeslieOA added a commit that referenced this pull request May 29, 2026
#44)

After this lands, every code-side and docs-side sub-task on #32 is
done; the tracking issue closes.

README.md
- Platform table: Android → "Shipping (Fabric / New Architecture)" at
  API 24. Drop "Stub only" row.
- Status section: name all three platforms.
- Quick start: clarify cocoapods-spm is iOS-only; macOS uses RN's
  first-party SPM, Android pulls Sora-Editor from Maven Central.
- Plugin snippet: add expo-build-properties android entries
  (minSdkVersion + newArchEnabled).
- Roadmap: drop "Android (TextKit alternative)" — it's done.
- Repo layout: describe android/ proper + the Expo CNG-shared
  example/expo-app/ (was example/ios-app/).
- Add "Running the Android example" section mirroring iOS scripts.
- License section: add Sora-Editor + LGPL boundary note.

docs/installation.md
- Add Android row to deployment-targets table (API 24).
- New "Backing libraries" subsection summarising STTextView vs Sora.
- Scope the SPM bridging section explicitly to iOS / macOS.
- New "Android (Expo CNG)" section: documents the autolinking-from-
  Maven flow + the coreLibraryDesugaring requirement that our config
  plugin auto-injects.
- New "Bare React Native — Android" section with the manual
  build.gradle snippet for non-Expo consumers.
- New "License note for Android" calling out the LGPL inheritance.
- Working examples block: ios-app → expo-app, add Android.

docs/usage.md
- `language` prop note: spell out the Android grammar source (VS Code
  TextMate via Sora) and the HTML caveat (no nested CSS/JS recursion
  on Android, unlike STTextView's attributed text).
- `lineNumbers` note: now applies to all platforms.
- `contentInsets` note: spell out the Android dp→px mapping.

docs/examples.md
- Add Android row to platform table — same expo-app/ project as iOS.
- Refresh layout/toolchain narrative for the Expo-shared model.
- Add android:run script line.

CHANGELOG.md
- Unreleased entry covering #34, #36, #38, #40, #42 plus the
  ios-app→expo-app rename.

Stacked on feat/ci-android (#42); merge order: #36#38#40#42
→ this.
@LeslieOA LeslieOA mentioned this pull request May 29, 2026
7 tasks
LeslieOA added a commit that referenced this pull request May 29, 2026
…42)

* feat(android): TextMate syntax highlighting (closes #35, refs #32)

Wire Sora-Editor's TextMate language module so the cross-platform
\`language\` prop (markdown / json / javascript / typescript / html)
actually highlights on Android. Plaintext stays unhighlighted, matching
iOS/macOS behaviour.

Library
- android/build.gradle: add language-textmate dep + core library
  desugaring (joni regex engine needs java.time on minSdk 24).
- android/src/.../SoraTextMate.kt: NEW. Lazy one-shot setup of Sora's
  GrammarRegistry + ThemeRegistry, with a string→scope map and an
  apply() that wires editor.colorScheme + editor.setEditorLanguage.
  Color-scheme-before-language ordering matters — Sora silently no-ops
  if a TextMateLanguage is set against a non-TextMateColorScheme.
- android/src/.../SourceEditorViewManager.kt: setLanguage now delegates
  to SoraTextMate.apply (was no-op).
- android/src/.../SourceEditorView.kt: forward setLanguage from manager.

Assets
- android/src/main/assets/textmate/{markdown,json,javascript,typescript,
  html}/*.tmLanguage.json: grammars from VS Code 1.94.0
  (microsoft/vscode/extensions/<lang>-basics/syntaxes/), MIT-licensed.
- android/src/main/assets/textmate/themes/dark_modern.json: VS Code's
  Dark Modern theme — closest off-the-shelf TextMate theme to the
  iOS/macOS Highlighter palette.
- android/src/main/assets/textmate/languages.json: manifest mapping
  language names to scope + grammar paths, consumed by Sora's
  GrammarRegistry.loadGrammars().

Asset payload: ~625KB grammars + 5KB theme. Joni regex engine adds
~1.2MB AAR-uncompressed via the language-textmate dep. Justified for a
source editor.

Expo config plugin
- app.plugin.js: extend with withAppBuildGradle modifier that injects
  coreLibraryDesugaringEnabled + the desugar_jdk_libs dep into
  android/app/build.gradle. Sora's language-textmate AAR declares the
  desugaring requirement; without it consumer build fails at
  checkDebugAarMetadata. Runs alongside the existing iOS Podfile
  cocoapods-spm injection.

Verified
- ./gradlew :app:assembleDebug → BUILD SUCCESSFUL.
- expo prebuild --platform android applies the desugar injection cleanly.

Not yet verified (needs your machine):
- Highlighting actually renders correctly per language. Test plan: cycle
  through the language chips in App.android.tsx and confirm token
  colours change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(android): swap dark_modern (JSONC) → darcula (strict JSON)

VS Code's `dark_modern.json` uses JSONC (trailing commas). Sora's
`TMParserJSON` is strict Gson and rejects it at runtime with:

  com.google.gson.stream.MalformedJsonException: Expected name at
  line 129 column 3 path $..widget.border

Swap to `darcula.json` (taken verbatim from sora-editor's sample app —
guaranteed to parse under their parser). Theme is a one-line constant
swap; can revisit when we wire the `theme` prop sub-task in #32.

* fix(android-demo): per-language sample text

The single-sample demo masked TextMate highlighting on tab switch — a
JS-flavored blob viewed under e.g. the markdown grammar tokenises as
plain paragraph text, which looks like "lost highlighting" but is
correct grammar behaviour. Mirror App.tsx's per-language SAMPLES record
so each tab loads syntax-appropriate content.

JS-side change only; library code unchanged.

* refactor(example): share preview helpers, mirror iOS UI on Android

Bring App.android.tsx to functional parity with App.tsx (modulo the
iOS-only @expo/ui glass aesthetic):

- Extract `stripTSTypes`, `wrapJSConsoleHTML`, `wrapMarkdownHTML` into
  `previewHelpers.ts`. Both apps import from there. Pure string-builders,
  no React/RN deps — safe to share.
- App.android.tsx now matches iOS in functionality:
  - Per-language sample text (already landed in the previous commit).
  - Source/Preview segmented toggle in the status bar, gated on
    previewable languages (markdown / html / js / ts).
  - WebView-rendered preview using the shared helpers (markdown via
    marked, HTML rendered as-is, JS executed in-browser, TS stripped
    then executed).
  - Auto-blur the editor when switching to preview.
  - Drop back to source view automatically when switching to a
    non-previewable language.

Skipped intentionally on Android (props are still ViewManager no-ops on
Android until their respective PRs against #32 land):
- lineNumbers toggle UI
- font / theme / contentInsets toolbar pieces
- @expo/ui glass-effect aesthetic — `@expo/ui/jetpack-compose` exposes
  a different component vocabulary (Switch / SegmentedButton / Chip vs
  Toggle / Picker / GlassEffectContainer), so true UI sharing isn't
  possible without a thicker abstraction layer than this PR warrants.

* feat(android): font, contentInsets, lineNumbers props (closes #37, refs #32)

Replace the three style/layout no-op stubs in SourceEditorViewManager
with real wiring through SourceEditorView setters. Demo app gains a
matching `#` toggle in the status bar to flip the gutter at runtime.

Library
- SourceEditorView.kt:
  - setFont: maps `{family,size}` → `editor.typefaceText` +
    `editor.setTextSize`. Defaults to MONOSPACE at 14pt to match Apple's
    `monospacedSystemFont(ofSize: 14)` when family/size are absent.
  - setContentInsets: maps `{top,bottom,left,right}` (RN dp) → raw px
    via display-metrics, then `View.setPadding(left,top,right,bottom)`.
    Mirrors UIEdgeInsets semantics — applied to the editor's text
    container.
  - setLineNumbers: `editor.isLineNumberEnabled = value`. Sora ships the
    gutter ON by default; this lets the prop turn it off.
- SourceEditorViewManager.kt: forward each of the three props to the
  view setters. Only `setTheme` remains a no-op; that's the next sub-PR.

Demo
- App.android.tsx: pass `lineNumbers`, `font={size:13}`, and
  `contentInsets={top:12,bottom:12,left:8,right:8}` to SourceEditor —
  matches the iOS App.tsx defaults. Status bar gains a `#` toggle that
  flips lineNumbers state, mirroring iOS's Toggle(systemImage="number").

Verified
- ./gradlew :app:assembleDebug → BUILD SUCCESSFUL
- npm run typecheck clean

Stacked on top of feat/android-highlighting (PR #36); merge that first
to get a clean diff against develop here.

* feat(android): theme prop — light / dark / auto (closes #39, refs #32)

Wire the `theme` prop on Android. Bundles a light TextMate theme
(quietlight.json from Sora's sample app) alongside the existing
darcula.json. SoraTextMate now loads both at setup and exposes
applyTheme() that flips ThemeRegistry's active scheme + re-applies
TextMateColorScheme on the editor.

- 'light' / 'dark' map to the bundled themes by name.
- 'auto' (and any unknown value) reads
  Configuration.UI_MODE_NIGHT_MASK to pick between them — same
  semantics as the iOS overrideUserInterfaceStyle = .unspecified path.

Refactor: SoraTextMate.apply → applyLanguage / applyTheme, since the
two prop concerns now diverge. Caller code in SourceEditorView updated
accordingly.

Demo: App.android.tsx gains a tiny A/L/D segmented picker in the status
bar to exercise the prop. Initial state is 'auto'.

Verified: ./gradlew :app:assembleDebug → BUILD SUCCESSFUL,
npm run typecheck clean.

Stacked on feat/android-style-props (#38). Merge order: #36#38 →
this.

* ci: add android + macos build jobs, fix iOS paths (closes #41, refs #32)

CI hadn't run since the ios-app → expo-app rename (PR #34) and the
slug change (SourceEditorExampleiOS → SourceEditorExample). Latent
breakage; this catches it up plus adds the two missing platforms.

build-android (NEW)
- ubuntu-latest, JDK 17 (Sora's language-textmate AAR + the gradle
  plugin chain require it).
- android-actions/setup-android@v3 with platforms;android-35 +
  build-tools;35.0.0 to match android/build.gradle's
  compileSdkVersion 35.
- expo prebuild --platform android — runs our app.plugin.js
  withAppBuildGradle modifier, injects coreLibraryDesugaring into
  android/app/build.gradle.
- ./gradlew :app:assembleDebug.

build-macos (NEW — replaces the placeholder comment)
- macos-15, latest-stable Xcode (matches build-ios).
- gem install cocoapods only — macos-app uses RN's first-party
  spm_dependency helper, no cocoapods-spm needed.
- pod install + xcodebuild -workspace MacosApp.xcworkspace.

build-ios (FIX)
- example/ios-app → example/expo-app paths.
- xcworkspace + scheme renamed: SourceEditorExampleiOS →
  SourceEditorExample (driven by app.json name change in PR #34).

typecheck (FIX)
- example/ios-app → example/expo-app path for the example typecheck.

* docs: Android shipping across README + docs/* + CHANGELOG (closes #43) (#44)

After this lands, every code-side and docs-side sub-task on #32 is
done; the tracking issue closes.

README.md
- Platform table: Android → "Shipping (Fabric / New Architecture)" at
  API 24. Drop "Stub only" row.
- Status section: name all three platforms.
- Quick start: clarify cocoapods-spm is iOS-only; macOS uses RN's
  first-party SPM, Android pulls Sora-Editor from Maven Central.
- Plugin snippet: add expo-build-properties android entries
  (minSdkVersion + newArchEnabled).
- Roadmap: drop "Android (TextKit alternative)" — it's done.
- Repo layout: describe android/ proper + the Expo CNG-shared
  example/expo-app/ (was example/ios-app/).
- Add "Running the Android example" section mirroring iOS scripts.
- License section: add Sora-Editor + LGPL boundary note.

docs/installation.md
- Add Android row to deployment-targets table (API 24).
- New "Backing libraries" subsection summarising STTextView vs Sora.
- Scope the SPM bridging section explicitly to iOS / macOS.
- New "Android (Expo CNG)" section: documents the autolinking-from-
  Maven flow + the coreLibraryDesugaring requirement that our config
  plugin auto-injects.
- New "Bare React Native — Android" section with the manual
  build.gradle snippet for non-Expo consumers.
- New "License note for Android" calling out the LGPL inheritance.
- Working examples block: ios-app → expo-app, add Android.

docs/usage.md
- `language` prop note: spell out the Android grammar source (VS Code
  TextMate via Sora) and the HTML caveat (no nested CSS/JS recursion
  on Android, unlike STTextView's attributed text).
- `lineNumbers` note: now applies to all platforms.
- `contentInsets` note: spell out the Android dp→px mapping.

docs/examples.md
- Add Android row to platform table — same expo-app/ project as iOS.
- Refresh layout/toolchain narrative for the Expo-shared model.
- Add android:run script line.

CHANGELOG.md
- Unreleased entry covering #34, #36, #38, #40, #42 plus the
  ios-app→expo-app rename.

Stacked on feat/ci-android (#42); merge order: #36#38#40#42
→ this.

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android: Fabric skeleton + MVP wiring (Sora-Editor)

1 participant