Skip to content

ADFA-2867: Migrate plugin tooltips into documentation.db#1019

Merged
Daniel-ADFA merged 7 commits intostagefrom
ADFA-2867
Mar 10, 2026
Merged

ADFA-2867: Migrate plugin tooltips into documentation.db#1019
Daniel-ADFA merged 7 commits intostagefrom
ADFA-2867

Conversation

@Daniel-ADFA
Copy link
Contributor

No description provided.

@Daniel-ADFA Daniel-ADFA requested review from a team and jimturner-adfa February 25, 2026 22:35
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 25, 2026

📝 Walkthrough

Release Notes: Plugin Tooltips Migration to documentation.db

Overview

Migrated plugin tooltip storage and runtime to the shared documentation.db and integrated plugin-scoped tooltip handling into the unified tooltip services.

Changes

  • Migrate plugin tooltip data and operations from per-plugin plugin_documentation.db to shared documentation.db.
  • Store plugin tooltips in existing tables: Tooltips, TooltipCategories, TooltipButtons using plugin_ category prefixes.
  • Remove legacy plugin-specific tables and tracking (PluginTooltips, PluginTooltipButtons, PluginTracking, PluginMetadata) and related lifecycle/initialization code.
  • Add one-time initialize() suspend method to perform setup and legacy plugin_documentation.db cleanup.
  • Change removePluginDocumentation(pluginId: String) -> removePluginDocumentation(pluginId: String, plugin: DocumentationExtension? = null).
  • Remove PluginTooltipManager entirely (all retrieval and UI popup wiring removed).
  • Update IdeTooltipServiceImpl:
    • Constructor now requires (context, pluginId, activityProvider).
    • Resolves context by preferring current Activity via ActivityProvider with fallback to app context.
    • Routes tooltip operations through the unified TooltipManager using plugin-specific category.
  • Update PluginDocumentationManager to operate exclusively against documentation.db and to derive per-plugin categories with plugin_ prefix.
  • Logging updated to reflect shared-database operations and plugin-scoped categories.

Risks & Best-practice Violations

  • Breaking changes:
    • PluginTooltipManager removed — any existing callers of PluginTooltipManager.getTooltip()/showTooltip() will fail to compile/run.
    • Signature change to removePluginDocumentation requires caller updates.
  • Migration and data risk:
    • Legacy plugin_documentation.db is deleted during initialization; if initialize() is not executed under a controlled migration path, existing tooltip data may be lost.
    • No explicit automatic data merge described in code — ensure migration/verification steps are executed during rollout.
  • Runtime/DI concerns:
    • IdeTooltipServiceImpl now depends on an optional ActivityProvider; if it is null or returns a destroyed Activity, tooltip operations may fail or misbehave.
    • Context resolution prefers Activity, creating potential race conditions if Activity lifecycle changes between resolution and use.
  • Testing required:
    • Verify initialize() cleanup/migration behavior and idempotency.
    • Validate tooltip retrieval/display via TooltipManager for all affected plugins and UI anchor scenarios.
    • Confirm category prefixing prevents collisions and that removal deletes related TooltipButtons and TooltipCategories.
    • Validate logging and error-handling when documentation.db is not present or accessible.
  • Backward compatibility:
    • Consumers must be updated for removed APIs and constructor/injection changes; consider a migration plan or compatibility shim if immediate consumer updates are impractical.

Action Items for Reviewers / Release

  • Ensure calling code is updated to:
    • Stop using PluginTooltipManager.
    • Instantiate IdeTooltipServiceImpl with pluginId and ActivityProvider.
    • Call initialize() during startup/migration flow before tooltip operations.
    • Update calls to removePluginDocumentation to pass the new optional parameter if needed.
  • Add/verify tests covering migration, retrieval, UI presentation, and error paths.

Walkthrough

Removes the legacy PluginTooltipManager, consolidates plugin documentation into the shared documentation.db using plugin-prefixed categories, and updates IdeTooltipServiceImpl and PluginManager to pass pluginId and an activityProvider when creating the tooltip service.

Changes

Cohort / File(s) Summary
Service Instantiation & Tooltip Service
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeTooltipServiceImpl.kt
IdeTooltipServiceImpl constructor changed to accept pluginId and activityProvider; PluginManager now instantiates it as IdeTooltipServiceImpl(context, pluginId, activityProvider). Tooltip flows now resolve activity-aware context and route requests via TooltipManager using a plugin-specific category.
Documentation DB Consolidation
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt
Plugin documentation moved into shared documentation.db tables (Tooltips, TooltipCategories, TooltipButtons) using plugin_<pluginId> category prefix; removed plugin-specific DB lifecycle and legacy tables; added initialize() and updated install/remove/verify flows to use shared schema. Public API: added initialize() and changed removePluginDocumentation(...) signature.
Legacy Tooltip System Removal
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/tooltip/PluginTooltipManager.kt
Removed entire PluginTooltipManager file and all plugin-specific tooltip data access and UI rendering logic (DB queries, popup/WebView rendering, helpers, and handlers).

Sequence Diagram(s)

sequenceDiagram
    participant PM as PluginManager
    participant ITS as IdeTooltipServiceImpl
    participant TM as TooltipManager
    participant DB as Shared documentation.db

    rect rgba(200,100,150,0.5)
    Note over PM,DB: Legacy (removed)
    PM->>PM: PluginTooltipManager handled DB reads and popup UI
    PM->>DB: Open plugin_documentation.db / query legacy tables
    DB-->>PM: Return plugin tooltip data
    PM->>PM: Inflate popup / render WebView / handle actions
    end

    rect rgba(100,150,200,0.5)
    Note over PM,DB: New Flow
    PM->>ITS: Instantiate IdeTooltipServiceImpl(context, pluginId, activityProvider)
    PM->>ITS: showTooltip(...)
    ITS->>ITS: resolvedContext() (Activity? -> Context)
    ITS->>TM: request tooltip using category "plugin_<pluginId>" and resolved context
    TM->>DB: Query Tooltips / TooltipCategories / TooltipButtons
    DB-->>TM: Return tooltip payload
    TM-->>ITS: Resolved tooltip data
    ITS->>ITS: instruct display using resolved context
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • itsaky-adfa

"🐰 I hopped through code with nimble feet,
Merged tooltips, made the DB neat,
Old popups gone, new paths in sight,
plugin_ now shines so bright,
A little thump for tidy light."

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author, so there is no description content to evaluate. Add a pull request description explaining the motivation, scope, and impact of the changes for better context during code review.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: migrating plugin tooltips into documentation.db.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ADFA-2867

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt (2)

171-229: ⚠️ Potential issue | 🟠 Major

db.insert failures are silently swallowed, corrupting the transaction.

SQLiteDatabase.insert() returns -1 on failure without throwing. Neither insertOrGetCategoryId nor insertTooltip checks this return value, so:

  • If category insertion fails, -1 is returned as the categoryId.
  • insertTooltip then writes a Tooltip row with categoryId = -1 — an invalid FK — and returns -1 to the caller.
  • installPluginDocumentation calls insertTooltipButton(db, tooltipId = -1, …), inserting a TooltipButton with an invalid tooltipId.
  • db.setTransactionSuccessful() is still called, committing corrupt data.

Both helpers should throw (or return a sentinel that the caller checks) when db.insert returns -1:

🐛 Proposed fix
 private fun insertOrGetCategoryId(db: SQLiteDatabase, category: String): Long {
     // ... existing query logic ...
     val values = ContentValues().apply {
         put("category", category)
     }
-    return db.insert("TooltipCategories", null, values)
+    val id = db.insert("TooltipCategories", null, values)
+    if (id == -1L) error("Failed to insert category '$category' into TooltipCategories")
+    return id
 }
-        return db.insert("Tooltips", null, values)
+        val id = db.insert("Tooltips", null, values)
+        if (id == -1L) error("Failed to insert tooltip '${entry.tag}' into Tooltips")
+        return id

The error() call throws IllegalStateException, which is already caught by the outer try/catch in installPluginDocumentation, so no additional error-handling plumbing is needed.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`
around lines 171 - 229, Both insertOrGetCategoryId and insertTooltip currently
ignore SQLiteDatabase.insert() return value (which is -1 on failure) and can
silently produce invalid FK rows; update both functions (insertOrGetCategoryId,
insertTooltip) to check the long returned by db.insert("TooltipCategories", ...)
and db.insert("Tooltips", ...) and if it equals -1 call error(...) (or throw
IllegalStateException) with a descriptive message so the outer
installPluginDocumentation try/catch will abort and avoid committing corrupt
rows (refer to table names "TooltipCategories" and "Tooltips" and the methods
insertOrGetCategoryId/insertTooltip to locate the change). Ensure you also check
the insert return before returning the id to callers (so insertTooltipButton is
never invoked with tooltipId == -1).

155-165: ⚠️ Potential issue | 🟡 Minor

Batch the IN (?) deletes to avoid the SQLite bound-parameter limit.

Both DELETE statements dynamically bind every tooltip ID as a separate host parameter. SQLite's SQLITE_MAX_VARIABLE_NUMBER defaults to 999 for versions prior to 3.32.0. Android devices on older API levels ship a pre-3.32.0 SQLite build, so a plugin with more than 999 tooltip IDs will cause both DELETEs to throw, rolling back the entire transaction and leaving stale data behind.

Process tooltipIds in chunks of, e.g., 500, or rewrite using a correlated subquery that avoids host parameters altogether:

♻️ Proposed fix using correlated subquery (no parameter limit)
-        if (tooltipIds.isNotEmpty()) {
-            val placeholders = tooltipIds.joinToString(",") { "?" }
-            val args = tooltipIds.map { it.toString() }.toTypedArray()
-            db.delete("TooltipButtons", "tooltipId IN ($placeholders)", args)
-            db.delete("Tooltips", "id IN ($placeholders)", args)
-        }
+        // Delete buttons for all tooltips in this category via a subquery
+        db.execSQL(
+            """
+            DELETE FROM TooltipButtons WHERE tooltipId IN (
+                SELECT T.id FROM Tooltips AS T
+                INNER JOIN TooltipCategories AS TC ON T.categoryId = TC.id
+                WHERE TC.category = ?
+            )
+            """.trimIndent(),
+            arrayOf(category)
+        )
+        db.execSQL(
+            """
+            DELETE FROM Tooltips WHERE categoryId IN (
+                SELECT id FROM TooltipCategories WHERE category = ?
+            )
+            """.trimIndent(),
+            arrayOf(category)
+        )

This also makes the earlier cursor-based ID collection unnecessary, so lines 146–165 can be removed entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`
around lines 155 - 165, The current code collects tooltipIds and builds a single
IN(...) parameter list, which can exceed SQLite's bound-parameter limit; update
the delete logic in PluginDocumentationManager to process tooltipIds in chunks
(e.g., 500) and call db.delete("TooltipButtons", "tooltipId IN ($placeholders)",
args) and db.delete("Tooltips", "id IN ($placeholders)", args) for each chunk:
inside the chunk loop build placeholders = chunk.joinToString(",") { "?" } and
args = chunk.map { it.toString() }.toTypedArray(), then perform both deletes per
chunk and finally close the cursor; keep using the existing tooltipIds list and
cursor handling but remove the single large delete so large sets won't exceed
SQLITE_MAX_VARIABLE_NUMBER.
🧹 Nitpick comments (3)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeTooltipServiceImpl.kt (1)

45-51: Inconsistent InflateException handling between the two overloads.

The 3-arg showTooltip (line 38) catches InflateException separately from Exception, while the 2-arg overload (line 48) only catches Exception. Since both delegate to the same TooltipManager.showTooltip, they're equally susceptible to InflateException. Consider making error handling consistent.

Match error handling with the 3-arg overload
     override fun showTooltip(anchorView: View, tag: String) {
         try {
             TooltipManager.showTooltip(resolvedContext(), anchorView, pluginCategory, tag)
+        } catch (e: android.view.InflateException) {
+            android.util.Log.e("IdeTooltipService", "Failed to inflate tooltip layout: $pluginCategory.$tag", e)
         } catch (e: Exception) {
             android.util.Log.e("IdeTooltipService", "Failed to show tooltip: $pluginCategory.$tag", e)
         }
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeTooltipServiceImpl.kt`
around lines 45 - 51, The 2-arg override showTooltip(anchorView: View, tag:
String) is missing the specific InflateException handling present in the 3-arg
overload; update this method so it mirrors the 3-arg overload error handling by
first catching android.view.InflateException and logging an appropriate message
(including pluginCategory and tag) and then catching Exception for other
failures; ensure you still call TooltipManager.showTooltip(resolvedContext(),
anchorView, pluginCategory, tag) and reference the same logging behavior used in
the 3-arg overload.
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt (1)

725-966: Consider deduplicating createPluginContextWithResources and createPluginContext.

These two methods share ~95% identical service-registration logic (including the tooltip service change in this PR). The only meaningful difference is the androidContext value passed to PluginContextImpl. A single private method that accepts an optional resourceContext parameter (defaulting to this.context) would eliminate ~100 lines of duplication and ensure future service changes don't need to be applied in two places.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`
around lines 725 - 966, Both createPluginContextWithResources and
createPluginContext duplicate nearly all service-registration logic; consolidate
them by creating a single private helper (e.g., createPluginContextInternal)
that takes pluginId, classLoader, permissions and an androidContext parameter
(make it optional/default to the existing context) and performs all
registerServiceWithErrorHandling calls and returns PluginContextImpl (setting
androidContext to the passed value and resources = ResourceManagerImpl(pluginId,
pluginsDir, classLoader)). Then update createPluginContextWithResources to call
this helper with resourceContext and createPluginContext to call it with the
default context (or remove those wrappers and replace callers to use the new
helper), ensuring all existing references to registerServiceWithErrorHandling,
PluginContextImpl, and ResourceManagerImpl remain unchanged.
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt (1)

300-329: O(n) database connections opened in verifyAllPluginDocumentation.

isPluginDocumentationInstalled opens and closes a fresh SQLiteDatabase connection on every call. Inside the plugin loop this produces one full open/close round-trip per plugin. For installations with many plugins this is unnecessary overhead.

Consider an internal overload that accepts an already-open SQLiteDatabase, so verifyAllPluginDocumentation (and verifyAndRecreateDocumentation) can open a single connection and pass it through:

♻️ Sketch of the refactor
+    private fun isPluginDocumentationInstalled(db: SQLiteDatabase, pluginId: String): Boolean {
+        return try {
+            val cursor = db.rawQuery(
+                "SELECT COUNT(*) FROM TooltipCategories WHERE category = ?",
+                arrayOf(pluginCategory(pluginId))
+            )
+            val installed = cursor.moveToFirst() && cursor.getInt(0) > 0
+            cursor.close()
+            installed
+        } catch (e: Exception) {
+            Log.e(TAG, "Failed to check plugin documentation for $pluginId", e)
+            false
+        }
+    }

     suspend fun verifyAllPluginDocumentation(plugins: Map<String, DocumentationExtension>): Int =
         withContext(Dispatchers.IO) {
             if (plugins.isEmpty()) return@withContext 0
             if (!isDatabaseAvailable()) { ... return@withContext 0 }
+            val db = getPluginDatabase() ?: return@withContext 0
             var recreatedCount = 0
             for ((pluginId, plugin) in plugins) {
                 try {
-                    if (!isPluginDocumentationInstalled(pluginId)) {
+                    if (!isPluginDocumentationInstalled(db, pluginId)) {
                         ...
                     }
                 } catch (e: Exception) { ... }
             }
+            db.close()
             recreatedCount
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`
around lines 300 - 329, verifyAllPluginDocumentation currently opens/closes a DB
per plugin because isPluginDocumentationInstalled creates its own SQLiteDatabase
each call; refactor by adding overloads like isPluginDocumentationInstalled(db:
SQLiteDatabase, pluginId: String) and installPluginDocumentation(db:
SQLiteDatabase, pluginId: String, plugin: DocumentationExtension), then change
verifyAllPluginDocumentation and verifyAndRecreateDocumentation to open a single
readable/writable SQLiteDatabase once, pass that db into the new overloads
inside the loop, and finally close the DB in a try/finally (or use.use {})
block; keep the original methods delegating to the new overloads to preserve
external API and ensure exception handling/logging remains intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`:
- Around line 51-53: In PluginDocumentationManager where you check
legacyDb.exists() and call legacyDb.delete(), capture the boolean result of
legacyDb.delete(); if it returns true keep the existing Log.d(TAG, "Removed
legacy plugin_documentation.db") but if it returns false call Log.w(TAG, "Failed
to remove legacy plugin_documentation.db") (or similar) so failures are logged;
reference the legacyDb variable and the delete() call to implement this
conditional logging.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeTooltipServiceImpl.kt`:
- Around line 35-43: The override of showTooltip(anchorView: View, category:
String, tag: String) ignores the incoming category and always uses
pluginCategory; update the implementation so it composes the caller-provided
category with the plugin namespace before calling TooltipManager.showTooltip
(e.g., combine pluginCategory and the category parameter into a single
namespacedCategory) so callers' intent is preserved while avoiding DB conflicts;
change the call in showTooltip to pass the composed namespacedCategory to
TooltipManager.showTooltip(resolvedContext(), anchorView, namespacedCategory,
tag) and keep the existing InflateException/Exception handling as-is.

---

Outside diff comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`:
- Around line 171-229: Both insertOrGetCategoryId and insertTooltip currently
ignore SQLiteDatabase.insert() return value (which is -1 on failure) and can
silently produce invalid FK rows; update both functions (insertOrGetCategoryId,
insertTooltip) to check the long returned by db.insert("TooltipCategories", ...)
and db.insert("Tooltips", ...) and if it equals -1 call error(...) (or throw
IllegalStateException) with a descriptive message so the outer
installPluginDocumentation try/catch will abort and avoid committing corrupt
rows (refer to table names "TooltipCategories" and "Tooltips" and the methods
insertOrGetCategoryId/insertTooltip to locate the change). Ensure you also check
the insert return before returning the id to callers (so insertTooltipButton is
never invoked with tooltipId == -1).
- Around line 155-165: The current code collects tooltipIds and builds a single
IN(...) parameter list, which can exceed SQLite's bound-parameter limit; update
the delete logic in PluginDocumentationManager to process tooltipIds in chunks
(e.g., 500) and call db.delete("TooltipButtons", "tooltipId IN ($placeholders)",
args) and db.delete("Tooltips", "id IN ($placeholders)", args) for each chunk:
inside the chunk loop build placeholders = chunk.joinToString(",") { "?" } and
args = chunk.map { it.toString() }.toTypedArray(), then perform both deletes per
chunk and finally close the cursor; keep using the existing tooltipIds list and
cursor handling but remove the single large delete so large sets won't exceed
SQLITE_MAX_VARIABLE_NUMBER.

---

Nitpick comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`:
- Around line 725-966: Both createPluginContextWithResources and
createPluginContext duplicate nearly all service-registration logic; consolidate
them by creating a single private helper (e.g., createPluginContextInternal)
that takes pluginId, classLoader, permissions and an androidContext parameter
(make it optional/default to the existing context) and performs all
registerServiceWithErrorHandling calls and returns PluginContextImpl (setting
androidContext to the passed value and resources = ResourceManagerImpl(pluginId,
pluginsDir, classLoader)). Then update createPluginContextWithResources to call
this helper with resourceContext and createPluginContext to call it with the
default context (or remove those wrappers and replace callers to use the new
helper), ensuring all existing references to registerServiceWithErrorHandling,
PluginContextImpl, and ResourceManagerImpl remain unchanged.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`:
- Around line 300-329: verifyAllPluginDocumentation currently opens/closes a DB
per plugin because isPluginDocumentationInstalled creates its own SQLiteDatabase
each call; refactor by adding overloads like isPluginDocumentationInstalled(db:
SQLiteDatabase, pluginId: String) and installPluginDocumentation(db:
SQLiteDatabase, pluginId: String, plugin: DocumentationExtension), then change
verifyAllPluginDocumentation and verifyAndRecreateDocumentation to open a single
readable/writable SQLiteDatabase once, pass that db into the new overloads
inside the loop, and finally close the DB in a try/finally (or use.use {})
block; keep the original methods delegating to the new overloads to preserve
external API and ensure exception handling/logging remains intact.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeTooltipServiceImpl.kt`:
- Around line 45-51: The 2-arg override showTooltip(anchorView: View, tag:
String) is missing the specific InflateException handling present in the 3-arg
overload; update this method so it mirrors the 3-arg overload error handling by
first catching android.view.InflateException and logging an appropriate message
(including pluginCategory and tag) and then catching Exception for other
failures; ensure you still call TooltipManager.showTooltip(resolvedContext(),
anchorView, pluginCategory, tag) and reference the same logging behavior used in
the 3-arg overload.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 34dc0fb and 1319f58.

📒 Files selected for processing (4)
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeTooltipServiceImpl.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/tooltip/PluginTooltipManager.kt
💤 Files with no reviewable changes (1)
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/tooltip/PluginTooltipManager.kt

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt (1)

80-85: ⚠️ Potential issue | 🟠 Major

Empty entry sets currently leave stale plugin docs behind.

Lines 82-85 return before Line 92 runs, so a plugin update that now exposes zero tooltip entries never clears the previously installed rows. The old documentation will keep showing until something else explicitly removes it. Treat an empty entry list as a successful uninstall inside the transaction instead of returning early.

🛠️ Suggested change
-        if (entries.isEmpty()) {
-            Log.d(TAG, "Plugin $pluginId has no tooltip entries")
-            db.close()
-            return@withContext true
-        }
-
-        Log.d(TAG, "Installing ${entries.size} tooltip entries for plugin $pluginId")
-
         db.beginTransaction()
         try {
             removePluginDocumentationInternal(db, pluginId)
+            if (entries.isEmpty()) {
+                Log.d(TAG, "Plugin $pluginId has no tooltip entries; removed existing documentation")
+                db.setTransactionSuccessful()
+                return@withContext true
+            }
+
+            Log.d(TAG, "Installing ${entries.size} tooltip entries for plugin $pluginId")
 
             val categoryId = insertOrGetCategoryId(db, pluginCategory(pluginId))

Also applies to: 90-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`
around lines 80 - 85, When plugin.getTooltipEntries() returns an empty list, do
not return early; instead perform the same removal/cleanup logic that the normal
update path uses (delete any existing docs/rows for pluginId inside the same
transaction), then close the db and return@withContext true; update the
entries.isEmpty() branch to call the delete/cleanup routine (the same method/SQL
used later in the non-empty flow) and keep db.close(), and apply the same fix to
the second identical block around the other entries.isEmpty() check.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`:
- Around line 37-39: In PluginDocumentationManager.kt update the catch-all
exception handlers in the suspend/withContext blocks (the handlers around the DB
open and the catch blocks at the sites corresponding to the earlier review) so
they do not swallow coroutine cancellation: at the start of each catch
(currently "catch (e: Exception)") rethrow if e is a CancellationException, then
catch the specific exceptions you expect from your DB operations (e.g.,
SQLiteException, SQLException, IOException) or use a more specific type instead
of Exception; apply this change to the handlers covering the open documentation
DB code and the other catch sites mentioned (the blocks that currently return
null/false), ensuring cancellation propagates while other DB errors are handled
and logged as before.

---

Outside diff comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`:
- Around line 80-85: When plugin.getTooltipEntries() returns an empty list, do
not return early; instead perform the same removal/cleanup logic that the normal
update path uses (delete any existing docs/rows for pluginId inside the same
transaction), then close the db and return@withContext true; update the
entries.isEmpty() branch to call the delete/cleanup routine (the same method/SQL
used later in the non-empty flow) and keep db.close(), and apply the same fix to
the second identical block around the other entries.isEmpty() check.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b4619493-7058-41d6-9b8e-8ca05f8a0e93

📥 Commits

Reviewing files that changed from the base of the PR and between 1319f58 and e34405e.

📒 Files selected for processing (1)
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt

# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
@jatezzz jatezzz self-requested a review March 9, 2026 20:14
Copy link
Collaborator

@jatezzz jatezzz left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Daniel-ADFA
In PluginDocumentationManager.kt you could improve the logic with something like this:

suspend fun initialize() = withContext(Dispatchers.IO) {
    val legacyDb = context.getDatabasePath("plugin_documentation.db")
    
    if (!legacyDb.exists()) return@withContext

    if (legacyDb.delete()) {
        Log.d(TAG, "Removed legacy plugin_documentation.db")
        return@withContext
    }
    Log.w(TAG, "Failed to remove legacy plugin_documentation.db")
}

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt (1)

92-115: ⚠️ Potential issue | 🟠 Major

Move beginTransaction() inside the guarded block.

db.beginTransaction() on Line 92 and Line 134 sits outside the try, so if it throws, the error bypasses your logging path and db.close() never runs. Start the transaction inside the try and only call endTransaction() when it actually began.

🛠️ Suggested pattern
-        db.beginTransaction()
-        try {
+        var transactionStarted = false
+        try {
+            db.beginTransaction()
+            transactionStarted = true
             // work
         } catch (e: Exception) {
             ...
         } finally {
-            db.endTransaction()
+            if (transactionStarted) {
+                db.endTransaction()
+            }
             db.close()
         }

Also applies to: 134-147

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`
around lines 92 - 115, The transaction is started before the try block so a
thrown exception can bypass logging/cleanup; move db.beginTransaction() into the
try right before calling removePluginDocumentationInternal(pluginId) and only
call db.endTransaction() if the transaction actually began (e.g., guard with a
boolean or check db.inTransaction if available). Update both places where
beginTransaction()/endTransaction() appear (the block that calls
removePluginDocumentationInternal, insertOrGetCategoryId, insertTooltip,
insertTooltipButton and the other similar block around lines 134–147) so
db.close() always runs in finally and endTransaction() is invoked only when a
transaction was successfully started.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`:
- Around line 90-96: The early-return branch that handles entries.isEmpty() must
call removePluginDocumentationInternal(db, pluginId) before returning so old
rows are deleted; update the logic in the installer function (the block that
currently logs "Installing ${entries.size} tooltip entries for plugin $pluginId"
and checks entries.isEmpty()) to invoke removePluginDocumentationInternal(db,
pluginId) prior to returning, and ensure the surrounding transaction started by
db.beginTransaction() is properly completed (mark transaction successful or
ended as the existing flow expects) so no stale rows remain when a plugin
updates with zero entries; reference the existing helpers insertOrGetCategoryId
and removePluginDocumentationInternal to locate where to insert the call.
- Around line 194-197: Replace the silent-failing SQLite inserts with throwing
variants so transactions abort on failure: in PluginDocumentationManager switch
the three calls that currently use db.insert(...) (the TooltipCategories insert
shown, plus the tooltip and button insert calls in the same class) to
db.insertOrThrow(...) so a SQLException is raised on failure and the existing
exception handler can roll back the transaction; ensure you don't swallow the
return values and let the exception propagate to prevent
setTransactionSuccessful() from being called on partial failures.

---

Outside diff comments:
In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt`:
- Around line 92-115: The transaction is started before the try block so a
thrown exception can bypass logging/cleanup; move db.beginTransaction() into the
try right before calling removePluginDocumentationInternal(pluginId) and only
call db.endTransaction() if the transaction actually began (e.g., guard with a
boolean or check db.inTransaction if available). Update both places where
beginTransaction()/endTransaction() appear (the block that calls
removePluginDocumentationInternal, insertOrGetCategoryId, insertTooltip,
insertTooltipButton and the other similar block around lines 134–147) so
db.close() always runs in finally and endTransaction() is invoked only when a
transaction was successfully started.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e07cc1c7-46ad-4f18-8418-f9f0c1c79284

📥 Commits

Reviewing files that changed from the base of the PR and between e34405e and 1e8d11a.

📒 Files selected for processing (1)
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/documentation/PluginDocumentationManager.kt

@Daniel-ADFA Daniel-ADFA merged commit d939eb9 into stage Mar 10, 2026
2 checks passed
@Daniel-ADFA Daniel-ADFA deleted the ADFA-2867 branch March 10, 2026 13:20
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.

3 participants