Skip to content

Diagram Server Communication ‐ Architectural Overview

Max Kasperowski edited this page Oct 10, 2024 · 6 revisions

Architectural View of an example language Extension and KLighD (with Lingua Franca)

klighd-lf-architecture

Acronyms:

LF - Lingua Franca
LS - Language Server
DLS - Diagram Language Server
LSP - Language Server Protocol
DSP - Diagram Server Protocol

Links to Code:

  1. https://github.com/lf-lang/lingua-franca/blob/108d4e4750283af21d200dbdf8e8ab6c2d70a61b/lsp/src/main/java/org/lflang/diagram/lsp/LFLanguageServerExtension.java#L49
  2. https://github.com/lf-lang/vscode-lingua-franca/blob/052e6ef8eebf8e8be8f42f7e83f54608df98d4de/src/build_commands.ts#L39C79-L39C79
  3. https://github.com/kieler/KLighD/blob/master/plugins/de.cau.cs.kieler.klighd.lsp/src/de/cau/cs/kieler/klighd/lsp/KGraphDiagramServer.xtend
  4. https://github.com/kieler/klighd-vscode/blob/main/packages/klighd-core/src/diagram-server.ts

Generic Architectural Overview of the diagram generation with the KLighD VS Code Extension

architecture

Diagram Server API Reference

The KLighD diagram server can be used to generate diagrams in VS Code or plain web sites using the KLighD CLI, and the communication API necessary is presented here. Example implementations are the Java Language Server and a minimal Python Language Server.

Initialization

initialize message

A typical language server initialization sequence looks like this, where KLighD initializes the socket connection (by default on port 5007) with the default LSP initialize message.

{
  "jsonrpc": "2.0",
  "id": 0,
  "method": "initialize",
  "params": {
    "processId": null,
    "workspaceFolders": null,
    "rootUri": null,
    "clientInfo": { "name": "webview" },
    "capabilities": {},
    "initializationOptions": {
      "clientDiagramOptions": ClientDiagramOptions
    }
  }
}

TODO: during initialization, the server should be able to send if it can do automatic layout in the LSP capabilities message, with false being the default. We envision that the language server tells the client if it can do layout. If it cannot do layout, the client will activate layout. If it can, the layout will be done on the server.

ClientDiagramOptions is a collection of options stored by the client that the server should consider in its diagram generation:

{
    "preference": {
        # collection of key-value pairs, representing the option's ID and its current value.
    },
    "render": {
        # collection of key-value pairs, representing the option's ID and its current value.
    },
    "synthesis": {
        # collection of key-value pairs, representing the option's ID and its current value. These are the only really important ones for the diagram synthesis.
    }
}

keith/preferences/setPreferences message

After that and some default LSP messaging, such as the initialized message, the client sets some more preferences with the keith/preferences/setPreferences message:

{
    "jsonrpc": "2.0",
    "method": "keith/preferences/setPreferences",
    "params": {
        "diagram.shouldSelectDiagram": false,
        "diagram.shouldSelectText": true,
        "diagram.incrementalDiagramGenerator": false
    }
}

The first two preferences are for interaction of the diagram with a text editor that may be open. They indicate, if a text selection should cause a diagram update with the same element selected (shouldSelectDiagram) or if a selection in the diagram should also select the corresponding region in the text (shouldSelectText). The incrementalDiagramGenerator is a preference to toggle full (false) or incremental (true) diagram generation, i.E. generation of only segments of the diagram incrementally. See the diagramPiece messages. TODO: it seems like the setPreferences message may be sent after the requestModel action in VS Code, check how this should be handled.

Basic diagram generation loop

diagram/accept message

All remaining diagram-related messages are sent via Sprotty's action framework. Each action uses the new diagram/accept LSP message:

{
  "jsonrpc": "2.0",
  "method": "diagram/accept",
  "params": {
    "clientId": "sprotty",
    "action": (Sprotty) Action
  }
}

requestModel Action

Whenever the client requests a diagram, it sends Sprotty's requestModel action like this:

{
  "kind": "requestModel",
  "options": {
    "needsClientLayout": true,
    "needsServerLayout": false,
    "sourceUri": "file:///path/to/your/file.kgt",
    "diagramType": "keith-diagram"
  },
  "requestId": "client_1"
}

The meaning of each parameter is as follows:

  • needsClientLayout: If the (KLighD-) client performs the layout. See the capabilities message above.
  • needsServerLayout: If the language server performs the layout. Only reasonable for Java or JavaScript language servers using ELK or elkjs. See the capabilities message above.
  • sourceUri: The URI of the model for that the server should provide a diagram. The file extension may be used to only show available syntheses for the model type given by that extension. Here an example as a textual KGraph file (.kgt).
  • diagramType: Sprotty-internal type. It is always "keith-diagram" for any KLighD diagram.
  • requestId: An identifier to match future diagram requests and updates to, so the server can handle multiple diagrams at once.

setSyntheses Action

After this request, the server needs to answer with the following three messages. First, an action notifying the client on the available syntheses for the requested diagram:

{
  "kind": "setSyntheses",
  "syntheses": [
    Synthesis
  ]
}

with each Synthesis being defined by an internal ID and a display name for the UI. An example for the synthesis for KGraphs in KLighD would result in this message:

{
  "id": "de.cau.cs.kieler.graphs.klighd.syntheses.KGraphDiagramSynthesis",
  "displayName": "KGraphDiagramSynthesis"
}

The server can also decide on its own that a new diagram should be sent, starting this same sequence of actions. A use case might be a model change, requiring a new diagram to be drawn for it.

updateOptions Action

Second, an action notifying the client on the available options for the chosen synthesis:

{
  "kind": "updateOptions",
  "valuedSynthesisOptions": [
    SynthesisOption
  ],
  "layoutOptions": [
    LayoutOption
  ],
  "actions": [
    (KLighD) Action
  ],
  "modelUri": "file:///path/to/your/file.kgt"
}

with SynthesisOption being defined like these examples:

{
  "synthesisOption": {
    "id": "de.cau.cs.kieler.graphs.klighd.syntheses.KGraphDiagramSynthesis.CHECK-1675366116",
    "name": "Suppress Edge Adjustement",
    "type": 0,
    "sourceHash": 1197289400,
    "initialValue": true
  },
  "currentValue": true
}
{
  "synthesisOption": {
    "id": "de.cau.cs.kieler.graphs.klighd.syntheses.KGraphDiagramSynthesis.CHOICE227839009",
    "name": "Default Values",
    "type": 1,
    "sourceHash": 1506798321,
    "values": ["As in Model", "On", "Off"],
    "initialValue": "As in Model"
  },
  "currentValue": "As in Model"
}
{
  "synthesisOption": {
    "id": "de.cau.cs.kieler.sccharts.ui.synthesis.hooks.LabelShorteningHook.RANGE-230395005",
    "name": "Shortening Width",
    "type": 2,
    "range": { "first": 0, "second": 1000 },
    "stepSize": 10,
    "updateAction": "de.cau.cs.kieler.sccharts.ui.synthesis.hooks.LabelShorteningHook",
    "category": {
      "id": "de.cau.cs.kieler.sccharts.ui.synthesis.GeneralSynthesisOptions.CATEGORY-2025855158",
      "name": "Layout",
      "type": 5,
      "sourceHash": 1427301019,
      "initialValue": false
    },
    "sourceHash": 1170645708,
    "initialValue": 200
  },
  "currentValue": 200
}
{
  "synthesisOption": {
    "id": "TEXT-1269441222",
    "name": "Shorten IDs by",
    "type": 3,
    "description": "Shorten the IDs of all artifacts by this prefix.",
    "sourceHash": 1441271795,
    "initialValue": ""
  },
  "currentValue": "de.cau.cs.kieler."
}
TODO: SEPARATOR option example
{
  "synthesisOption": {
    "id": "de.cau.cs.kieler.sccharts.ui.synthesis.GeneralSynthesisOptions.CATEGORY-2025855158",
    "name": "Layout",
    "type": 5,
    "sourceHash": 1427301019,
    "initialValue": false
  },
  "currentValue": false
}
  • "id": unique ID of the option
  • "name": Display name for the option
  • "type": option type with the mapping {0: check, 1: choice, 2: range, 3: text, 4: separator, 5: category}
  • "sourceHash": unique hash code of the option TODO: should not be needed anymore
  • "initialValue": the initial value of the option
  • "currentValue": The currently configured value of the option
  • "category": The full JSON definition of the category of the option (optional) TODO: I think only the ID is required here
  • "description": A descriptive text describing the option on hover (optional)
  • "range": the range of the option (only range options)
  • "stepSize": the allowed step size for ranges (only range options)
  • "values": the possible values of the option (only choice options)
  • ""updateAction": ? TODO:

Two examples of LayoutOption may look like this:

{
  "optionId": "org.eclipse.elk.layered.highDegreeNodes.treatment",
  "defaultValue": { "k": false, "v": "False" },
  "type": 1,
  "name": "High Degree Node Treatment",
  "description": "Makes room around high degree nodes to place leafs and trees.",
  "choices": ["false", "true"],
  "minValue": 0.0,
  "maxValue": 100.0,
  "availableValues": { "k": [true, false], "v": ["True", "False"] }
}
{
  "optionId": "org.eclipse.elk.layered.crossingMinimization.strategy",
  "defaultValue": { "k": 0, "v": "Layer_sweep" },
  "type": 5,
  "name": "Crossing Minimization Strategy",
  "description": "Strategy for crossing minimization.",
  "choices": ["LAYER_SWEEP", "INTERACTIVE", "NONE"],
  "minValue": 0.0,
  "maxValue": 100.0,
  "availableValues": {
    "k": [1, 0],
    "v": ["Interactive", "Layer_sweep"]
  }
}

And an example of a KLighD Action looks like this:

{
  "actionId": "de.cau.cs.kieler.spviz.osgiviz.viz.actions.UndoAction",
  "displayedName": "Undo",
  "tooltipText": "Undoes the last action performed on the view model."
}

requestBounds Action

Third, the server can now send the entire KGraph to the client for (layout and) drawing. In case the client should do the layout, a requestBounds action should be sent:

{
  "kind": "requestBounds",
  "newRoot": {
    "properties": {},
    "revision": 1,
    "type": "graph",
    "id": "file:///path/to/your/file.kgt",
    "children": [
        KNode (a single root node)
    ]
  }
}

How this KNode should look like, see TODO: schema ref

setModel action

If instead the server should perform the layout, a setModel action should be sent:

{
  "clientId": "keith-diagram_sprotty",
  "action": {
    "kind": "setModel",
    "newRoot": {
      "properties": {},
      "revision": 1,
      "type": "graph",
      "id": "file:///path/to/your/file.kgt",
      "children": [
        KNode (a single root node)
      ]
    }
  }
}

updateModel action

If a diagram for the same model has been sent previously already and the a new diagram is sent, an updateModel action should be sent instead. This ensures that the client will try to animate the changes in the diagram.

{
  "kind": "updateModel",
  "newRoot": {
    "properties": {},
    "revision": 2,
    "type": "graph",
    "id": "file:///path/to/your/file.kgt",
    "children": [
      KNode (a single root node)
    ]
  }
}

Further diagram messages

checkImages action

If the graph contains any KImages, the presence of cached images needs to be synchronized before the diagram can be sent. This leads to the following message sequence, staring with a checkImages action from the server:

{
  "clientId": "keith-diagram_sprotty",
  "action": {
    "kind": "checkImages",
    "images": [
      {
        "bundleName": "de.cau.cs.kieler.klighd.ide",
        "imagePath": "icons/full/model/error_sign.png"
      }
    ]
  }
}

checkedImages action

The client responds with a checkedImages response. The k and v values uniquely identify the image.

{
  "clientId": "keith-diagram_sprotty",
  "action": {
    "kind": "checkedImages",
    "notCached": [
      {
        "k": "de.cau.cs.kieler.klighd.ide",
        "v": "icons/full/model/error_sign.png"
      }
    ],
    "responseId": ""
  }
}

storeImages action

If the checkedImages action contains any non-cached images, the server sends base-64 versions of the images to the client in a storeImages action. TODO: other image formats are possible as well with no bundlePath/imagePath but other refs.

{
  "kind": "storeImages",
  "images": [
    {
      "k": {
        "k": "de.cau.cs.kieler.klighd.ide",
        "v": "icons/full/model/error_sign.png"
      },
      "v": "iVBORw0[.....full base-64 encoding of the image...]CYII\u003d"
    }
  ]
}

requestDiagramPiece action

If the incrementalDiagramGenerator preference is turned on (this currently is a debug option and not fully supported yet), the diagram generation starts incrementally sending diagram pieces. The first message in the diagram generation loop are the same as without the incremental strategy. After the initial setModel or updateModel action, that only contains the root node, further messages are requested and sent. The client may request new diagram pieces based on their ID with the requestDiagramPiece action:

{
  "kind": "requestDiagramPiece",
  "requestId": "client_31",
  "modelElementId": "$root"
}

setDiagramPiece action

This is then responded by the server with this message:

{
  "kind": "setDiagramPiece",
  "diagramPiece": {
    KNode
  }
}

This message contains the requested element and its immediate children, but nothing further of the graph. The client will integrate this into the current diagram. After this, the client will request diagram pieces for all new graph IDs, expecting an answer, incrementally building up the diagram.

general/sendMessage message

If the server wants to notify something, that will be shown in the KLighD VS Code extension, it can be sent via this message.

{
  "jsonrpc": "2.0",
  "method": "general/sendMessage",
  "params": [
    "java.lang.Exception: uh oh, an exception!\n\u0026nbsp;\u0026nbsp;\u0026nbsp;\u0026nbsp;at de.cau.cs.kieler.sccharts.ui.synthesis.SCChartsSynthesis.transform(SCChartsSynthesis.java:472)\n",
    "error"
  ]
}

type is one of "info", "warn", and "error".

TODO: general/replaceContentInFile TODO: all the interactive messages/actions TODO: all the structure-based editing messages (not yet on main)

Interaction between diagram and text

diagram/openInTextEditor message

After the client sent an elementSelectedAction (see below), the server might answer with a Sprotty diagram/openInTextEditor message. This message traces the selected elements back to the ranges in the textual definition of the source that correspond to the selected element. This message is only sent if the diagram.shouldSelectText preference is true.

{
  "jsonrpc": "2.0",
  "method": "diagram/openInTextEditor",
  "params": {
    "location": {
      "uri": "file:///path/to/your.file",
      "range": {
        "start": { "line": 4, "character": 8 },
        "end": { "line": 4, "character": 13 }
      }
    },
    "forceOpen": false
  }
}

textDocument/documentHighlight

The other way around, clicking on elements in the text on the client causes a textDocument/documentHighligh message, to which the server may respond to with a diagram selection of the corresponding diagram elements. It should only be handled this way, if the diagram.shouldSelectDiagram preference is true.

{
  "jsonrpc": "2.0",
  "id": 29,
  "method": "textDocument/documentHighlight",
  "params": {
    "textDocument": {
      "uri": "file:///path/to/your.file"
    },
    "position": { "line": 4, "character": 12 }
  }
}

This could lead to a sequence of responses by the server using default Sprotty selection actions like these to deselect everything and after that only select the clicked element, centering that clicked element as well:

{
  "clientId": "keith-diagram_sprotty",
  "action": { "kind": "allSelected", "select": false }
}
{
  "clientId": "keith-diagram_sprotty",
  "action": {
    "kind": "elementSelected",
    "selectedElementsIDs": ["$root$NMOTOR"],
    "deselectedElementsIDs": [],
    "preventOpenSelection": true
  }
}
{
  "clientId": "keith-diagram_sprotty",
  "action": {
    "kind": "fit",
    "elementIds": ["$root$NMOTOR"],
    "maxZoom": 1.0,
    "animate": true
  }
}

Interaction with the diagram

Other Sprotty actions, such as elementSelected action

As an example, there are other actions defined and implemented by Sprotty, to that the server may react to. One of them is the elementSelected action, as shown here:

{
  "kind": "elementSelected",
  "selectedElementsIDs": ["$root$NMOTOR"],
  "deselectedElementsIDs": ["$root$NMOTOR$NSetSpeeds$NSetSpeeds$NProcessInputs"]
}

For more information on such messages, see Sprotty's action implementations.

performAction action

The diagram's renderings may have actions associated with them that should be executed when clicking on them. This message is sent by the client when the action is clicked on the client and should be executed by the server.

{
  "kind": "performAction",
  "actionId": "de.cau.cs.kieler.sccharts.ui.synthesis.hooks.actions.MemorizingExpandCollapseAction",
  "kGraphElementId": "$root$NMOTOR$NSetSpeeds$NSetSpeeds$NProcessInputs",
  "kRenderingId": "$root$NMOTOR$NSetSpeeds$NSetSpeeds$NProcessInputs$$$R2$R0",
  "revision": 1
}

The IDs identify the action on the server (as sent in the rendering of the model), and the corresponding graph element and rendering.

Interaction with the sidebar

  • Whenever a preference is changed in the diagram sidebar, a keith/preferences/setPreferences message is sent, see above definition.

refreshLayout action

This action can be issued by the sidebar and should lead to the server re-layout the current diagram and send it back to the client. This message should therefore skip re-generating the model and only do the layout step after (see the architectural overview above.) This message is only sensible to be handled if needsServerLayout is true.

{
  "clientId": "keith-diagram_sprotty",
  "action": { "kind": "refreshLayout" }
}

refreshDiagram action

This action should lead to the diagram to be fully re-generated.

{
  "clientId": "keith-diagram_sprotty",
  "action": { "kind": "refreshDiagram" }
}

setSynthesis action

The user can request a new diagram using a different synthesis via the sidebar. This issues a setSynthesis action:

{
  "clientId": "keith-diagram_sprotty",
  "action": {
    "kind": "setSynthesis",
    "id": "de.cau.cs.kieler.klighd.ide.syntheses.EObjectFallbackSynthesis"
  }
}

This should then trigger a new diagram generation loop using this synthesis.

keith/diagramOptions/setSynthesisOptions method

To change an option, the client may send this message. Most important, next to identifying the option, is the new currentValue of the option.

{
  "jsonrpc": "2.0",
  "method": "keith/diagramOptions/setSynthesisOptions",
  "params": {
    "synthesisOptions": [
      SynthesisOption
    ],
    "uri": "file:///path/to/your.file"
  }
}

keith/diagramOptions/performAction message

KLighD actions can also be performed by clicking on a button on the sidebar, if the updateOptions action contains such actions.

{
  "jsonrpc": "2.0",
  "method": "keith/diagramOptions/performAction",
  "params": {
    "actionId": "de.cau.cs.kieler.spviz.osgiviz.viz.actions.UndoAction",
    "uri": "file:///path/to/your.file"
  }
}

keith/diagramOptions/setLayoutOptions message

Layout options can also be changed from the sidebar, if the updateOptions action contains such layout options.

{
  "jsonrpc": "2.0",
  "method": "keith/diagramOptions/setLayoutOptions",
  "params": {
    "layoutOptions": [
      {
        "optionId": "org.eclipse.elk.layered.highDegreeNodes.treatment",
        "value": true
      }
    ],
    "uri": "file:///path/to/your.file"
  }
}