-
Notifications
You must be signed in to change notification settings - Fork 6
Diagram Server Communication ‐ Architectural Overview
LF - Lingua Franca
LS - Language Server
DLS - Diagram Language Server
LSP - Language Server Protocol
DSP - Diagram Server Protocol
- https://github.com/lf-lang/lingua-franca/blob/108d4e4750283af21d200dbdf8e8ab6c2d70a61b/lsp/src/main/java/org/lflang/diagram/lsp/LFLanguageServerExtension.java#L49
- https://github.com/lf-lang/vscode-lingua-franca/blob/052e6ef8eebf8e8be8f42f7e83f54608df98d4de/src/build_commands.ts#L39C79-L39C79
- https://github.com/kieler/KLighD/blob/master/plugins/de.cau.cs.kieler.klighd.lsp/src/de/cau/cs/kieler/klighd/lsp/KGraphDiagramServer.xtend
- https://github.com/kieler/klighd-vscode/blob/main/packages/klighd-core/src/diagram-server.ts
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.
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.
}
}
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.
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
}
}
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.
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.
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."
}
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
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)
]
}
}
}
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)
]
}
}
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"
}
]
}
}
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": ""
}
}
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"
}
]
}
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"
}
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.
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)
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
}
}
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
}
}
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.
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.
- Whenever a preference is changed in the diagram sidebar, a
keith/preferences/setPreferences
message is sent, see above definition.
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" }
}
This action should lead to the diagram to be fully re-generated.
{
"clientId": "keith-diagram_sprotty",
"action": { "kind": "refreshDiagram" }
}
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.
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"
}
}
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"
}
}
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"
}
}