Skip to content

Instantly share code, notes, and snippets.

@bash-spbu
Last active April 23, 2022 17:50
Show Gist options
  • Save bash-spbu/dcdea6791aa89d9607db51f78f2a9bd8 to your computer and use it in GitHub Desktop.
Save bash-spbu/dcdea6791aa89d9607db51f78f2a9bd8 to your computer and use it in GitHub Desktop.
IntelliJ Diagrams API high-level overview

Use this extension point to implement diagramming support for a framework, language or technology.

Integration with the underlying library

We are using the 3-rd party library for graphs rendering. We have our own data models, node view (Swing-based) components and actions, so we use the library mostly for:

  • graph elements movement support,
  • incremental rendering,
  • [edges layout and rendering](com.intellij.openapi.graph.services.GraphEdgeRealizerService),
  • [user interaction and events handling](com.intellij.openapi.graph.view.StandardGraphViewControllerBuilder),
  • different, powerful and customizable [graph layout algorithms](com.intellij.openapi.graph.services.GraphLayoutService),
  • [com.intellij.openapi.graph.services.GraphAnimationService animations support],
  • snapping lines (which are drawn for alignment when dragging nodes) calculation and rendering,
  • [overview window support](com.intellij.openapi.graph.builder.components.BaseGraphStructureViewBuilder),
  • and a few other handy low-level features.

We have our own low level 1 to 1 wrappers ([com.intellij.openapi.graph]) over the library API because we publish this API and need to satisfy the policy that every published class is under our control.

One may also notice that the version of the library we use is pretty old. Moreover, the library JAR we use is slightly patched by ourselves to allow us tweaking some private implementation intrinsics we needed. So the library renewal will require a bunch of hard routine work, e.g. regenerating of all the wrappers, applying all the patches, repairing usages of the private implementation internals etc. On the other hand its renewal currently won't bring any essential benefits: there are no new important features and the current performance is fully acceptable.

Existing API variants

Historically, there are 4 ways of implementing graphs display in the IDEA platform, ordered from the lowest to the highest level:

  1. Using the low level wrappers API directly as it is done for Rider and CLion debugger parallel stacks view feature -- though this is the most flexible approach it requires thousands of lines of the boilerplate code fully tied to the library and does not provide 30-40 of common actions and features which we have for diagrams implemented with other approaches.

  2. Using the simple Model-View-Controller (MVC) wrapper over the API via [com.intellij.openapi.graph.builder.GraphBuilder]. It distinguishes [com.intellij.openapi.graph.builder.GraphDataModel data model] from [the view](com.intellij.openapi.graph.builder.GraphPresentationModel) and provides [the controller](com.intellij.openapi.graph.builder.GraphBuilder) with a bunch of the common functionality implemented. Also, user interaction and event handling are [fully implemented](com.intellij.openapi.graph.view.StandardGraphViewControllerBuilder), [structure view](com.intellij.openapi.graph.builder.components.BaseGraphStructureViewBuilder) is provided automatically along with ~30 different [common actions](com.intellij.openapi.graph.builder.actions) and finally there is a set of common utility [services](com.intellij.openapi.graph.services), including lightweight undo/redo support for graph movements via the [com.intellij.openapi.graph.services.GraphUndoService].

  3. Using this diagram API via the [DiagramProvider]. Essentially, it is an extension of the graph API with the same MVC architecture and a few additional major features supported, e.g.

    • diagram editor implementation (UmlFileEditorImpl),
    • common action for the diagram initialization and show ShowDiagramBase (either in editor or in the popup),
    • diagram [state](com.intellij.diagram.presentation.DiagramState) dump including save on the disk,
    • heavyweight undo/redo functionality (see UmlUndoableAction) via the diagram state storing/restoring for actions requiring data model rebuild,
    • toolbar actions tweaking displaying graph content implemented as [DiagramCategory] switching on/off,
    • color theme [support](DiagramColorManager),
    • common [user settings page](com.intellij.diagram.settings.DiagramConfiguration) providing,
    • high-performant UML-like view [components](com.intellij.diagram.components) with render cache,
    • and a bunch of other small things.Historically, this API was formed as generalization of the Java UML classes diagram. That is why you may find a bunch of classes named with prefix Uml and generally all the API is shifted to the UML classes diagram support (e.g [DiagramElementManager] and [components](com.intellij.diagram.components) are for UML-like diagrams only), though it can be (and it is) used for providing diagrams of any type.

    Most of the existing diagram types (~40) are successfully implemented with this API. However, it has a few fundamental problems which are hard to address at the current usage rate:

    • Data model integration is broken. An API was designed initially for a small simple UML class diagrams with fast data models: for example the core implementation uses full blocking [DiagramDataModel#refreshDataModel()] (most of the time on EDT) on nearly all simple action, e.g. even node movement undo requires full model rebuild. That's unacceptable for more heavy and time-consuming data models, e.g. such with Gradle, microservices etc. [DiagramDataModel.AsyncDataLoader#refreshDataModelAsync(ProgressIndicator)] has been introduced at least for the initial diagram build, but still there are 46 usages of the [DiagramDataModel#refreshDataModel()] in the core implementation.
    • Some abstractions are broken. [DiagramElementManager] was designed to be called frequently from EDT (even inside Swing components) what breaks SlowOperations restriction. Data model items representation should be computed till the data model initialization and be its part. But the [DiagramElementManager] is a singleton on the provider level and can't be simply made as a part of the [DiagramDataModel].
    • It still requires a lot of boilerplate code: e.g. one just wants to display the graph he has and still needs to implement dozens of methods in 7 different managers. That's too much -- for simple tasks there should be simple and dense API.
    • An API is still partially tied to the library we use.That is why yet another new API has been introduced in the following point:
  4. There is a new undergoing experimental API located in [com.intellij.diagram.v2]:

    • It is designed according to the Builder pattern.
    • It is extremely decent and as easy to use as possible -- not harder than popup creation with [com.intellij.openapi.ui.popup.PopupChooserBuilder]
    • It has the simplest possible correct integration with data model of any complexity.
    • It is fully self-contained and implementation agnostic.
    • It is well-documented.
    • From the implementation side, it is still a wrapper over the existing diagram MVC API.

Diagram MVC API architecture and high-level overview

Diagrams architecture is a simple Model-View-Controller (MVC):

  • [DiagramDataModel] is data model, which is the most essential component one needs to implement in order to support new diagram type
  • [DiagramPresentationModel] is a view. Most likely a common implementation DiagramPresentationModelImpl will be enough and one won't need overriding it.
  • [DiagramBuilder] is a controller. Most likely a common implementation UmlGraphBuilder will be enough and one won't need overriding it.

Typically, all operations are lazy and cumulative, i.e. no changes would be actually applied until appropriate update methods are called:

  • [DiagramDataModel#refreshDataModel()] to reload all data elements from project
  • [DiagramPresentationModel#update()] to update view (typically edges) without nodes relayout
  • [DiagramBuilder#queryUpdate()].withDataReload().withPresentationUpdate() to update controller internal mappings and presentation model from the data model (note that it does never update [DiagramDataModel] itself, for which [DiagramDataModel#refreshDataModel()] must be invoked)

Additionally, to the MVC components there are different managers, each of which does its own task. There are 2 types of them:

  • Global are stored as a singletons for each provider (i.e. diagrams of the same provider share the same instance). These typically should not contain any state and just provide some common algorithms like resolve or items names / icons providing.
  • Local are stored as a singletons for each diagram, typically in its [DiagramDataModel]. These often contain some state like which categories and in which scope are displayed now on the diagram.

One may want to use [BaseDiagramProvider] as a base for small and simple diagrams

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment