This document describes the plugin's modular architecture for JetBrains Remote Development (Split Mode), following the official IntelliJ Platform Modular Plugin guidelines.
git-scope-pro/
├── src/main/resources/META-INF/plugin.xml ← root descriptor (metadata + <content> only)
├── shared/ ← loaded everywhere (backend + frontend)
├── backend/ ← loaded on backend (or monolithic IDE)
├── frontend/ ← loaded on frontend (or monolithic IDE)
├── build.gradle.kts ← root build (pluginModule references)
└── settings.gradle.kts ← includes shared, backend, frontend
The plugin follows the JetBrains modular plugin guidelines:
- Module descriptors in
src/main/resources/(not META-INF):gitscope.shared.xml,gitscope.backend.xml,gitscope.frontend.xml - Root plugin.xml contains only
<content>module declarations — no extensions, actions, or listeners - Backend XML declares dependencies on
intellij.platform.backend,intellij.platform.kernel.backend,gitscope.shared,Git4Idea - Frontend XML declares dependencies on
intellij.platform.frontend,gitscope.shared - Shared XML declares dependency on
intellij.platform.rpc - Content modules use
required-if-availablefor strict loading validation splitMode = trueandpluginInstallationTarget = BOTHdeclared atintellijPlatformlevelrpccompiler plugin (fleet rpc-compiler-plugin 2.3.20-0.1) applied to all modules- No external runtime dependencies in any module (classloader isolation safe)
@Rpc
interface GutterRpcApi : RemoteApi<Unit> {
suspend fun getGutterUpdates(projectId: ProjectId): Flow<GutterUpdateEvent>
}GutterUpdateEvent is a @Serializable sealed class with variants: DataUpdated, DataCleared, AllCleared.
- Implements
GutterRpcApiusingcallbackFlow - Registers listener on
GutterDataServiceBEFORE replaying existing data (no race condition) - Registered via
com.intellij.platform.rpc.backend.remoteApiProviderextension point
- Subscribes with
durable {}for automatic reconnection on network errors - Only activates in split mode (
!IdeProductMode.isMonolith) to avoid feedback loops - Publishes received data to the frontend's local
GutterDataService
@Serializable data classes: GutterFileDataDto, GutterRangeDto — contain file path, line ranges, base/head content,
scope display name, and settings state.
MyLineStatusTrackerImpl → GutterDataService.publish()
→ GutterRenderingService (direct listener, same JVM)
RPC subscription is skipped. Frontend gets data directly from the shared GutterDataService.
Backend JVM:
MyLineStatusTrackerImpl → GutterDataService.publish()
→ BackendGutterRpcImpl (callbackFlow listener) → Flow<GutterUpdateEvent> ── RPC ──→
Frontend JVM:
← durable { GutterRpcApi.getInstance().getGutterUpdates() }
→ FrontendGutterSubscriptions → GutterDataService.publish()
→ GutterRenderingService → ScopeLineStatusMarkerRenderer → paint()
On subscription (or reconnection), the backend replays all current gutter data so the frontend never misses state.
| Dependency | Before | After | Reason |
|---|---|---|---|
RxJava (io.reactivex.rxjava3) |
implementation |
Removed | Replaced with CopyOnWriteArrayList listener pattern. Module classloaders can't see lib/ JARs. |
Gson (com.google.code.gson) |
implementation |
compileOnly |
Platform bundles Gson; only needed for compilation of MyModelConverter. |
rpc compiler plugin |
— | Applied to all modules | Required for @Rpc interface codegen (remoteApiDescriptor). |
intellij.platform.rpc |
— | Shared module dependency | Provides RemoteApi, @Rpc, RemoteApiProviderService classes. |
ViewService.initLater() handles deterministic tab activation:
initTabsSequentially()creates all tabs (HEAD + saved models)tabInitializationInProgress = true— suppressesMyTabContentListenerselectTabByIndex(savedTabIndex)— selects the saved tab without side effectstabInitializationInProgress = falsesetActiveModel()— fires exactly once, triggerscollectChanges()for the correct scope
This prevents the generation counter invalidation that occurred when selectTabByIndex triggered the listener, causing
a double setActiveModel() → double incrementUpdate() → first collectChanges result discarded.
The gutter diff popup (ScopeGutterPopupPanel) closes on:
- Mouse wheel scroll —
MouseWheelListeneron editor scroll pane - Caret movement —
CaretListener(covers keyboard scrolling: arrows, Page Up/Down) - Mouse click in editor — existing
EditorMouseListener
The popup stays open during prev/next change navigation — a suppressCaretCancel flag is set during programmatic
moveToLogicalPosition + scrollToCaret calls.
All existing source files moved from src/main/java/ into subprojects. No packages renamed.
All VCS, service, model, listener, toolwindow, settings, state, and statusBar packages.
Gutter rendering: ScopeDiffViewer.kt, ScopeGutterHighlighterManager.kt, ScopeGutterPopupPanel.kt,
ScopeLineStatusMarkerRenderer.kt, LineStatusGutterMarkerRenderer.kt.
Range.kt, RangesBuilder.kt, GitScopeSettings.java, Defs.java, Notification.java.
| File | Purpose |
|---|---|
shared/src/main/java/service/GutterDataService.java |
In-process bridge: holds per-file gutter data, listener pattern for pub/sub |
shared/src/main/java/rpc/GutterRpcApi.kt |
@Rpc interface for cross-process gutter data streaming |
shared/src/main/java/rpc/GutterTopics.kt |
@Serializable DTOs for RPC transport |
shared/src/main/java/utils/SharedReflection.java |
Reflection bridge for getGutterArea() (internal API) |
backend/src/main/java/rpc/BackendGutterRpcImpl.kt |
RPC implementation + RemoteApiProvider registration |
backend/src/main/java/implementation/lineStatusTracker/MyLineStatusTrackerImpl.java |
Rewritten: computes ranges on background threads, publishes to GutterDataService |
frontend/src/main/java/gitscope/frontend/GutterRenderingService.java |
Manages renderers per document, listens to GutterDataService |
frontend/src/main/java/gitscope/frontend/GutterRenderingStartup.java |
Eager init of GutterRenderingService |
frontend/src/main/java/rpc/FrontendGutterListeners.kt |
RPC subscription with durable {}, publishes to frontend GutterDataService |
root build.gradle.kts:
pluginModule(implementation(project(":shared")))
pluginModule(implementation(project(":backend")))
pluginModule(implementation(project(":frontend")))
backend/build.gradle.kts:
intellijPlatform: intellijIdea, Git4Idea, intellij.platform.backend, kernel.backend, rpc.backend
implementation(project(":shared"))
compileOnly("com.google.code.gson:gson:2.13.2")
frontend/build.gradle.kts:
intellijPlatform: intellijIdea, intellij.platform.frontend
implementation(project(":shared"))
shared/build.gradle.kts:
intellijPlatform: intellijIdea, intellij.platform.rpc
All modules apply: org.jetbrains.intellij.platform.module, java, org.jetbrains.kotlin.jvm,
org.jetbrains.kotlin.plugin.serialization, rpc.