ADFA-3546: Add code snippet support for plugin api#1162
ADFA-3546: Add code snippet support for plugin api#1162Daniel-ADFA wants to merge 3 commits intostagefrom
Conversation
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.
|
Warning Rate limit exceeded
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 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 configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughIntroduces 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
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
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)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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
Exceptioncan 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 inpreDestroy().The listener is registered at line 622 but never cleared. The method signature confirms
setSnippetRefreshListener()accepts null, andpreDestroy()already follows a cleanup pattern for other listeners (e.g., drawer listener, bottom sheet callback). Add the following topreDestroy():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
📒 Files selected for processing (13)
app/src/main/java/com/itsaky/androidide/activities/editor/BaseEditorActivity.ktapp/src/main/java/com/itsaky/androidide/handlers/SnippetHandler.ktcommon/src/main/java/com/itsaky/androidide/utils/Environment.javalsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetParser.ktlsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/SnippetRegistry.ktlsp/api/src/main/java/com/itsaky/androidide/lsp/snippets/UserSnippetLoader.ktlsp/java/src/main/java/com/itsaky/androidide/lsp/java/providers/snippet/JavaSnippetRepository.ktlsp/xml/src/main/java/com/itsaky/androidide/lsp/xml/providers/snippet/XmlSnippetRepository.ktplugin-api/src/main/kotlin/com/itsaky/androidide/plugins/extensions/SnippetExtension.ktplugin-api/src/main/kotlin/com/itsaky/androidide/plugins/services/IdeSnippetService.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/core/PluginManager.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/services/IdeSnippetServiceImpl.ktplugin-manager/src/main/kotlin/com/itsaky/androidide/plugins/manager/snippets/PluginSnippetManager.kt
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