Skip to content

ADFA-3546: Add code snippet support for plugin api#1162

Open
Daniel-ADFA wants to merge 3 commits intostagefrom
ADFA-3546
Open

ADFA-3546: Add code snippet support for plugin api#1162
Daniel-ADFA wants to merge 3 commits intostagefrom
ADFA-3546

Conversation

@Daniel-ADFA
Copy link
Copy Markdown
Contributor

Adds a snippets plugin that lets users create, edit, and delete custom code completion snippets through a Material 3 editor tab. Snippets are stored in .codeonthego/snippets.json per project and bootstrapped with useful Java defaults on first use. Backs into a new SnippetRegistry and IdeSnippetService for live snippet registration across built-in, user, and plugin sources.

Screen.Recording.2026-04-08.at.14.30.09.mov

  Adds a snippets plugin that lets users create, edit, and delete custom
  code completion snippets through a Material 3 editor tab. Snippets are
  stored in .codeonthego/snippets.json per project and bootstrapped with
  useful Java defaults on first use. Backs into a new SnippetRegistry and
  IdeSnippetService for live snippet registration across built-in, user,
  and plugin sources.
  Adds a snippets plugin that lets users create, edit, and delete custom
  code completion snippets through a Material 3 editor tab. Snippets are
  stored in .codeonthego/snippets.json per project and bootstrapped with
  useful Java defaults on first use. Backs into a new SnippetRegistry and
  IdeSnippetService for live snippet registration across built-in, user,
  and plugin sources.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 8, 2026

Warning

Rate limit exceeded

@Daniel-ADFA has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 7 minutes and 8 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 7 minutes and 8 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e46fd592-78d5-446b-a0a2-cc64d0ee68c6

📥 Commits

Reviewing files that changed from the base of the PR and between be32977 and ee1df91.

📒 Files selected for processing (6)
  • app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt
  • app/src/main/java/com/itsaky/androidide/handlers/SnippetHandler.kt
  • lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetRegistry.kt
  • lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/UserSnippetLoader.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/snippets/PluginSnippetManager.kt
📝 Walkthrough

Walkthrough

Introduces a comprehensive snippet management system supporting user-defined and plugin-provided code snippets. Adds snippet loading at editor initialization, a centralized registry for organizing snippets across languages and scopes, user snippet file parsing, plugin snippet contribution and runtime refresh capabilities, and UI initialization hooks.

Changes

Cohort / File(s) Summary
Core Snippet Registry & Management
lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetRegistry.kt, lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/UserSnippetLoader.kt, lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetParser.kt
New SnippetRegistry singleton manages built-in, user, and plugin snippets with read/write locking. UserSnippetLoader loads user snippets from SNIPPETS_DIR per language. SnippetParser extended with parseFromReader() for flexible JSON parsing.
Snippet Handler & Initialization
app/src/main/java/com/itsaky/androidide/handlers/SnippetHandler.kt, app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt, common/src/main/java/com/itsaky/androidide/utils/Environment.java
New SnippetHandler singleton centralizes loading of user and plugin snippets. BaseEditorActivity calls snippet loading on onCreate. Environment adds SNIPPETS_DIR directory.
Language Repository Updates
lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt, lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/snippet/XmlSnippetRepository.kt
Both repositories refactored to use SnippetRegistry.initBuiltIn() and compute snippets on access via SnippetRegistry.getSnippets() instead of eager initialization.
Plugin Snippet Contracts
plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/SnippetExtension.kt, plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeSnippetService.kt
New extension interface SnippetExtension and data class SnippetContribution allow plugins to provide snippets. New service interface IdeSnippetService enables runtime snippet refresh.
Plugin Manager Integration
plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/snippets/PluginSnippetManager.kt, plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeSnippetServiceImpl.kt
PluginManager registers SnippetExtension implementations, manages plugin activation/unload hooks, and registers IdeSnippetService. New PluginSnippetManager singleton manages plugin snippet contributions with thread-safe maps. IdeSnippetServiceImpl provides refresh callback mechanism.

Sequence Diagram(s)

sequenceDiagram
    participant BaseEditor as BaseEditorActivity
    participant SnippetHandler as SnippetHandler
    participant UserLoader as UserSnippetLoader
    participant Registry as SnippetRegistry
    participant PluginMgr as PluginSnippetManager

    BaseEditor->>+SnippetHandler: loadUserSnippets()
    SnippetHandler->>+UserLoader: loadUserSnippets(language, scopes)
    UserLoader-->>-SnippetHandler: Map[scope -> List<ISnippet>]
    SnippetHandler->>+Registry: registerUserSnippets(...)
    Registry-->>-SnippetHandler: (registered)

    BaseEditor->>+SnippetHandler: loadPluginSnippets()
    SnippetHandler->>+PluginMgr: getAllSnippets()
    PluginMgr-->>-SnippetHandler: Map[pluginId -> List<SnippetContribution>]
    SnippetHandler->>+Registry: registerPluginSnippets(...)
    Registry-->>-SnippetHandler: (registered)

    BaseEditor->>PluginMgr: setSnippetRefreshListener(callback)
    Note over PluginMgr: Runtime refresh triggered by plugin updates
Loading
sequenceDiagram
    participant Plugin as SnippetExtension (Plugin)
    participant IdeService as IdeSnippetService
    participant IdeImpl as IdeSnippetServiceImpl
    participant PluginMgr as PluginSnippetManager
    participant Registry as SnippetRegistry
    participant Callback as BaseEditor Callback

    Plugin->>IdeService: refreshSnippets(pluginId)
    IdeService->>+IdeImpl: refreshSnippets(pluginId)
    IdeImpl->>+PluginMgr: refreshPlugin(pluginId)
    PluginMgr->>Plugin: getSnippetContributions()
    Plugin-->>PluginMgr: List<SnippetContribution>
    PluginMgr-->>-IdeImpl: updated contributions
    IdeImpl->>Callback: invoke(pluginId)
    Callback->>+Registry: getSnippets(language, scope)
    Registry-->>-Callback: List<ISnippet> (merged with new plugin snippets)
    IdeImpl-->>-IdeService: (refresh complete)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Suggested reviewers

  • jomen-adfa
  • itsaky-adfa

Poem

🐰 Snippets now bloom in springtime's embrace,
User and plugin contributions find their place,
Registry syncs with a lock and a key,
Runtime refresh dances wild and free,
Code snippets hop—the IDE's sweet treat! 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 15.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding code snippet support for the plugin API, which aligns with the primary objectives of the changeset.
Description check ✅ Passed The description is directly related to the changeset, detailing the snippets plugin functionality, snippet persistence, bootstrap defaults, and the underlying registry and service infrastructure introduced.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ADFA-3546

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.

@Daniel-ADFA Daniel-ADFA requested a review from jatezzz April 8, 2026 13:48
Copy link
Copy Markdown
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: 7

🧹 Nitpick comments (2)
lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/UserSnippetLoader.kt (1)

21-26: Consider narrowing the exception catch.

Catching broad Exception can mask unexpected errors. Since this parses JSON files, consider catching specific types.

♻️ Suggested narrower exception handling
 				try {
 					SnippetParser.parseFromReader(file.reader())
-				} catch (e: Exception) {
+				} catch (e: java.io.IOException) {
+					log.error("Failed to read user snippets from {}", file.absolutePath, e)
+					emptyList()
+				} catch (e: com.google.gson.JsonParseException) {
 					log.error("Failed to parse user snippets from {}", file.absolutePath, e)
 					emptyList()
 				}

Based on learnings: prefer narrow exception handling that catches only specific exception types rather than broad catch-all.

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

In
`@lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/UserSnippetLoader.kt`
around lines 21 - 26, The current catch of Exception around
SnippetParser.parseFromReader(file.reader()) in UserSnippetLoader.kt is too
broad; change it to catch only the likely parse/IO failures (e.g., IOException
and the JSON parsing exception your parser throws such as JsonParseException or
JsonSyntaxException), log the error with file.absolutePath as before and return
emptyList() for those specific exceptions, and let other unexpected exceptions
propagate (i.e., rethrow) so they aren't accidentally swallowed; update the
try-catch around SnippetParser.parseFromReader and add the appropriate imports
for the chosen exception classes.
app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt (1)

622-624: Clear the snippet refresh listener in preDestroy().

The listener is registered at line 622 but never cleared. The method signature confirms setSnippetRefreshListener() accepts null, and preDestroy() already follows a cleanup pattern for other listeners (e.g., drawer listener, bottom sheet callback). Add the following to preDestroy():

IDEApplication.getPluginManager()?.setSnippetRefreshListener(null)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt`
around lines 622 - 624, The snippet refresh listener registered via
IDEApplication.getPluginManager()?.setSnippetRefreshListener in the constructor
(which calls refreshPluginSnippets) is never cleared; update preDestroy to clear
that listener by invoking setSnippetRefreshListener with a null value on
IDEApplication.getPluginManager() so the callback is removed during cleanup
alongside the other listeners (follow the same pattern used for drawer listener
and bottom sheet callback).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@app/src/main/java/com/itsaky/androidide/handlers/SnippetHandler.kt`:
- Around line 51-60: loadUserSnippetsForLanguage currently only registers
new/updated scopes and leaves old scope entries intact; before iterating new
userSnippets call a clear operation to remove the prior snapshot for that
language (e.g. invoke a method on SnippetRegistry such as
clearUserSnippetsForLanguage(language) or remove all scopes for the language) so
deleted or emptied scopes are removed, then proceed to loop and call
SnippetRegistry.registerUserSnippets(language, scope, snippets) as before.

In `@lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetRegistry.kt`:
- Around line 70-75: The built-in init copies parsed snippet lists with
toMutableList(), which snapshots the (initially empty) lists that
SnippetParser.parse() returns and prevents later background population by
readSnippets() from becoming visible; change initBuiltIn to store the parsed
lists by reference instead of copying them (i.e., stop calling toMutableList()
and assign the lists returned by SnippetParser.parse(language, scopes) directly
into builtIn), or alternatively ensure SnippetParser.parse() returns a
thread-safe mutable list that readSnippets() updates in-place so initBuiltIn
(and the builtIn map) sees updates; update the code paths around initBuiltIn,
SnippetParser.parse, and readSnippets accordingly.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt`:
- Around line 436-438: The snippet extension registration happens after
plugin.activate(), so if a plugin calls IdeSnippetService.refreshSnippets()
during activation, PluginSnippetManager.refreshPlugin(manifest.id) finds no
entry; move the PluginSnippetManager.getInstance().registerPlugin(manifest.id,
plugin) call to occur before plugin.activate() for plugins implementing
SnippetExtension and also ensure symmetric handling in enablePlugin() and
disablePlugin() by registering on enablePlugin() and unregistering/removing in
disablePlugin() so snippet state stays in sync (refer to SnippetExtension,
PluginSnippetManager.registerPlugin/unregister or refreshPlugin, and the
plugin.activate()/enablePlugin()/disablePlugin() paths).
- Around line 485-486: The unload flow currently calls
PluginProjectManager.getInstance().cleanupPluginTemplates(pluginId) and
PluginSnippetManager.getInstance().cleanupPlugin(pluginId) but does not remove
contributions already pushed into the live SnippetRegistry, leaving stale
completions; update the unload/uninstall sequence to also remove the plugin's
snippets from the live registry by invoking the SnippetRegistry removal API
(e.g., call SnippetRegistry.getInstance().unregisterSnippetsForPlugin(pluginId)
or implement a removeSnippetsForPlugin(pluginId) method if none exists)
immediately after PluginSnippetManager.cleanupPlugin(pluginId) to ensure all
runtime snippet contributions are unregistered.
- Around line 958-969: The current refresh callback passed into
IdeSnippetServiceImpl via registerServiceWithErrorHandling uses
snippetRefreshListener directly and silently no-ops if it's null, causing early
refreshSnippets() calls to be dropped; fix this by adding a pending buffer and
draining it when a listener is attached: introduce a
pendingSnippetRefreshRequests (e.g., MutableSet<String>) in the PluginManager,
change the setRefreshCallback lambda in the IdeSnippetService registration to if
(snippetRefreshListener != null) snippetRefreshListener(pid) else
pendingSnippetRefreshRequests.add(pid), and update the code path that assigns
snippetRefreshListener to, after setting the listener, iterate and invoke the
listener for each pid in pendingSnippetRefreshRequests then clear the set so no
refreshes are lost.

In
`@plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/snippets/PluginSnippetManager.kt`:
- Around line 40-53: refreshPlugin reads the extension while
synchronized(contributions) then calls extension.getSnippetContributions()
outside the lock and unconditionally writes contributions[pluginId] = snippets,
which can recreate contributions for a plugin that was cleaned up; after
retrieving snippets but before writing, re-acquire the same
synchronized(contributions) lock and verify the plugin is still registered by
checking extensions[pluginId] (or that contributions does not indicate cleanup)
and only then assign contributions[pluginId] = snippets, otherwise skip the
write and return emptyList; reference the refreshPlugin function, the extensions
map, the contributions map and cleanupPlugin in your change.
- Around line 25-36: In registerPlugin(pluginId: String, extension:
SnippetExtension) ensure the extension is added even if
getSnippetContributions() throws: call extensions[pluginId] = extension before
or regardless of the try/catch, and on exception set snippets to an empty
collection (e.g., emptyList()) instead of returning; then proceed to the
synchronized(contributions) block to assign contributions[pluginId] = snippets
and log the error; this preserves the extension entry so refreshPlugin(pluginId)
can recover later.

---

Nitpick comments:
In
`@app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt`:
- Around line 622-624: The snippet refresh listener registered via
IDEApplication.getPluginManager()?.setSnippetRefreshListener in the constructor
(which calls refreshPluginSnippets) is never cleared; update preDestroy to clear
that listener by invoking setSnippetRefreshListener with a null value on
IDEApplication.getPluginManager() so the callback is removed during cleanup
alongside the other listeners (follow the same pattern used for drawer listener
and bottom sheet callback).

In
`@lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/UserSnippetLoader.kt`:
- Around line 21-26: The current catch of Exception around
SnippetParser.parseFromReader(file.reader()) in UserSnippetLoader.kt is too
broad; change it to catch only the likely parse/IO failures (e.g., IOException
and the JSON parsing exception your parser throws such as JsonParseException or
JsonSyntaxException), log the error with file.absolutePath as before and return
emptyList() for those specific exceptions, and let other unexpected exceptions
propagate (i.e., rethrow) so they aren't accidentally swallowed; update the
try-catch around SnippetParser.parseFromReader and add the appropriate imports
for the chosen exception classes.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: ca5e7c6b-3866-4508-a915-0ceb200e03cc

📥 Commits

Reviewing files that changed from the base of the PR and between 82c6e60 and be32977.

📒 Files selected for processing (13)
  • app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.kt
  • app/src/main/java/com/itsaky/androidide/handlers/SnippetHandler.kt
  • common/src/main/java/com/itsaky/androidide/utils/Environment.java
  • lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetParser.kt
  • lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetRegistry.kt
  • lsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/UserSnippetLoader.kt
  • lsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.kt
  • lsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/snippet/XmlSnippetRepository.kt
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/SnippetExtension.kt
  • plugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeSnippetService.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeSnippetServiceImpl.kt
  • plugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/snippets/PluginSnippetManager.kt

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.

2 participants